How to Secure the Docker Remote API
The safest way to secure Docker remote API access is to keep the daemon off the network entirely and reach it over SSH. If you genuinely need a TCP endpoint, protect it with mutual TLS (both server and client present certificates) and lock the port behind a firewall. Never bind the daemon to 0.0.0.0:2375 unauthenticated. That single line of config has handed root on thousands of servers to cryptominers.
Why you must secure Docker remote API access
The Docker daemon runs as root. The API that controls it does not check who you are unless you tell it to. So anyone who can reach the socket can run a container, mount the host filesystem into it, and read or write any file on the box. Here is the classic escape, and it works against any open daemon:
# Given a reachable daemon on 1.2.3.4:2375
docker -H tcp://1.2.3.4:2375 run -v /:/host -it alpine chroot /host sh
That mounts the host root at /host and drops you into a shell on the machine itself. No exploit, no CVE. It is the API working as designed. The design just assumes the transport is already trusted.
This is why the mistake is so costly. A firewall gap on port 2375 is not an information leak. It is a remote root shell. Scanners sweep the whole IPv4 space for it constantly, and an open daemon usually gets a mining container within hours.
Check how your daemon is currently exposed
Before changing anything, find out what you have. On the host:
# What is dockerd actually listening on?
sudo ss -tlnp | grep dockerd
# Inspect the daemon config
sudo cat /etc/docker/daemon.json
sudo systemctl cat docker | grep ExecStart
If you see -H tcp://0.0.0.0:2375 in the ExecStart line or a hosts entry pointing at tcp://0.0.0.0 with no TLS, stop and fix that now. Port 2375 is plaintext. Port 2376 is the convention for TLS-protected traffic, but the port number alone protects nothing. TLS has to actually be configured.
Option 1: Don't expose it. Use SSH.
This is the right answer for most people. Leave the daemon on its default Unix socket, /var/run/docker.sock, which is only reachable locally and guarded by file permissions. Then use Docker's built-in SSH transport to reach it:
# From your laptop, talk to a remote daemon over SSH
docker -H ssh://deploy@server.example.com ps
# Or set it once as a context
docker context create prod --docker "host=ssh://deploy@server.example.com"
docker context use prod
docker ps
There is no new port, no certificate to rotate, no daemon reconfiguration. You reuse the SSH auth, key management, and audit logging you already run. The user just needs SSH access and membership in the docker group on the remote host. Requires a reasonably recent Docker CLI and OpenSSH on both ends.
Be honest about what the docker group means, though. Adding a user to it is equivalent to giving them root, because of the chroot trick above. Grant it the same way you grant sudo, and no more widely.
Managing servers over SSH is also how Docker HQ works. It talks to your hosts over standard SSH connections, so there is no daemon port to open and nothing extra to secure on the host. You point it at a server the same way you would ssh into it, and you can check containers, logs, and host metrics from your phone. If a host or container goes down you get a push alert instead of finding out from a customer.
Option 2: If you must open a TCP port, require mutual TLS
Some tools (older CI runners, certain orchestration setups) need a real TCP endpoint. If that is you, run it with mutual TLS so both sides authenticate. The server proves its identity to the client, and, more importantly, the client proves its identity to the server. Without client verification, TLS only gives you encryption, and anyone can still connect.
First create a CA and a server certificate. Set the CN or subjectAltName to the host's real DNS name:
# CA key and cert
openssl genrsa -aes256 -out ca-key.pem 4096
openssl req -new -x509 -days 365 -key ca-key.pem -sha256 -out ca.pem
# Server key + signing request
openssl genrsa -out server-key.pem 4096
openssl req -subj "/CN=server.example.com" -sha256 -new -key server-key.pem -out server.csr
# Allow the daemon's hostname/IP
echo subjectAltName = DNS:server.example.com,IP:10.0.0.5 > extfile.cnf
echo extendedKeyUsage = serverAuth >> extfile.cnf
openssl x509 -req -days 365 -sha256 -in server.csr -CA ca.pem -CAkey ca-key.pem \
-CAcreateserial -out server-cert.pem -extfile extfile.cnf
Then a client certificate signed by the same CA:
openssl genrsa -out key.pem 4096
openssl req -subj '/CN=client' -new -key key.pem -out client.csr
echo extendedKeyUsage = clientAuth > extfile-client.cnf
openssl x509 -req -days 365 -sha256 -in client.csr -CA ca.pem -CAkey ca-key.pem \
-CAcreateserial -out cert.pem -extfile extfile-client.cnf
Configure the daemon to demand a client cert. In /etc/docker/daemon.json:
{
"tlsverify": true,
"tlscacert": "/etc/docker/certs/ca.pem",
"tlscert": "/etc/docker/certs/server-cert.pem",
"tlskey": "/etc/docker/certs/server-key.pem",
"hosts": ["unix:///var/run/docker.sock", "tcp://0.0.0.0:2376"]
}
The key setting is tlsverify: true. That is what makes the daemon reject any client that does not present a certificate signed by your CA. Restart Docker, then connect from the client:
docker --tlsverify \
--tlscacert=ca.pem --tlscert=cert.pem --tlskey=key.pem \
-H tcp://server.example.com:2376 ps
A note on that hosts array. If you set hosts in daemon.json, the systemd unit's own -H flag will conflict and the daemon will refuse to start. Add a drop-in that clears it:
sudo mkdir -p /etc/systemd/system/docker.service.d
sudo tee /etc/systemd/system/docker.service.d/override.conf <<'EOF'
[Service]
ExecStart=
ExecStart=/usr/bin/dockerd
EOF
sudo systemctl daemon-reload && sudo systemctl restart docker
Lock the port down anyway
TLS proves identity. It does nothing to reduce your attack surface. Keep the port off the public internet regardless. If your clients live in a known network, allow only those addresses:
# Allow the CI subnet, drop everyone else on 2376
sudo ufw allow from 10.0.0.0/24 to any port 2376 proto tcp
sudo ufw deny 2376/tcp
Better yet, bind the daemon to a private interface or a WireGuard address so the port never faces the internet at all. Defense in depth here is cheap: firewall plus mTLS means an attacker needs both a network path and a valid client cert.
Protect the socket itself
Even with no TCP port open, the Unix socket is still a root-equivalent target. Two habits matter.
Do not bind-mount /var/run/docker.sock into containers casually. Plenty of tools (dashboards, CI agents) ask for it. Any container with the socket can control the daemon and therefore own the host. If a tool truly needs it, treat that container as if it were root on the box. A read-only proxy like docker-socket-proxy can expose just the endpoints a tool needs instead of the whole API.
And audit who is in the docker group. That is your real access list.
These controls are one layer. Pair them with the rest of your hardening: see Docker security best practices for production for the full checklist, how to run a Docker container as a non-root user to reduce blast radius inside containers, and Docker secrets management for keeping credentials out of images and environment variables.
Frequently asked questions
Is port 2375 ever safe to use?
No. Port 2375 is the plaintext, unauthenticated convention. Anything that can route to it gets root on the host. Only use it bound to 127.0.0.1 for local tooling, and even then SSH or the Unix socket is a cleaner choice. Public exposure of 2375 is scanned and exploited within hours.
What is the difference between tls and tlsverify?
--tls (or tls: true) encrypts the connection and has the server present a certificate, but it accepts any client. --tlsverify (or tlsverify: true) additionally requires the client to present a certificate signed by your trusted CA. Only tlsverify gives you authentication. Encryption without authentication still lets anyone connect.
Should I use SSH or TLS to secure the Docker remote API?
Use SSH unless a specific tool forces you to open a TCP port. SSH reuses auth and key management you already operate, opens no new port, and is a one-line docker -H ssh://.... Reach for mutual TLS only when a client cannot speak the SSH transport, and firewall the port on top of it.
How do I know if my Docker daemon is already exposed?
Run sudo ss -tlnp | grep dockerd on the host to see what it is listening on. From outside, curl http://your-host:2375/version returning JSON means the API is open and unauthenticated. If that call succeeds from the public internet, assume the host is already compromised and rebuild it.
Your next step
SSH into your hosts right now and run sudo ss -tlnp | grep dockerd. If you see anything bound to 0.0.0.0:2375, that server is one scan away from a mining container, or already running one. Fix that host first, then switch your workflow to SSH contexts so there is no port to forget about again.