Blog

Kubernetes penetration testing with AI: from a mounted token to root on the node

How an autonomous agent walks a Kubernetes cluster: RBAC abuse, ServiceAccount token theft, kubelet API, secrets, container escape, each proven when executed.

· 8 min read

A Kubernetes cluster fails in a specific, repeatable order. A workload gets a ServiceAccount it never needed. A RoleBinding grants a verb that looks harmless in isolation. A NodePort exposes an internal service to anyone who can reach the node. None of these is a vulnerability on its own, and a config scanner will happily list all three as separate low findings. The interesting part is the path that connects them, from a token mounted inside a pod to root on the node, and a scanner does not walk paths. That is the gap an autonomous agent is built to close.

This is how the Darkmoon kubernetes agent works a cluster. It runs every tool through a controlled MCP layer rather than a shell, chains each step off what the last one returned, and marks an issue EXPLOITED only when it actually executed the impact. The related docker, container-platform and container-registry agents cover the runtime and the registry side of the same attack surface. Everything below assumes an engagement you are authorized to run against a cluster you own or have written permission to test.

The cluster attack surface an autonomous agent maps first

Before any exploitation, the agent runs a preflight and a non destructive inventory. It confirms it has a working kubectl context and can reach the API server, then it enumerates the shape of the cluster: namespaces, nodes, every pod and every service. If the API server is unreachable or the kubeconfig is missing, the run stops there and says why, rather than inventing findings against an empty cluster.

kubectl get ns -o wide
kubectl get nodes -o wide
kubectl get pods -A -o wide
kubectl get svc -A -o wide

That inventory is where the surface starts to take shape. The service list exposes NodePort mappings, which are the services reachable from outside the cluster network. The pod list, described one level deeper, exposes the workloads that carry risk by construction: pods with securityContext.privileged, pods with hostPID or hostNetwork, and pods with a hostPath mount that reaches into the node filesystem. The agent records these as leads, not yet as findings, because presence is not impact. It maps them against MITRE ATT&CK for Containers so that each lead has a technique to prove.

RBAC least-privilege analysis with rbac-police and abusable ServiceAccounts

RBAC is where over permission hides in plain sight. A cluster can have hundreds of Roles, ClusterRoles and bindings, and the dangerous ones are dangerous because of a single verb. The agent dumps the full RBAC graph and then reasons over it with rbac-police and kubectl-who-can, which answer the question a human reviewer struggles with at scale: given this identity, what can it actually do, and does any of it lead somewhere it should not.

kubectl get clusterroles
kubectl get roles -A
kubectl get rolebindings -A
kubectl get clusterrolebindings

kubectl-who-can create pods
kubectl-who-can create rolebindings
kubectl-who-can '*' '*'

The verbs that matter are the ones that let an identity grant itself more. create pods in a namespace whose default ServiceAccount is over privileged is a path to that account. create or patch on rolebindings and clusterrolebindings lets an identity bind itself to a stronger role. impersonate lets it act as another subject entirely. The agent looks for these specifically, because a ServiceAccount that can create a pod and mount an admin token is one apply away from cluster-admin. That is the RBAC misconfiguration in the OWASP Kubernetes Top Ten, stated as a reachable path rather than a lint warning.

Stealing the ServiceAccount token from /var/run/secrets and what it unlocks

Kubernetes mounts a ServiceAccount token into almost every pod by default, at a fixed path. If the agent gains execution inside a workload, whether through an application flaw or an exposed NodePort service, that token is the first thing it reads.

cat /var/run/secrets/kubernetes.io/serviceaccount/token
cat /var/run/secrets/kubernetes.io/serviceaccount/namespace

# use it directly against the API server
TOKEN=$(cat /var/run/secrets/kubernetes.io/serviceaccount/token)
kubectl --token="$TOKEN" auth can-i --list

The token is a JWT, and its power is exactly the RBAC the previous phase mapped. auth can-i --list turns the stolen identity into a concrete capability list. If that account can read Secrets, the agent reads them. If it can create pods, the agent has a path to the node. If automatic token mounting was left on for an account that did not need API access, this is where a contained application compromise becomes a cluster problem. The agent treats the token as EXPLOITED only once it has used it to perform an action the account should not have been able to perform, not merely because the file was readable.

