Why does exposing dockerd on tcp://0.0.0.0:2375 hand out root, and what is the safe alternative?
answer
- The API has no users and no passwords
- Reaching the endpoint is the authorization
- One run flag reaches the host filesystem
- 2375 plaintext, 2376 protected by convention
- --tls encrypts, --tlsverify checks the client
basics
~20 sPort 2375 is the plain, unauthenticated Engine API, and that API is root-equivalent: any caller can start a privileged container mounting the host root filesystem. Prefer an ssh:// endpoint, or dockerd --tlsverify on 2376, never publicly reachable.
solid answer
~50 sThe Engine API has **no authentication of its own**. Its authorization model is "you can reach the endpoint", and what it grants is total: `docker run -v /:/host --privileged` on that engine reads every file, writes authorized_keys for root, and loads kernel modules. So `-H tcp://0.0.0.0:2375` publishes root on the host to the whole network, and internet-facing 2375 is scanned and mined within minutes. The two safe transports are: an `ssh://` context, which reuses per-user SSH keys and exposes no new port at all; or TLS on 2376 with `dockerd --tlsverify --tlscacert --tlscert --tlskey -H tcp://0.0.0.0:2376`, where the client must present a certificate signed by the same CA (`--tls` alone only encrypts and authenticates nobody). Even then, certificates are all-or-nothing - there are no roles - so keep the endpoint on a private network and treat a client key like a root password.
code
bash · 5 linesdockerd --tlsverify \
--tlscacert=/etc/docker/ca.pem \
--tlscert=/etc/docker/server-cert.pem \
--tlskey=/etc/docker/server-key.pem \
-H tcp://10.4.19.37:2376 -H unix:///var/run/docker.sockgo deeper
Learn the headline: the Docker Engine API has no login, so an open TCP port on 2375 is equivalent to handing out root on that machine. Never enable it to try something out.
Explain the mechanism - a container with the host root filesystem bind-mounted and extra privileges - and the port convention, plus what --tlsverify requires that --tls does not.
Demonstrate operating judgement: pick ssh:// by default, bind listeners to private interfaces, and treat a discovered open 2375 as an incident with credential rotation, not a config change.
Own the policy: who may reach an engine at all, how client credentials are issued and revoked, and whether a root-equivalent shared build host should exist rather than ephemeral per-job builders.
## The API is the security boundary, and it is root-shaped `dockerd` normally listens on `/var/run/docker.sock`, a unix socket whose file permissions - root, group `docker` - are the entire access-control system. Adding `-H tcp://0.0.0.0:2375` puts that same API on a TCP port with **no authentication whatsoever**. There is no user, no password, no token, no per-verb permission in the Engine API. Reaching it is the authorization. And what it authorizes is not "managing containers", it is the host. A caller who can reach the API can start a container with the host's root filesystem bind-mounted and extra privileges, at which point every file on the machine is readable and writable - private keys, database credentials, `/etc/shadow`, another user's home directory - and adding a key to root's `authorized_keys` turns API access into a root login. Nothing about images or containers needs to be understood to do this; it is one `docker run` away. This is why open 2375 is not a theoretical finding. Internet-wide scanners look for it continuously, and an exposed daemon is typically found and used to run mining or botnet containers in minutes to hours, not weeks. "It is only on the office network" is a weaker mitigation than it sounds, because it means every laptop, CI runner and compromised browser on that network holds root on the build host. ## Convention: 2375 is plaintext, 2376 is TLS The ports are only a convention, but a universally followed one. 2375 is the unencrypted API; 2376 is the TLS-protected one. Seeing 2375 in a systemd drop-in or a cloud image is itself the finding. ## Option 1: ssh:// - usually the right answer ``` docker context create build-host --docker "host=ssh://[email protected]" ``` This opens no new listening port at all. The CLI reaches the remote engine through the SSH service that is already there and already managed: per-user keys, key rotation and revocation, a real user identity in the logs, and whatever multi-factor or bastion policy the organisation already applies to shells. Authorization on the far side is still just membership of the `docker` group, so it remains a root-equivalent grant, but you now know *which* human or robot used it and you can withdraw one key without reissuing anything to anyone else. For a small team sharing one build host, this is almost always the correct choice, and it is the default recommendation for that reason. ## Option 2: mutual TLS on 2376 When SSH is not available - a daemon consumed by a tool that speaks the API directly, for example - TLS with client certificates is the built-in mechanism: ``` dockerd --tlsverify \ --tlscacert=/etc/docker/ca.pem \ --tlscert=/etc/docker/server-cert.pem \ --tlskey=/etc/docker/server-key.pem \ -H tcp://0.0.0.0:2376 -H unix:///var/run/docker.sock ``` The distinction that matters: `--tls` turns on encryption but verifies no client, so it stops eavesdropping and stops nothing else. `--tlsverify` additionally requires the client to present a certificate signed by the CA in `--tlscacert`, which is what actually turns "anyone on the network" into "holders of an issued key". On the client side, `DOCKER_TLS_VERIFY=1` plus `DOCKER_CERT_PATH` pointing at a directory containing `ca.pem`, `cert.pem` and `key.pem`, or the same material baked into a context so it travels with the endpoint name. Two caveats. A client certificate is all-or-nothing - the API has no roles, so a certificate issued for "just running builds" is also a certificate for reading every file on the host. And you now own a small CA: issuing, distributing, expiring and revoking keys, with revocation being the part teams reliably forget. ## Defence in depth around either choice Bind the listener to a private interface rather than `0.0.0.0` whenever a network endpoint is needed at all, and put a host firewall in front of it so reachability is an explicit allowlist. If a genuinely restricted API surface is required, an authorization plugin can gate individual API calls - but treat that as a specialist control rather than the baseline. And keep the daemon's own configuration honest: specifying listen addresses both as a `dockerd` flag and in the daemon configuration file makes the daemon refuse to start with a complaint that the directive is given twice. ## In a review, the checklist is short Is anything listening on 2375? Is a TCP listener bound to `0.0.0.0`? Is `--tlsverify` present, or only `--tls`? Who holds client keys, and what happens when one leaks? If the answer to the last question is "we would reissue everyone's", that is an argument for `ssh://`.
- What is the difference between starting dockerd with --tls and with --tlsverify?`--tls` enables TLS so the traffic is encrypted, but the daemon accepts any client that completes the handshake - it authenticates nobody. `--tlsverify` additionally requires the client to present a certificate signed by the CA given in `--tlscacert`. Only the second one turns network reachability into an issued credential; the first is often mistaken for security.
- You find a build host with 2375 open. Beyond closing the port, what do you assume?Treat the host as compromised until proven otherwise. Anyone who reached it could read every file and plant persistence, so rotate every credential the machine held, inspect running containers and images for ones nobody created, check root's authorized_keys and scheduled jobs, and prefer rebuilding the host over cleaning it. Then close the port and move the CLI to ssh://.
- Why is an ssh:// context usually preferred over mutual TLS for a shared build host?It adds no listening service, and it reuses identity infrastructure that already exists: per-user keys, an audit trail of who connected, and revocation by removing one key. Running your own CA means issuing, distributing and expiring certificates yourself, and revocation is the step teams skip. Both still grant root-equivalent access once connected.
saying these in an interview costs you the question
- Thinks the Engine API has usernames or tokens
- Believes a firewall alone makes 2375 acceptable
- Uses --tls and assumes clients are authenticated
- Says API access only allows managing containers
- Treats an exposed daemon as a low-severity finding
- Assumes client certificates can be scoped to some commands