Docker audit fix

How to Secure the Docker Daemon TCP Socket on Linux

The Docker Engine API is root on the host and authenticates nobody by itself, so a daemon started with -H tcp:// hands that root to anyone who can route to the port. Keep dockerd on its unix socket and manage it remotely over SSH.

Hiren KalariyaLast reviewed: Oct 6, 2026Check docker-tcp-socket
High
severity
No
needs root or sudo
4
results it can return
2 of 8
checks in this audit

Every threshold on this page is transcribed from the docker-security-daemon-socket audit script that CtrlOps runs, and a build check fails if the two ever disagree. See all 8 Daemon & Socket checks, or how the audit runs.

High severityNo sudo needed

What this check reads

It gathers three independent signals and concatenates them into one string. ss -tln supplies any listener whose local address ends in :2375 or :2376, keeping the first two matches; ps -eo args supplies the first -H tcp://... on the running dockerd command line; and /etc/docker/daemon.json supplies the first tcp:// entry inside its "hosts" array. An empty combined string means the daemon is on the local unix socket only and the check PASSes. If it is not empty the port number alone decides severity: 2376 appearing anywhere in it is a WARN, on the assumption that TLS is in use, and anything else is a FAIL.

When it applies

Runs whenever the docker binary exists - it does not need daemon access, which is why it works unprivileged. It combines three independent signals: TCP listeners on 2375/2376 (via ss, if installed), -H tcp://… on the running dockerd command line, and a tcp:// entry in the "hosts" array of /etc/docker/daemon.json (readable without root on a default install). Because any one of them triggers the finding, a daemon configured but not currently listening is still reported. The port number alone decides severity: 2376 is assumed to mean TLS and produces WARN, anything else FAIL - the check never verifies that --tlsverify and client certificates are actually enforced.

What each result means

Result thresholds this check applies
ResultWhenWhat it means
SKIPDocker not installedNothing was checked. With no docker binary present, none of the three signals was collected.
WARNTCP on 2376 (verify `--tlsverify`)The daemon is on TCP, and the port number is the only reason this is not a failure. Nothing here confirmed that tlsverify and client certificates are enforced, so confirm it yourself.
FAILTCP on 2375 or any non-2376 TCP hostThe Docker API is reachable over TCP on a port that implies no TLS. Anyone who can route to it is root on this host.
PASSUnix socket onlyNo TCP listener, no -H tcp:// flag and no tcp:// host entry was found, so access runs through the local unix socket and its file permissions.

Why it matters

The Docker API is root-equivalent: anyone who can reach it can run docker run -v /:/host --privileged and own the machine. Unauthenticated TCP (2375) exposed to the internet is mass-scanned and exploited within minutes for cryptominers. Docker docs: "Protect the Docker daemon socket"; CIS Docker 2.7 "Ensure TLS authentication for Docker daemon is configured"; OWASP Docker Cheat Sheet Rule #1 "Do not expose the Docker daemon socket (even to the containers)".

Why it fails, and when it is wrong

  • Port 2376 with --tls but without --tlsverify encrypts but does not authenticate clients; the check cannot tell and reports WARN. Check ps -o args -C dockerd for --tlsverify and --tlscacert.
  • TCP bound to 127.0.0.1:2375 (some CI tools) still FAILs; it is reachable by any local user and via SSRF, and the daemon's local-only bind is a weak control. Prefer the Unix socket.
  • ss reports the port regardless of which process owns it; a different service on 2375/2376 would be a false positive. Verify with ss -tlnp.
  • systemd socket activation (docker.socket with ListenStream=0.0.0.0:2375) is detected via ss but not via the daemon.json grep.
  • The severity test is whether 2376 appears anywhere in the combined string, so a daemon listening on both 2375 and 2376 reports WARN, not FAIL. Read the listener list in the message and treat any 2375 in it as the FAIL it is.
  • The daemon.json signal only matches a "hosts" array written on one line; a pretty-printed array spread over several lines is not read. The command-line signal matches -H tcp:// but not the long form --host=tcp://. The ss signal still catches a listener on 2375 or 2376 in both cases, but a non-standard port configured that way is missed entirely.