Talking to the kubelet API and the API server (kubeletctl)

Every node runs a kubelet, and the kubelet has its own API, typically on port 10250. Where that API accepts anonymous requests or a weak authorization mode, it becomes a second, lower level control plane: it can list the pods on the node and run commands inside them, without going through the API server or its audit trail at all. The agent probes it with kubeletctl.

kubeletctl -i --server <node-ip> pods
kubeletctl -i --server <node-ip> scan rce
kubeletctl -i --server <node-ip> exec "id" -p <pod> -c <container> -n <namespace>

A kubelet that answers exec anonymously is command execution in any pod on that node, which is also a route to every ServiceAccount token on that node. This is a SSRF target as much as a direct one: an application inside the cluster that can be coerced into making requests can be pointed at the kubelet, at the API server, or at a cloud metadata endpoint. We cover the metadata variant of that chain in the autonomous cloud penetration testing write up. The agent chains the two worlds when the cluster runs in a cloud account.

Secrets, image-layer credentials and private registry access

Secrets in Kubernetes are only base64 encoded, not encrypted, once you can read them. The agent harvests them from every source a workload exposes: kubectl get secrets where RBAC allows, ConfigMaps that were misused to hold credentials, environment variables via cat /proc/self/environ, and mounted volumes. It looks specifically for AWS keys, TLS private keys, JWT signing secrets, kubeconfig files and registry credentials, because each one is a pivot to a different system.

kubectl get secrets -A -o json | jq -r '.items[] | .metadata.name'
kubectl describe pod <pod> -n <namespace>
cat /proc/self/environ | tr '\0' '\n'

# a recovered registry credential
docker login <registry> -u <user> -p <redacted, in the report>

Registry credentials open a further door. With a pull secret the agent can reach the private registry, pull images, and inspect them layer by layer. Secrets that were added and then deleted in a later instruction still sit in an earlier layer, so a credential the developer thought they removed is often still there in the history. This is the same class of image and registry abuse we walk through in the Docker socket, container escape, Vault and registry write up, applied to the cluster registry rather than a standalone daemon.

Container escape to the host and CIS benchmark gaps

The escape from container to node is where a workload compromise becomes a node compromise. The agent tests the well understood primitives it found flagged during inventory: a privileged container, a container with excessive Linux capabilities, a hostPath mount that exposes the node filesystem, access to /proc on the host, and any pod that mounts /var/run/docker.sock for a Docker-in-Docker workflow. The socket case is the cleanest: whoever can talk to it can start a privileged container that mounts the host root and copies files off it.

ls -la /var/run/docker.sock
docker run -v /:/host --privileged --rm -it alpine chroot /host sh   # from a pod with the socket

Alongside the exploitation, the agent runs kube-bench for the CIS Kubernetes Benchmark and the Docker CIS checks. The value there is not the raw checklist, it is the correlation: an insecure API server flag, an unprotected etcd, a missing admission controller or a disabled admission policy is only interesting when it lines up with a path the agent is already walking. A CIS failure that no reachable identity can exploit is recorded at its real, low severity, not inflated because a scanner flagged it.

Proof, not a checklist: what gets marked EXPLOITED

Every Darkmoon agent carries the same status discipline, and it is adversarial by design. The agent challenges its own claims before it writes them down and demotes anything that is only an observation. A reachable route, a differential response, a token file that exists but was never used, a CIS flag with no path behind it: these stay UNCONFIRMED at low severity. A finding is only promoted when the impact was actually demonstrated.

Attack pathWhat earns EXPLOITEDWhat stays lower
RBAC to cluster-adminCreated a binding or pod and used the resulting identitycan-i shows a verb, but it was never exercised
ServiceAccount tokenToken used to read secrets or act beyond its scopeToken file readable, no privileged action taken
Kubelet APIexec returned command output from a podPort 10250 open, no anonymous exec confirmed
Container escapeRead or wrote a host file from inside the podPrivileged flag present, escape not executed

This is why the output reads as an attack chain with a MITRE ATT&CK mapping and an exact escalation path, rather than a severity histogram. The runtime security tools a cluster deploys, Falco, Kyverno, Kubescape, Cilium Tetragon, are themselves part of the assessment: the agent notes where a policy is weak or an enforcement gap lets a step through quietly, because a control that does not fire is not a control.

