Skip to content

This tool is not affiliated with, endorsed by or sponsored by The Linux Foundation, the Cloud Native Computing Foundation (CNCF) or the Kubernetes project. Kubernetes and K8s are registered trademarks of The Linux Foundation. EKS, GKE, AKS and other names are trademarks of their respective owners.

Kubernetes Attack Walkthrough: A Fictional Incident

A fictional Kubernetes incident, step by step in the audit log: exposed dashboard token, can-i recon, secret theft, privileged DaemonSet, cluster-admin, XMRig.

Published on 7 min read

TL;DR. This is a fictional incident, generated for the "Try a sample" button of the Kubernetes audit log analyzer. Cluster, people, IP addresses, images and registry are all made up. In fifteen minutes of audit events, an attacker uses an exposed Kubernetes Dashboard service account token from the internet, maps its permissions with kubectl auth can-i, lists every secret, deploys a privileged DaemonSet mounting each node's filesystem, opens a root shell with chroot /host, creates a service account bound to cluster-admin, schedules an XMRig CronJob and deletes Events. The verdict is Compromised; this article reads the evidence line by line.

Fictional scenario. Everything below is synthetic. The internet addresses come from the ranges reserved for documentation (RFC 5737), the domains end in .example, and no real organisation, person or incident is described.

The sample covers six hours (08:00 to 14:00 UTC on 14 September 2026) of a small production cluster called shop-prod: three worker nodes renewing leases, controllers reconciling, Prometheus scraping, a Helm-based CI deploying twice an hour, a platform admin called Alice using kubectl, and the Kubernetes Dashboard polling pods from its in-cluster address. About 2,080 events in total, of which about forty-five belong to the attack. That ratio is realistic: the hard part is finding those few dozen lines.

The verdict

Loading the sample (the audit log plus two Falco alerts) gives Likely compromised, driven by three critical findings (container escape to the node, binding to cluster-admin, crypto-mining image) and several high ones. The remediation checklist starts with preserving evidence and scoping, then deleting malicious workloads, rebuilding nodes, enforcing Pod Security, removing bindings and revoking tokens.

Now the timeline, as the analyzer's incident timeline shows it (times UTC).

13:01:58 – A familiar identity from an unfamiliar place

The first event of the attack is a get /version by system:serviceaccount:kubernetes-dashboard:kubernetes-dashboard. That service account has been active all morning, but from 10.0.3.15 with the user agent dashboard/v2.7.0. This request comes from 203.0.113.45, a public address, with kubectl/v1.30.2 (linux/amd64).

Two findings fire on it: Service account token used from a public IP (high) and Service account token used with kubectl / curl (medium). The story they tell is simple: the dashboard's token left the cluster and someone is using it by hand. In this scenario, the dashboard runs with an over-privileged role, which is what makes the rest possible.

Discovery calls to /api and /apis follow within two seconds, as kubectl always does on first contact.

13:02:31 – "What can I do?"

Seven requests in fifteen seconds: six selfsubjectaccessreviews and one selfsubjectrulesreviews. With RequestResponse logging, the bodies show exactly what was asked: can I list secrets, create pods in kube-system, create daemonsets in kube-system, create clusterrolebindings, create pods/exec in kube-system, and * *? Everything is allowed except the wildcard.

That is Permission enumeration (kubectl auth can-i), medium. Two 403 responses at 13:03 on list nodes and list clusterroles confirm the role is broad but not unlimited. The attacker now knows the plan will work. See secrets and token theft for this recon pattern.

13:04:02 – Every secret in the cluster

A single list on secrets with no namespace: Secrets listed across all namespaces (high). That one response contains every secret of the cluster. The attacker then reads twelve secrets one by one between 13:04:40 and 13:05:13: database credentials, a payment API key, the registry credentials, a JWT signing key, a Vault token, a webhook secret, Grafana and Alertmanager credentials, the CI deployer token, a GitHub App private key, cloud credentials and an etcd backup bucket key. Burst of secret reads (medium) fires on the tenth distinct name.

For the responder, this list is the rotation plan. Given the cluster-wide list, every other secret must be treated as exposed too.

13:06:12 – A privileged DaemonSet in kube-system

The dashboard token creates a DaemonSet named kube-proxy-monitor in kube-system: image alpine:3.20, hostPID and hostNetwork, a privileged container, the node's / mounted at /host, a toleration for every taint so it lands on every node. Five findings fire on this single event:

  • Container escape to the node (critical): privileged plus hostPath / plus hostPID;
  • Privileged container deployed, Sensitive host path mounted, DaemonSet created in kube-system (high);
  • Workload shares the node's PID / network / IPC namespace (medium).

