Rootless mode, capabilities and seccomp

Docker · Expert · 15 min

Defence in depth turns a container compromise into a contained incident instead of a host takeover.

Root in a container is the host's UID 0 (unless user namespaces are used), restricted by several layers. Knowing them lets you tighten each.

Capabilities split root's power into units. Docker grants a reduced default set (including CHOWN, DAC_OVERRIDE, SETUID, SETGID, NET_BIND_SERVICE, NET_RAW, KILL) and withholds dangerous ones like SYS_ADMIN, SYS_MODULE and NET_ADMIN. Best practice: --cap-drop ALL, then --cap-add only what's needed.

seccomp filters system calls. Docker's default profile blocks dozens of risky syscalls (kernel module loading, reboot, kexec_load, mount without the right capability...) while allowing what normal apps use. Custom profiles go in with --security-opt seccomp=profile.json. Never use seccomp=unconfined casually.

`--security-opt no-new-privileges` stops setuid binaries and file capabilities from granting more privilege than the process started with.

LSMs: AppArmor (docker-default profile) or SELinux labels add mandatory access control on files and mounts.

`--privileged` turns nearly all of this off: all capabilities, all devices, no seccomp. It is effectively host root, so avoid it outside of special cases such as Docker-in-Docker or hardware access.

User namespaces remap container root to an unprivileged host UID. userns-remap in daemon.json does this for all containers. Rootless mode goes further: the daemon itself runs as a normal user (dockerd-rootless-setuptool.sh install), using subordinate UID ranges from /etc/subuid and RootlessKit for networking. A full escape then yields only that user's rights. Trade-offs: binding ports below 1024 needs a sysctl change, and some networking and storage features are limited.

Combine these with a non-root USER and a read-only root filesystem for strong defaults.

Example

docker run -d --name api \
  --user 10001:10001 \
  --read-only --tmpfs /tmp \
  --cap-drop ALL --cap-add NET_BIND_SERVICE \
  --security-opt no-new-privileges \
  -p 80:80 acme/api:1.4.2

docker inspect api --format '{{.HostConfig.CapDrop}} {{.HostConfig.SecurityOpt}}'
docker run --rm alpine grep Cap /proc/self/status     # default capability bitmasks

# Rootless Docker for the current user
dockerd-rootless-setuptool.sh install
export DOCKER_HOST=unix://$XDG_RUNTIME_DIR/docker.sock
docker info --format '{{.SecurityOptions}}'   # [name=seccomp,... name=rootless ...]

Remember

  • Drop all capabilities and add back only what's needed
  • The default seccomp profile blocks risky syscalls; don't run unconfined
  • --privileged disables almost all isolation
  • userns-remap or rootless mode map container root to an unprivileged host user

Watch out for

  • Reaching for --privileged to fix a permission error instead of adding the single capability or device actually required.

Check yourself

  1. Which flag most effectively removes root powers a web app doesn't need?
  2. In rootless mode, what does root inside a container correspond to on the host?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: docs.docker.com/engine/security/rootless/

Builds on

Unlocks

Nothing builds on this yet.