Docker Security Fundamentals and Hardening: From Capabilities to Rootless Containers

Containers & DevSecOps
Time it takes to read this article 6 minutes.

Disclaimer: This article is for educational purposes and authorized security testing only. Run the offensive techniques shown here only against systems and containers you own or have explicit written permission to test. Misusing them against third-party infrastructure is illegal.

Introduction / Overview

Containers feel like virtual machines, but they are not. A Docker container is just a set of processes on the host kernel, isolated by namespaces and constrained by cgroups, capabilities, and seccomp. When those guardrails are weakened — most often by --privileged, dropped seccomp profiles, or running everything as UID 0 — a single application bug can become full host compromise.

This article walks through the kernel primitives that actually enforce container isolation, demonstrates a realistic container-escape proof of concept in a lab, and then spends equal time on the blue-team side: how to harden images and runtimes so the escape never works. If you are new to Linux internals, my notes on Linux privilege escalation basics provide useful background.

How it works / Background

Three kernel mechanisms do most of the heavy lifting:

  • Linux capabilitiesCAP_SYS_ADMIN, CAP_NET_RAW, CAP_SYS_PTRACE, etc. Root inside a container does not get the full capability set by default. Docker's default bounding set drops dangerous capabilities like CAP_SYS_ADMIN, CAP_SYS_MODULE, and CAP_SYS_TIME.
  • seccomp — a syscall filter (BPF) that blocks roughly 40+ dangerous syscalls (keyctl, ptrace in some configs, mount, unshare, bpf, etc.). Docker ships a default profile at /usr/share/containers/seccomp.json-style allowlists. Disabling it with --security-opt seccomp=unconfined reopens that attack surface.
  • Namespaces — PID, mount, network, user, IPC, and UTS namespaces give the container its own view of the system. The user namespace is what makes rootless Docker possible: in-container UID 0 maps to an unprivileged host UID.

The danger flags are --privileged (grants all capabilities, disables seccomp/AppArmor, and exposes host devices under /dev), and bind-mounting the host filesystem or /var/run/docker.sock. These map to MITRE ATT&CK T1611 — Escape to Host.

Prerequisites / Lab setup

Use a throwaway VM. Everything below assumes Docker Engine 24+ on Linux.

# Confirm version and check the active security profile of a test container
docker version --format '{{.Server.Version}}'
docker run --rm alpine grep CapEff /proc/self/status
docker run --rm alpine grep Seccomp /proc/self/status   # Seccomp: 2 = filter active
Bash

Inspect the default capability set with capsh to see what root inside a normal container can and cannot do:

docker run --rm --cap-drop=ALL ubuntu \
  bash -c 'capsh --print | head -3'
Bash

Walkthrough / PoC

1. Demonstrate why --privileged is dangerous

A privileged container can mount the host disk and read or write any file. In a lab VM, identify the host root device and mount it from inside the container:

# Inside a privileged container — DO NOT do this in production
docker run --rm -it --privileged ubuntu bash

# (in container) host root filesystem is typically the largest non-loop disk
lsblk
mkdir /host && mount /dev/sda1 /host
ls /host/etc/shadow        # full read access to the host
Bash

The mount syscall succeeds because --privileged grants CAP_SYS_ADMIN and removes the seccomp filter that would normally block it. This is the canonical escape primitive.

2. The Docker socket mount

Mounting the daemon socket is functionally equivalent to giving root on the host, because you can ask the daemon to start a new privileged container for you:

docker run --rm -it -v /var/run/docker.sock:/var/run/docker.sock docker:cli sh
# inside: spawn a container that mounts the host root and chroots into it
docker run --rm -it -v /:/host alpine chroot /host sh
Bash

3. Verifying a hardened container blocks the same actions

Now run the correct way and watch the same primitives fail:

docker run --rm -it \
  --cap-drop=ALL \
  --security-opt no-new-privileges \
  --read-only \
  ubuntu bash