How to fix it

Find where TCP is configured

Check all three places the audit reads before you change anything. A systemd drop-in is the usual culprit, not daemon.json.

systemctl cat docker docker.socket | grep -nE 'ExecStart|ListenStream'
sudo grep -n -A3 '"hosts"' /etc/docker/daemon.json
sudo ss -tlnp | grep -E ':(2375|2376)\b'

Remove the TCP listener

Delete the tcp:// entry from the hosts array in daemon.json, or the -H tcp:// flag from the drop-in, then reload and restart. Never set hosts in both places: the daemon refuses to start when it finds them in both.

sudo systemctl edit docker      # remove -H tcp://... from the ExecStart override
sudo systemctl daemon-reload
sudo systemctl restart docker

Manage the host over SSH instead

A Docker context over SSH reaches the daemon through its unix socket on the far side, with no port open at all.

docker context create remote --docker host=ssh://user@host
docker --context remote ps

If TCP is unavoidable, require mutual TLS

Run the daemon on 2376 with client certificate verification, so only clients holding a certificate signed by your CA can connect, and firewall the port to the addresses that need it.

dockerd --tlsverify --tlscacert=ca.pem --tlscert=server-cert.pem --tlskey=server-key.pem -H=0.0.0.0:2376
sudo ufw allow from 203.0.113.10 to any port 2376 proto tcp

--tls without --tlsverify encrypts the connection but accepts any client, so it does not count.

Verify the fix

# No daemon socket should be listening on TCP at all:
ss -tlnp | grep -E ':(2375|2376)\b'          # expected: no output
ps -eo args | grep '[d]ockerd'                # expected: no -H tcp://
sudo grep -n hosts /etc/docker/daemon.json    # expected: no tcp:// entry

# If TCP is deliberate, prove mutual TLS is actually enforced:
curl -s http://<host>:2376/version            # expected: fails (TLS required)
curl -s https://<host>:2376/version --cacert ca.pem   # expected: fails without a client cert
curl -s https://<host>:2376/version --cacert ca.pem --cert cert.pem --key key.pem   # expected: works

Debugging

Check all three sources above; a systemd drop-in is the usual culprit (systemctl cat docker | grep ExecStart), not daemon.json. Note that specifying hosts in both daemon.json and the unit makes the daemon refuse to start, which is why the drop-in is often the real source.

The check assumes TLS from the port number only. Confirm with the curl sequence above: if plain HTTP or a certificate-less client works, treat this as a FAIL, not a warning. Real enforcement needs --tlsverify, --tlscacert, --tlscert, --tlskey.

The check would miss a daemon on a non-standard TCP port that is set somewhere other than the three sources, and it cannot see a socket-forwarding proxy (a container publishing the socket over TCP, socat, or an exposed docker.sock behind a web server). Look for those with sudo ss -tlnp and the Docker Socket In Containers check.

That signal is silently unavailable; the command-line and daemon.json checks still run. Install iproute2 for full coverage.

Do not expose TCP. Use docker context over SSH (docker context create remote --docker host=ssh://user@host), which needs no open port at all.

Sources

How the script reads this

Next

Re-run the Daemon & Socket audit after applying the fix and confirm this check moves to PASS. CtrlOps runs all 8 checks over your existing SSH connection and scores the result, so the change is visible without reading another config file.

All 8 Daemon & Socket fixes
Audit your fleet

Find every one of these on every server, in one click

CtrlOps runs this audit over your existing SSH connection - no agents, no scripts to manage. $7/user/month after a 1 month free trial - no credit card required.

Windows

✓ Start instantly·✓ No credit card·✓ No sneaky autorenewals