Chapter 47

Container Forensics

Container environments (Docker, Kubernetes) present unique forensic challenges: containers are ephemeral (evidence disappears when they're restarted), overlay filesystems layer changes on read-only base images, and traditional host-level forensic tools may not see into container namespaces. This chapter covers evidence collection from running and stopped containers before they're lost.

Scenario

A Kubernetes pod running a web application was compromised. The attacker exploited a RCE vulnerability, gained shell access, and installed a cryptominer. The pod hasn't been restarted yet — all evidence is still live. You have 30 minutes before the DevOps team restarts the pod as part of their standard remediation. This chapter covers what to collect before that restart destroys the container.

Why containers demand forensic urgency — the 30-minute window

In traditional IR, a compromised server can sit isolated for 12–24 hours while you prepare your forensic workstation. Containers give you no such luxury. Kubernetes deployments are typically configured with health checks, auto-restart policies, and automated remediation. A pod that fails a liveness probe restarts within 30–90 seconds. Even without automation, the DevOps team's standard remediation is "restart the pod" — and they will do this within minutes of being notified of a compromise. When a container restarts, the entire read-write overlay layer is destroyed: your attacker's files, shell history, and live network connections are gone. Every minute you spend in a meeting rather than collecting evidence is evidence you'll never recover. The first action on a compromised container is not isolation — it is collection.

Container Evidence Sources

  Docker/Kubernetes Forensic Evidence Sources
  ═══════════════════════════════════════════════════════════════════

  1. Container runtime logs (docker logs / kubectl logs)
     ├── STDOUT/STDERR of the container process
     └── Available even after restart (for a retention window)

  2. Container filesystem (overlay layers)
     ├── Read-only base layers (from the image)
     ├── Read-write upper layer (container's changes since start)
     └── Attacker's files are ONLY in the upper layer — easy to identify

  3. Container inspect metadata
     ├── When container started
     ├── Mounts, network settings, environment variables
     └── Image used + any volume mounts

  4. Host-level evidence (from the underlying Linux host)
     ├── /proc/[container_pid]/: process info from host perspective
     ├── Container filesystem at /var/lib/docker/overlay2/[hash]/
     └── Host network connections: netstat sees container traffic

  5. Kubernetes-specific
     ├── Pod events: kubectl describe pod
     ├── API server audit logs: every kubectl call is logged
     └── Kubelet logs: container lifecycle events

Docker vs Kubernetes Evidence Comparison

Evidence typeDocker (standalone)KubernetesSurvives restart?
Container STDOUT/STDERRdocker logskubectl logs --previousYes (retention window)
Filesystem changes (attacker files)UpperDir overlay layerUpperDir on node where pod ranNo — lost on restart
Live process listdocker top or nsenter -nkubectl exec -- psNo — lost on restart
Active network connectionsnsenter -t [pid] -n -- sskubectl exec -- ssNo — lost on restart
Container metadata/configdocker inspectkubectl describe pod / pod YAMLYes
Orchestration audit logDocker daemon log (journald)K8s API server audit logYes (per retention policy)
Persistent storageVolume mounts on hostPersistentVolumeClaimsYes
Network trafficHost netflow/PCAPNode netflow/PCAP or service mesh logsYes (per retention policy)
Bashdocker-forensics.sh
CONTAINER_ID="b3f12a9c8e1d"
CASE_DIR="/cases/CASE-2026-009/containers"
mkdir -p $CASE_DIR

# Step 1: Save container metadata before it changes
echo "Collecting container metadata..."
docker inspect $CONTAINER_ID > $CASE_DIR/inspect.json
docker logs $CONTAINER_ID > $CASE_DIR/container-stdout.log 2>&1

# Step 2: Export the container filesystem (read-write layer = attacker changes)
echo "Exporting container filesystem..."
docker export $CONTAINER_ID > $CASE_DIR/container-filesystem.tar
# Extract and look for attacker files:
mkdir $CASE_DIR/filesystem
tar xf $CASE_DIR/container-filesystem.tar -C $CASE_DIR/filesystem

# Step 3: Find files added to the container (not in the base image)
# Mount the container's diff (overlay upper layer) on the host:
CONTAINER_GRAPH=$(docker inspect $CONTAINER_ID | jq -r '.[0].GraphDriver.Data.UpperDir')
echo "Attacker-modified files (upper overlay layer):"
find $CONTAINER_GRAPH -type f | sort

# Step 4: Live process snapshot (before restart)
CONTAINER_PID=$(docker inspect $CONTAINER_ID | jq -r '.[0].State.Pid')
echo "Container PID: $CONTAINER_PID"

# Get process tree inside container from host perspective
ps auxf | grep -A100 "container" | head -30

# Network connections from the container
nsenter -t $CONTAINER_PID -n -- ss -tulpn > $CASE_DIR/container-netstat.txt
nsenter -t $CONTAINER_PID -n -- ss -tp > $CASE_DIR/container-connections.txt

# Step 5: Memory dump of container PID
cd $CASE_DIR && avml --pid $CONTAINER_PID memory-container.lime 2>/dev/null || \
    gcore -o $CASE_DIR/core $CONTAINER_PID  # fallback: gcore dump
Bashkubernetes-forensics.sh
NAMESPACE="production"
POD="webapp-abc123"
CASE_DIR="/cases/CASE-2026-009/kubernetes"
mkdir -p $CASE_DIR

# Kubernetes forensic collection before pod restart

# Step 1: Capture pod metadata and events
kubectl describe pod $POD -n $NAMESPACE > $CASE_DIR/pod-describe.txt
kubectl get pod $POD -n $NAMESPACE -o yaml > $CASE_DIR/pod-manifest.yaml
kubectl get events -n $NAMESPACE --field-selector involvedObject.name=$POD \
    > $CASE_DIR/pod-events.txt

# Step 2: Capture pod logs
kubectl logs $POD -n $NAMESPACE --all-containers > $CASE_DIR/pod-logs.txt
kubectl logs $POD -n $NAMESPACE --previous > $CASE_DIR/pod-logs-previous.txt 2>/dev/null

# Step 3: Run forensic commands inside the pod (before restart)
# Only if container is still running
kubectl exec $POD -n $NAMESPACE -- ps auxf > $CASE_DIR/processes.txt
kubectl exec $POD -n $NAMESPACE -- ss -tulpn > $CASE_DIR/network.txt
kubectl exec $POD -n $NAMESPACE -- find / -type f -newer /proc -mmin -1440 \
    2>/dev/null > $CASE_DIR/recently-modified.txt  # files modified in last 24h
kubectl exec $POD -n $NAMESPACE -- cat /etc/crontab > $CASE_DIR/crontab.txt 2>/dev/null
kubectl exec $POD -n $NAMESPACE -- history > $CASE_DIR/shell-history.txt 2>/dev/null

# Step 4: Copy suspicious files from container
kubectl cp $NAMESPACE/$POD:/tmp/ $CASE_DIR/container-tmp/
kubectl cp $NAMESPACE/$POD:/var/www/html/ $CASE_DIR/webroot/ 2>/dev/null

# Step 5: Check Kubernetes API audit logs
# API server audit log location varies by cluster deployment
# For kubeadm clusters:
kubectl logs -n kube-system kube-apiserver-$(hostname) --tail=1000 | \
    grep -i "$POD\|$NAMESPACE" > $CASE_DIR/apiserver-logs.txt
Common mistake: running forensic tools inside the compromised container

Analysts new to container forensics often kubectl exec into a compromised container and run commands like find, ps, and netstat directly inside it. The problem: those tools come from the container image. If the attacker replaced them with modified versions (a common persistence technique), your forensic output is attacker-controlled. More critically, running commands inside the container modifies the container's filesystem timestamps — you've compromised the very evidence you're collecting. The correct approach is to collect from the outside: use nsenter -t [pid] on the host to enter only the container's network or PID namespace while running host binaries, and use the overlay UpperDir on the host filesystem to examine what the attacker added without touching the container. Reserve kubectl exec for commands that cannot be run from the host and document each one explicitly in your case notes.

Q & A

Q: The compromised pod was already restarted before you could collect evidence. What's left?

Significant evidence remains after a pod restart: (1) kubectl logs --previous: Kubernetes retains the previous container's log output. The --previous flag retrieves the logs from the last crashed or terminated container instance. This captures the attacker's commands if they appeared in STDOUT/STDERR. (2) Kubernetes API server audit log: every kubectl command the attacker ran (exec, cp, port-forward) is logged in the API server audit log with the user, command, and timestamp. This survives pod restarts completely. (3) Container image layers: the base image layers are unchanged. If the attacker modified files that were part of the original image, those modifications are gone. But the image itself is available for analysis. (4) Host-level logs: the underlying Kubernetes node's auditd and systemd journal log container lifecycle events. The docker daemon log (journalctl -u docker) records container start/stop and exec events. (5) Network telemetry: NetFlow/PCAP from the node captures all traffic the container sent/received, regardless of pod lifecycle. If you have 72 hours of packet capture on the node, the exfiltration traffic is still there. (6) Persistent volumes: if the pod had a PersistentVolumeClaim, the attacker may have written to the persistent storage, which survived the restart. Check the PVC for modified files.