What we do not claim

These are the techniques the kubernetes agent is built to run and the statuses it assigns, drawn from the agent definition in the public repository. The specific results always depend on the cluster under test. We are not publishing a fixed finding count here, because there is no single benchmark cluster behind this article the way the Terraform write up had a real campaign report behind it. An issue is EXPLOITED in a given engagement only when that engagement actually executed the impact, and never by inference from a reachable port or a readable file.

Keeping the model away from your cluster internals

A Kubernetes engagement is full of exactly the data you do not want leaving your environment: node IPs, internal service DNS, ServiceAccount tokens, registry credentials, secret values. Darkmoon runs on a local model and puts a Privacy Gateway in front of it. The model only ever sees deterministic placeholders, never your real IPs, hosts or credentials: an internal address becomes something like HOST_INTERNAL_001, and the real value is rehydrated locally only at the moment a tool runs, then masked back out of the result. It is honest to note the limits of that design, which we set out in how we pentest without sending data to the LLM: deterministic placeholders still leak structure, cardinality and co-occurrence by design, and the command gateway is a policy layer, not a sandbox. It is not a claim that zero information reaches the model. It is a claim that the sensitive values themselves do not.

Remediation

  • Set automountServiceAccountToken: false on every ServiceAccount and pod that does not call the API server, so a workload compromise does not hand over a usable token.
  • Review RBAC for the verbs that escalate: create pods, create or patch on bindings, and impersonate. Run rbac-police and kubectl-who-can in CI, not once a year.
  • Lock down the kubelet: disable anonymous authentication, set the authorization mode to Webhook, and keep port 10250 off any network an untrusted workload can reach.
  • Never mount /var/run/docker.sock into a workload, and forbid privileged containers, added capabilities and host path mounts with an admission policy such as Kyverno or Pod Security Standards.
  • Encrypt Secrets at rest, move them to an external secret store where you can, and scan image layers so a credential deleted in a later instruction is not still sitting in an earlier one.
  • Run kube-bench against the CIS Kubernetes Benchmark and fix the API server, etcd and admission controller findings, then re test whether any identity can still reach the node.

FAQ

What does an AI Kubernetes pentest actually test? The full chain a real attacker walks: cluster enumeration, RBAC analysis, ServiceAccount token theft, secrets and image layer credentials, the kubelet and API server, private registry access, container escape to the node, and namespace pivoting toward kube-system. Each step is driven by what the previous step returned, not run from a fixed list.

How does the agent find over-permissive RBAC? It dumps the Roles, ClusterRoles and bindings, then reasons over them with rbac-police and kubectl-who-can to answer which identity can perform which verb, and specifically whether any identity can grant itself more through create pods, binding creation or impersonate.

Can it demonstrate a container escape safely? Yes. On an authorized, non destructive run the agent proves the primitive, for example reading a host file through a hostPath mount or the Docker socket, and records that as the evidence. It does not run the destructive follow on unless the engagement scope explicitly allows it, the same way our Terraform run minted and then deleted an access key to prove persistence.

Does it need cluster-admin to run? No. It starts from whatever access it is given, an exposed service, a low privileged token, a foothold in a single pod, and its whole purpose is to show how far that access reaches. A high privilege kubeconfig makes the inventory faster, but the point is escalation from a small starting position.

How is this different from kube-bench or a config scanner? Those tell you a setting is non compliant. This tells you whether a reachable identity can actually exploit it, with the exact request, the raw response and the extracted data, mapped to MITRE ATT&CK. The agent runs kube-bench as one input, then uses the reachable paths to decide which of its findings matter.

What this proves about autonomous pentesting

The path from a mounted token to root on a node is short, mechanical and made entirely of misconfigurations that each look acceptable on their own. That is exactly the shape of problem an autonomous agent is good at, because the risk lives in the joins between findings, not in any single one, and the joins change every time a workload is redeployed. Testing the chain continuously, with an agent that only marks impact it actually executed, tells you something a periodic checklist cannot: whether the path is open today.

Darkmoon is GPL-3.0 and self hosted: source, docs.

Run it against your own lab

Darkmoon is open source (GPL-3.0) and self hosted. Clone it, point it at a target you own, and read every line.