Container Runtime Hardening: Ten Controls Against Host Compromise
Default container installations favor developer convenience over host isolation. When you run a container without explicit configuration flags, Docker grants broad Linux capabilities, maps root execution to UID 0, and mounts an unrestricted writable filesystem. If an attacker exploits a remote code execution bug in your application, these permissive defaults turn an application-layer incident into root access across the underlying Linux node.
Containers are not virtual machines. They are isolated processes sharing the host Linux kernel. Any permissive runtime flag weakens that process boundary and exposes the host.
Hardening container execution does not require rebuilding your infrastructure from scratch. Applying defensive flags at the daemon and runtime levels restricts process privileges, prevents persistent root filesystem modifications, and controls network transit between local services.
Checklist
- Non-Root User Execution – Never allow application entrypoints to execute as root. Assign an explicit unprivileged UID and GID inside your Dockerfile or pass
--user 10001:10001during container initialization. If an attacker breaks execution flow through a memory vulnerability, remaining an unprivileged user stops them from modifying system binaries or writing to root-owned paths on mounted volumes. - Read-Only Root Filesystem – Pass the
--read-onlyruntime argument to mount the root container image as immutable. Attackers rely on writing download scripts, compiling exploit tools, and dropping persistence files directly into directories like/tmpor/etc. Forcing an immutable root filesystem blocks payload installation immediately at the kernel filesystem layer. - Drop All Linux Capabilities – Modern Linux grants processes specific kernel privileges through capabilities. Containers start with dangerous defaults such as
CAP_NET_RAWandCAP_SYS_CHROOT. Pass--cap-drop=ALLon startup, then explicitly whitelist only the bare minimum flags needed for production traffic, such asNET_BIND_SERVICEif binding privileged ports. - Block Privilege Escalation Flags – Configure
--security-opt=no-new-privileges:truefor every production workload. This flag tells the Linux kernel that the container process and its children can never gain new privileges through setuid or setgid binaries. Even if an attacker finds an existing setuid binary inside a vulnerable image base, the kernel denies execution elevation. - Disable Inter-Container Communication – By default, Docker connects containers on the default bridge and allows unrestricted network communication between them. Add
"icc": falseto daemon options. This forces all inter-container traffic through explicit user-defined networks, stopping compromised web services from scanning administrative databases on the same host bridge. - Prohibit Docker Socket Mounts – Avoid mounting the Docker UNIX socket into application containers under all circumstances. Access to the UNIX Docker socket gives a process full administrative control over the Docker API, enabling it to launch privileged containers that mount host root disks. Isolate automation tools or use scoped remote TCP endpoints with mutual TLS instead.
- Strict Memory and CPU Quotas – Set hard cgroup limits with
--memory,--memory-swap, and--cpuson each service. Without these boundaries, a single memory leak, runaway recursive query, or denial-of-service attack can starve the host kernel of available RAM, triggering the Out-of-Memory killer to terminate critical system services. - Process Table Limits – Prevent containerized fork bombs by passing
--pids-limit. Malicious or broken processes can spawn thousands of threads rapidly, exhausting the kernel PID space and locking administrators out of the host terminal. Capping worker threads to a known upper boundary keeps host operating system process management stable under heavy loads. - Volatile Storage for Temporary Files – When an application demands writable temporary storage, attach explicit RAM-backed mounts with
--tmpfs /tmp:rw,noexec,nosuid,size=64m. Thenoexecmount option prevents binary execution inside temporary directories, whilenosuidstops privilege transitions. Volatile memory allocation also ensures secrets flushed to temporary files vanish upon process termination. - Automated Log Rotation Limits – Configure daemon-level logging limits to prevent unmonitored output from filling host storage drives. Set
log-drivertojson-filewithmax-size: "10m"andmax-file: "3"in your daemon configuration. Unbounded stdout logging frequently creates disk exhaustion failures that crash the container engine and corrupt production datastores.
Here is an example command combining these runtime restrictions into a single hardened deployment command:
docker run -d \
--name payment-worker \
--user 10001:10001 \
--read-only \
--cap-drop=ALL \
--cap-add=NET_BIND_SERVICE \
--security-opt=no-new-privileges:true \
--tmpfs /tmp:rw,noexec,nosuid,size=64m \
--pids-limit 128 \
--memory 512m \
--cpus 1.5 \
--network custom-internal-net \
--restart unless-stopped \
production/worker:v1.2.0
Audit running containers on your nodes using docker inspect to verify that these constraints remain active. Enforcing strict process boundaries at launch time contains compromise to the individual container instance and prevents lateral movement into the host operating system.