# (in container) the mount syscall is denied
mkdir /host && mount /dev/sda1 /host
# mount: permission denied  ->  CAP_SYS_ADMIN missing + seccomp active
Bash

4. Run rootless to remove the last bit of host trust

Rootless mode runs the entire daemon as an unprivileged user via user namespaces, so even a full breakout lands as a non-root host UID:

# Install and start the rootless daemon (Docker provides a helper script)
dockerd-rootless-setup-tool.sh install
export DOCKER_HOST=unix://$XDG_RUNTIME_DIR/docker.sock
docker run --rm alpine id        # uid 0 in-container...
# ...but on the host this maps to your subuid range, e.g. 100000+
Bash

5. Scan the image before you ship it

Vulnerable base images are the most common real-world entry point. Use Trivy or Grype:

trivy image --severity HIGH,CRITICAL --ignore-unfixed myapp:latest
grype myapp:latest -o table
Bash

For context, runc's CVE-2019-5736 allowed overwriting the host runc binary from inside a container, and CVE-2024-21626 (the "Leaky Vessels" WORKDIR file-descriptor leak) enabled escape during build/run — both are caught by keeping runc and Docker patched, which scanners flag.

Mermaid diagram

Docker Security Fundamentals and Hardening: From Capabilities to Rootless Containers diagram 1

The diagram shows the decision chain every container syscall passes through: --privileged bypasses all of it, while capabilities, seccomp, and rootless mapping each independently shrink the blast radius.

Detection & Defense (Blue Team)

Hardening is layered. Apply as many of these as your workload allows.

Drop privileges by default. Never use --privileged unless absolutely required (e.g., some CI/build agents). Drop all capabilities and add back only what you need:

docker run \
  --cap-drop=ALL --cap-add=NET_BIND_SERVICE \
  --security-opt no-new-privileges \
  --security-opt seccomp=default.json \
  --read-only --tmpfs /tmp \
  --user 1000:1000 \
  myapp:latest
Bash

Run as non-root in the image. Add a USER directive so even the default process is unprivileged:

RUN addgroup -S app && adduser -S -G app app
USER app
Dockerfile

Enforce policy at the orchestrator. In Kubernetes, use a securityContext and Pod Security Admission restricted profile:

securityContext:
  runAsNonRoot: true
  allowPrivilegeEscalation: false
  readOnlyRootFilesystem: true
  capabilities:
    drop: ["ALL"]
  seccompProfile:
    type: RuntimeDefault
YAML

Detection. Watch for the tell-tale escape signals:

  • Audit the daemon: alert on docker run events carrying Privileged: true, /var/run/docker.sock bind mounts, or seccomp=unconfined. Falco rules Launch Privileged Container and Docker Socket Mounted cover these directly.
  • Use Falco or auditd to catch suspicious in-container syscalls — mount, unshare, setns, and writes to /proc/sys/kernel/core_pattern (a known escape via cgroups release_agent).
  • Scan continuously in CI with Trivy/Grype and gate on CRITICAL findings; pin base images by digest (FROM alpine@sha256:...) so the scan result is reproducible.

Reduce trust. Prefer rootless Docker or Podman, enable user-namespace remapping (/etc/docker/daemon.json with "userns-remap": "default"), keep the default AppArmor/seccomp profiles enabled, and patch runc promptly. For deeper container-escape tradecraft, HackTricks' container section is the standard reference; see also my walkthrough on Kubernetes RBAC misconfigurations.

Conclusion

Docker isolation is real but conditional. The default profile is reasonably safe; the danger comes from operators who reach for --privileged, mount the socket, or disable seccomp to "make it work." Drop capabilities, keep seccomp on, run rootless and non-root, scan every image, and monitor the daemon for privileged launches. Each layer is cheap; together they turn a container escape from a single misstep into a chain an attacker is unlikely to complete.

References

You Might Also Like

If you found this useful, these related deep-dives cover adjacent techniques and their defenses:

Comments

Copied title and URL