On a typical Docker host, anyone who can reach the Docker daemon, by socket, by TCP or by running a Compose file, effectively has root on that host, and most real Docker incidents start there rather than with an exotic container escape. Docker Compose makes this easy to miss: a single compose.yaml can mount the host filesystem, publish ports to the internet, add capabilities and pass secrets as environment variables, and Compose applies all of it as written. This guide covers the Docker-specific and Compose-specific controls: protecting the daemon and its socket, the docker group, rootless and user namespace modes, safe Compose defaults, Compose and build secrets, port publishing and firewalls, and what to monitor. It is for developers, platform engineers and security teams running Docker Engine on servers, build agents and developer machines. For container security across the full lifecycle, see the Container Security Guide.
Key takeaways
- The daemon is the boundary. Docker's own documentation says the
dockergroup grants root-level privileges and that only trusted users should control the daemon; treat socket access as privileged access. - Never mount
/var/run/docker.sockinto a container you would not give host root. OWASP notes that a read-only mount does not make the API read-only. - A Compose file is code you run. Docker's Compose trust model treats every Compose file as trusted input, including nested
includeandextendsreferences; review the resolved output ofdocker compose configbeforeup. - Use secrets, not environment variables. Compose secrets mount as files under
/run/secrets/, build secrets use--mount=type=secret, and build arguments and environment variables persist in the final image. - Published ports are public by default.
ports: "8080:80"listens on all host addresses and Docker's NAT rules bypass ufw; bind to127.0.0.1unless the service must be reachable.
Why the Docker daemon is the real attack surface
The Docker Engine documentation splits Docker security into four areas: kernel namespaces and cgroups, the attack surface of the daemon, the container configuration profile, and kernel hardening features. The second is where most organisations get hurt. The daemon (dockerd) runs as root unless rootless mode is used, and it will do what any authorised client asks: start a container with the host's / directory mounted, in the host network and PID namespaces, with all capabilities. Docker's documentation is explicit that this lets a container alter the host filesystem without restriction, and that only trusted users should be allowed to control the daemon.
| Access path | What it gives | Control |
|---|---|---|
Membership of the docker group | Root-level privileges on the host, as Docker's post-install guide warns | Treat as privileged access; review membership; prefer rootless mode on shared hosts |
/var/run/docker.sock mounted into a container | Full control of the daemon from inside that container | Do not mount it; if a tool truly needs the API, use an authorisation plugin or a scoped proxy and isolate the tool |
TCP listener (-H tcp://..., ports 2375 and 2376) | Remote root on the host for anyone who can connect | Do not expose; if needed, TLS client certificates on 2376, restricted to a management network, or SSH (DOCKER_HOST=ssh://user@host) |
| CI job or agent with Docker access | Host root for anyone who can change the pipeline | Dedicated, ephemeral build hosts; rootless or daemonless builders; no shared long-lived agents |
| Running an untrusted Compose file | Whatever the file requests: bind mounts, privileged mode, host networking | Review docker compose config output; pin remote references to digests |
Remote access deserves particular care. Docker's remote access page warns that opening the daemon to the network can let remote non-root users gain root on the host, and the default ports are 2376 with TLS and 2375 without. The engine security page states that exposing the daemon API over HTTP without TLS is not permitted and causes the daemon to fail early on startup in current releases, but older engines still in service accept it. Unauthenticated Docker APIs on the internet are routinely found and abused for cryptomining and credential theft, for example by the Carbonato botnet, which planted an agent on exposed Docker hosts to hunt AI API keys.
Rootless mode, user namespaces and daemon options
There are three ways to reduce what a compromise of the daemon or a container means for the host. They are not equivalent.
| Option | How it works | Choose when | Limits |
|---|---|---|---|
| Rootful daemon, non-root containers | Daemon runs as root; images set USER and containers drop capabilities | Default for most servers; easiest compatibility | Daemon compromise or socket access is still host root |
userns-remap | Daemon runs as root, but container root is mapped to an unprivileged range from /etc/subuid and /etc/subgid | Shared hosts where container root must not equal host root | Daemon still root; some volume and host-namespace features need per-container exceptions |
| Rootless mode | Daemon and containers run inside a user namespace as an ordinary user; no setuid binaries except newuidmap and newgidmap | Developer machines, CI runners, multi-user hosts | Needs at least 65,536 subordinate IDs; some networking and storage features differ; shared kernel is still the boundary |
OWASP's Docker Security Cheat Sheet makes the same point: rootless mode reduces the impact of a daemon or runtime compromise but does not protect against kernel vulnerabilities, and userns-remap leaves the daemon running as root. Where you need a stronger boundary between tenants, use separate virtual machines.
- Default container posture: Docker starts containers with a restricted capability allowlist and a default seccomp profile. Do not weaken them globally; add exceptions per service.
- Daemon-wide defaults in
/etc/docker/daemon.json: settings such as"no-new-privileges": true,"userns-remap","live-restore"and log driver limits apply to every container on the host. Change them in a pilot ring first. - Keep the engine current: Docker's port publishing documentation notes that before Engine 28.0.0, hosts on the same layer 2 segment could reach ports published only to localhost.
Secure Compose files
Docker's Compose trust model page lists the fields with security implications when set by an untrusted author. These are the fields to look for in every review, and the ones to block or flag in policy checks.
| Compose field | Risk | Safer default |
|---|---|---|
privileged: true | Full access to the host | Remove; add only the specific capability or device needed |
cap_add | Adds powers such as SYS_ADMIN or NET_RAW | cap_drop: [ALL] and add back only what is required |
security_opt | Can disable seccomp or AppArmor (unconfined) | Use it to add no-new-privileges:true, not to remove profiles |
volumes with host paths | Mounts host directories, including /, /etc or the Docker socket | Named volumes; host paths read-only (:ro) and as narrow as possible |
network_mode: host, pid: host | Shares the host network stack or process namespace | Default bridge networks; dedicated networks per application tier |
devices | Exposes host devices | Only specific devices, never whole /dev |
image | Pulls and runs any image | Images from your own registry, pinned by digest |
env_file, secrets/configs with file:, include, extends | Read files from the host, and the trust model notes their contents can appear in docker compose config output | Review resolved output; keep referenced files inside the project |
provider | Runs a binary on the host, outside any container, on up, down or stop | Treat as host code execution; allow only reviewed providers |
ports without a host IP | Listens on all host addresses | "127.0.0.1:8080:80" unless external access is required |
A hardened service in Compose typically combines user: set to a non-root UID, read_only: true with tmpfs for scratch space, cap_drop: [ALL], security_opt: ["no-new-privileges:true"], resource limits so one container cannot exhaust the host, and an image pinned by digest. OWASP's cheat sheet recommends the same set: set a user, drop all capabilities and add back only what is needed, use no-new-privileges and run with a read-only filesystem.
Remote and nested Compose files
- Resolve before you run:
docker compose configshows the fully merged configuration, includinginclude,extends, overrides and interpolated variables. A top-level file can look safe while a nested reference adds a privileged service. - Pin by digest: Docker's guidance is to reference remote Compose artefacts by digest, because tags are mutable, and to treat a digest update as a code change.
- Do not accept prompts blindly: Compose warns when a configuration references remote sources; read every reference in the chain.
- Gate in CI: avoid public or unverified Compose files in pipelines, which often hold cloud tokens and Docker sockets.
Secrets in Compose and builds
Docker's Compose documentation explains the problem with environment variables directly: they are often available to all processes, access is hard to track, and they can be printed in logs when debugging. Secrets avoid this.
- Runtime secrets: declare them in the top-level
secrets:element (sourcefile,environmentorexternal) and grant them per service with the service-levelsecrets:attribute. Compose bind-mounts each one as a file at/run/secrets/<name>, which allows standard filesystem permissions inside the container. Compose secrets are supported on Linux containers only. _FILEvariables: many official images, includingmysqlandpostgres, accept variables such asMYSQL_PASSWORD_FILE=/run/secrets/db_password, so the password never sits in the environment.- Build secrets: Docker's build documentation states that build arguments and environment variables are inappropriate for passing secrets because they persist in the final image. Use
RUN --mount=type=secret,id=...in the Dockerfile andbuild: secrets:in Compose, or SSH mounts for Git access. - Keep secret source files out of Git: a
db_password.txtbesidecompose.yamlis only safer if it is generated at deploy time, excluded by.gitignoreand.dockerignore, and owner-only on the host. - Know what Compose secrets are not: with a
file:source, the secret is a bind-mounted host file, not an encrypted store. For production, source secrets from a secrets manager at deploy time. See the Secrets Management Guide.
Leaked .env files are one of the most common ways cloud credentials escape: in the exposed .env files campaign, attackers harvested credentials from publicly reachable environment files and used them for cloud extortion. Images are the other route: see the Secrets in Container Images and Build Artefacts Guide.
Networking and port publishing
Docker's port publishing documentation calls publishing container ports "insecure by default": a published port is reachable from outside the host, not just from the host. Two Docker behaviours make this worse than many teams expect:
- ufw is bypassed: Docker's packet filtering page states that Docker and ufw are incompatible in the way they use firewall rules: published container traffic is diverted in the
nattable before ufw's rules apply. A host that looks locked down inufw statuscan still expose every published port. Filter in theDOCKER-USERchain or at an upstream firewall instead. - Bind addresses:
-p 127.0.0.1:8080:80(or"127.0.0.1:8080:80"in Compose) limits the port to the host. Databases, admin consoles and metrics endpoints should never be published on all addresses. - Separate networks per tier: containers on the same user-defined bridge network can reach each other on any port; put the database on an internal network shared only with the application.
- Forwarding: Docker enables IP forwarding and sets the forward policy to drop unless told otherwise; do not set
ip-forward-no-dropunless the host must route.
Where identity fits
Docker hosts hold some of the most powerful non-human credentials in an estate: registry tokens in ~/.docker/config.json, cloud credentials passed to containers, CI tokens on build agents and TLS client certificates for the daemon. Each should be scoped to one job, stored outside the image and rotated.
- Registry credentials: use a credential helper rather than base64 credentials in
config.json, and scoped, expiring tokens for CI. See the Docker Hub Tokens Security Guide. - Daemon TLS certificates: a client certificate for a TLS-protected daemon is a root credential for that host. Issue it per person or per pipeline, with short lifetimes.
- Cloud credentials: prefer workload identity from the platform (instance roles, OIDC federation) to static keys in
.envfiles. - The
dockergroup as an entitlement: include it in access reviews alongside sudo rights.
Detection and measurement
- Daemon events: stream
docker events(or the engine API events endpoint) to the SIEM and alert on containers started with--privileged, host namespaces, host root bind mounts or a socket mount. - Host audit: auditd watches on
/usr/bin/dockerd,/etc/docker/,/var/run/docker.sockand the docker systemd units show tampering and unexpected socket use. See the Linux Logging and auditd for Defenders Guide. - External exposure: scan your own address space for TCP 2375 and 2376 and for published database and admin ports.
- Compose policy checks: lint
docker compose configoutput in CI and fail on the risky fields above. - Runtime: eBPF or audit-based sensors catch shells in containers, writes to host paths and calls to the Docker API from inside containers. See the Container Runtime Security Guide.
| Metric | What it shows | Target |
|---|---|---|
| Hosts with a TCP daemon listener reachable outside a management network | Remote root exposure | Zero |
| Containers with the Docker socket mounted | Daemon access from workloads | Zero, or documented exceptions on isolated hosts |
| Running containers as root, privileged or with host namespaces | Container posture | Falling; privileged near zero |
Members of the docker group per host | Privileged access sprawl | Reviewed quarterly; falling |
Compose services with secrets in environment: | Secret handling | Zero in production |
| Published ports bound to all addresses that should be internal | Network exposure | Zero |
Implementation roadmap
- Inventory: list Docker hosts, engine versions, daemon listeners,
dockergroup members, containers with socket mounts and every published port. - Close remote root first: remove unauthenticated TCP listeners, move remote administration to SSH or TLS client certificates, and remove socket mounts from application containers.
- Set safe Compose defaults: publish a reviewed base Compose fragment (non-root user,
read_only,cap_drop: [ALL],no-new-privileges, localhost-bound ports) and add a CI check on resolved configuration. - Move secrets: replace secrets in
environment:and build arguments with Compose secrets,_FILEvariables and build secret mounts. - Pilot daemon changes: try
no-new-privilegesas a daemon default,userns-remapor rootless mode on a test ring; keep the previousdaemon.jsonunder version control as the rollback. - Monitor and review: daemon events in the SIEM, quarterly review of the
dockergroup and exposure scans.
Common failure patterns
- Daemon on 2375 for "temporary" remote access: the listener stays, the host is found by internet scanners and turned into a miner.
- Socket mounted for convenience: a monitoring, CI or reverse-proxy container gets
/var/run/docker.sock, and a flaw in it becomes host root. A:roflag on the mount does not help. - Database published on 0.0.0.0 behind ufw: the team trusts the host firewall, which Docker's NAT rules bypass.
- Secrets in
environment:and.envcommitted to Git: credentials appear indocker inspect, logs and the repository history. - Build arguments for tokens: an npm or Git token passed with
--build-argpersists in the image and is shipped to the registry. - Running a remote Compose file unreviewed: a nested
includeadds a privileged service nobody saw in the top-level file.
Framework mapping
| Framework | Reference | How it applies |
|---|---|---|
| NIST SP 800-190 | Application Container Security Guide | Host, orchestrator, image and container risks and countermeasures |
| NIST SP 800-53 Rev. 5 | AC-6 Least Privilege; CM-7 Least Functionality; SC-39 Process Isolation; CM-6 Configuration Settings | Socket and group access, minimal capabilities, isolation modes, hardened defaults |
| NIST CSF 2.0 | PR.PS-01; PR.IR-01 | Configuration management; protecting environments from unauthorised access |
| MITRE ATT&CK v20 | T1610 Deploy Container; T1611 Escape to Host; T1552.007 Container API; T1613 Container and Resource Discovery; T1612 Build Image on Host | Abuse of exposed daemons and sockets, credential theft from the API, host escape |
| MITRE D3FEND | D3-EI Execution Isolation | Rootless mode, user namespaces, seccomp and capability limits |
Practitioner checklist
- Remove every unauthenticated TCP daemon listener; use SSH or TLS client certificates on 2376 from a management network only.
- Treat
dockergroup membership and daemon client certificates as privileged access and review them. - Remove
/var/run/docker.sockmounts from application containers. - Use rootless mode for CI runners and multi-user hosts, or
userns-remapwhere rootless is not practical. - Run
docker compose configand a policy check on the resolved output before deploying any Compose file; pin remote references by digest. - Set non-root
user,read_only,cap_drop: [ALL]andno-new-privilegeson every service unless an exception is recorded. - Pass runtime secrets as Compose secrets and build secrets with
--mount=type=secret, never as environment variables or build arguments. - Bind internal ports to
127.0.0.1and filter published ports inDOCKER-USERor upstream, not with ufw alone. - Stream daemon events to the SIEM and alert on privileged containers, host mounts and socket mounts.
Standards and references
- Docker: Docker Engine security
- Docker: Protect the Docker daemon socket
- Docker: Rootless mode
- Docker: Manage secrets securely in Docker Compose
- Docker: Compose trust model
- Docker: Packet filtering and firewalls
- OWASP: Docker Security Cheat Sheet
- NIST: SP 800-190 Application Container Security Guide
Related NHI Mgmt Group resources: Container Security Guide · Container Runtime Security Guide · Secrets in Container Images and Build Artefacts Guide · Docker Hub Tokens Security Guide · NIST SP 800-190 Container Security Guide