One second later the DaemonSet controller creates three pods, one per node. Those creations are by a system identity; the accountable identity is the one that wrote the DaemonSet. The privileged pods article explains why the analyzer reads the pod template inside the workload.

13:07:40 – A root shell on the node

An exec into kube-proxy-monitor-x7k2p with command=chroot&command=%2Fhost&command=sh: chroot /host sh, a shell on worker-1's own filesystem. Service account ran a command in a container (high). The event appears in three stages sharing one auditID (RequestReceived, ResponseStarted with code 101, ResponseComplete at 13:08:55); the analyzer folds them into one session of about 75 seconds.

The audit log stops here: what was typed in that shell is invisible. The sample's Falco file contains a "Terminal shell in container" alert at 13:07:41 on the same pod, with chroot as parent process. It is a Notice-level alert, below the analyzer's alert threshold, but it sits on the same timeline and confirms the shell. The limitations article covers why this pairing matters.

13:09 – A new identity with cluster-admin

Three writes in 26 seconds:

  1. create serviceaccounts kube-system/node-health: a plausible-sounding name.
  2. create clusterrolebindings node-health-admin, roleRef cluster-admin, subject the new service account: Binding to cluster-admin created (critical) and Cluster-wide role binding created (low).
  3. create serviceaccounts/token for node-health, with expirationSeconds: 31536000: Service account token minted (medium). A one-year token for an identity nobody will think to revoke.

This is the persistence step described in the RBAC escalation article. From 13:11:02 onwards, requests come from node-health, still from 203.0.113.45 with the same kubectl build: the IP and user agent link the two identities to one operator.

13:12:30 – The miner

node-health creates a CronJob kube-state-sync in kube-system, every five minutes, image registry.k8s-mirror.example:5000/xmrig/xmrig:6.21.0, arguments --url=pool.minexmr.example:4444 --donate-level=0. Findings: Crypto-mining image or command (critical), CronJob created (medium), Image from an unusual registry (low). At 13:15 and 13:20 the Job controller creates the jobs and pods; the miner rule fires on those too, because a miner is a miner whoever started it.

The second Falco alert, Critical, at 13:15:07 on worker-2, shows xmrig connecting to port 4444: Falco runtime alert (high). The crypto-mining article covers this pattern.

13:16:40 – Covering tracks

deletecollection on events in kube-system: Kubernetes events deleted (low). Events would have shown the DaemonSet pods being scheduled and the miner image being pulled. The audit log, stored outside the cluster, still has everything. (On EKS, whose managed policy does not log Events at all, this step would not be visible.)

The false positive in the report

One finding has nothing to do with the attack: Interactive command in a container (medium) for alice@example.com at 10:12:03, an sh into an API pod in shop. It is a legitimate debugging session from her usual IP with her usual kubectl. That is how exec findings should be handled: confirm with the owner, note it, move on. See detecting kubectl exec.

What the responder does next

Following the analyzer's checklist and the incident response guide:

  1. Preserve: export the audit logs, save the DaemonSet, CronJob, ServiceAccount and ClusterRoleBinding specs, snapshot worker-1 (shell) and the other nodes (privileged pods ran on all three).
  2. Contain: delete node-health-admin, delete and recreate both service accounts (the minted one-year token dies with node-health), restrict API server access, take the dashboard offline.
  3. Eradicate: delete the CronJob, its Jobs and pods, and the DaemonSet; search every namespace for the image and the pool.
  4. Recover: replace the three nodes; rotate every secret, starting with the cloud credentials, the GitHub App key and the CI token, which reach outside the cluster.
  5. Scope beyond the cluster: the stolen cloud credentials and CI token must be traced in the cloud and CI audit logs.
  6. Harden: no dashboard exposed, least-privilege dashboard role, Pod Security Admission on kube-system, trusted registries only.

Try it yourself

Open the analyzer, click Try a sample, and follow along: the timeline, the entity pivot on 203.0.113.45 and the detail sheet of each event show everything described here. The step-by-step guide explains each panel.

Related articles

Step-by-step Kubernetes audit log analysis with a free in-browser tool: load EKS, GKE, AKS or raw exports, read the verdict, findings, timeline and entities.
The blind spots of Kubernetes audit logs: in-container activity, kubelet and etcd access, policy gaps, spoofable fields, and the evidence that fills them.
A practitioner's Kubernetes incident response guide: the audit log evidence to collect, the questions to answer, the attack patterns to hunt and how to contain.

This tool is not affiliated with, endorsed by or sponsored by The Linux Foundation, the Cloud Native Computing Foundation (CNCF) or the Kubernetes project. Kubernetes and K8s are registered trademarks of The Linux Foundation. EKS, GKE, AKS and other names are trademarks of their respective owners.