
Back in the 4Cs post I walked an attack chain that started with a single exposed Docker port and ended with a container escape to the host. This is the other half of that story: what that port actually is, why it's so catastrophic when it's open, and how you lock the Docker daemon down properly. Because "an exposed Docker socket is game over" gets repeated a lot, and I wanted to actually understand the why, not just parrot it.
I'll start with the daemon itself, show the door people accidentally leave open, then close it step by step. What, how, why, takeaway.
What the Docker daemon is (and why it's root)
When you type docker run, the docker command isn't doing the work. It's just a client. The real engine is a background program called the Docker daemon (dockerd), and it's the thing that actually pulls images, starts containers, mounts volumes and talks to the kernel. On Linux it usually runs as a systemd service, so it starts at boot and stays up. You can peek at it:
systemctl status docker # is the daemon running?
systemctl start docker # start it
systemctl stop docker # stop it
Here's the part that matters for security: the daemon runs as root, and it can do anything root can. It can start a container that mounts the host's entire filesystem, or one that runs with full privileges. So whoever can send commands to the daemon effectively is root on that machine. Hold that thought, it's the whole point.
dockerd (add --debug for verbose logs printed straight to the terminal). It's useful when something won't start and you want to watch it fail in real time, rather than digging through journald.How you talk to it: the Unix socket
So how does the docker client reach the daemon? By default, through a Unix socket, a special file at /var/run/docker.sock. A Unix socket is just a local pipe between processes on the same machine, so this is a safe default: only someone already logged into the host (and in the right group) can use it. Nothing on the network can touch it.
So out of the box, Docker is local-only. The danger starts the moment you change that.
The door people leave open: the daemon on TCP
Sometimes you genuinely need to reach the daemon from another machine, managing a server from your laptop, or wiring it into some CI tool. To do that, you tell the daemon to also listen on a TCP port (a network port), not just the local socket. And this is exactly where it goes wrong.
The naive way looks harmless:
dockerd --host=tcp://192.168.1.10:2375
Then from your laptop you point the client at it and it just works:
export DOCKER_HOST="tcp://192.168.1.10:2375"
docker ps
Port 2375 is the plain, unencrypted Docker API. No TLS, no login, nothing. Anyone who can reach that port can run docker commands as root on your host. And people leave these open to the internet constantly, scanners find them in seconds.
Locking it down, step by step
Right, how to do it properly. The mindset is layered: first don't expose it at all, then if you must, encrypt it, then on top of that, authenticate it. Here's the order I'd work in.
SSH keys, no root login, close spare ports
don't expose TCP unless you truly need to
if remote, use 2376 + TLS, never 2375
tlsverify + client certs, or it's still open
Step 1: secure the host first
Before touching Docker, treat the machine like any server you'd harden. Turn off direct root login, only give accounts to people who need them, use SSH keys instead of passwords, and close ports you're not using. None of the Docker settings below matter if the box itself is soft. This is the same "patch and harden the base" idea from the CIS benchmarks post.
Step 2: if you expose it, bind to a private interface only
If you really do need remote access, at least don't put it where the whole internet can see it. Bind the daemon to a private IP, and double-check it isn't reachable on any public-facing interface. In /etc/docker/daemon.json (this file doesn't exist by default, you create it):
{
"hosts": ["tcp://192.168.1.10:2375"]
}
That keeps it on the internal network, but it's still unencrypted and unauthenticated. It's a step, not the destination. Keep going.
Step 3: encrypt with TLS on 2376
Now encrypt the connection with TLS (the same certificate tech that puts the padlock on HTTPS). You set up a certificate authority (CA), generate a server certificate and key, switch the port to the TLS convention 2376, and turn TLS on:
{
"hosts": ["tcp://192.168.1.10:2376"],
"tls": true,
"tlscert": "/var/docker/server.pem",
"tlskey": "/var/docker/serverkey.pem"
}
Now traffic is encrypted, so nobody can sniff your Docker commands off the wire. But, and this catches people, TLS on its own only proves the server's identity and scrambles the traffic. It does nothing to check who the client is. Anyone can still connect; they just do it over an encrypted channel. Encryption is not authentication.
Step 4: authenticate clients with tlsverify
This is the step that actually locks the door. You turn on tlsverify, which tells the daemon: only accept clients whose certificate was signed by our CA. Now the CA has to sign a client certificate too, and only people you hand that certificate to can connect. Both sides prove themselves. The full daemon.json:
{
"hosts": ["tcp://192.168.1.10:2376"],
"tls": true,
"tlsverify": true,
"tlscacert": "/var/docker/cacert.pem",
"tlscert": "/var/docker/server.pem",
"tlskey": "/var/docker/serverkey.pem"
}
On the client side you hand over your signed certificate. Docker even auto-detects certs placed in ~/.docker, or you can point at them by hand:
export DOCKER_TLS_VERIFY=true
export DOCKER_HOST="tcp://192.168.1.10:2376"
docker --tlscacert=ca.pem --tlscert=client.pem --tlskey=client-key.pem ps
tls encrypts and tlsverify authenticates, and you need both. Encryption without authentication is a locked-looking door that anyone can walk through.
daemon.json, restart the service (systemctl restart docker) for it to take effect. Second: don't set the same option in both daemon.json and on the command line, Docker sees the conflict and refuses to start. Pick one place (the file is the tidy choice).Finding an exposed daemon (yours or theirs)
From the attacker's side this is trivial recon, which makes it a great audit for defenders too. The check is simply: is anything answering on 2375 or 2376, and does it talk without a certificate?
tlsverify with CA-signed client certs. Firewall the port to known admin IPs. And remember the socket itself is powerful, mounting /var/run/docker.sock into a container hands that container root on the host, so treat that mount like handing over the keys.What finally sat right
Two posts in, the exposed-Docker-daemon thing finally sits right in my head. It's not that Docker is insecure, it's that the daemon is root by design, so the only real question is who's allowed to talk to it. Local socket: fine. TCP with no auth: you've handed root to the network. The bit I want to hammer home to myself is that encryption and authentication are different jobs, TLS alone feels secure and isn't, and tlsverify is the flag that actually matters. I've done the daemon.json side but not yet stood up my own CA and minted client certs end to end, so that's the next lab, and I suspect the cert paths will fight me. Next in the series I want to bring this back up to Kubernetes and look at the container runtime there, now that the Docker foundations make sense. If you audit hosts, tell me, do you still find open 2375s in the wild? I bet you do.
If this cleared up the Docker daemon for you, come say hi on LinkedIn or the contact page, and tell me what to break down next. More in the Kubernetes Journey.
Further reading
- Docker docs: protect the Docker daemon socket
- Docker docs: dockerd and daemon.json reference
- Docker docs: Docker security
FAQ
Why is exposing the Docker daemon on TCP dangerous?
The Docker daemon runs as root and can start any container, mount any host path and read any volume. If you expose its API on a TCP port with no encryption or authentication, anyone who reaches that port effectively gets root on the host. It is one of the fastest paths to full compromise.
What is the difference between Docker ports 2375 and 2376?
Port 2375 is the plain, unencrypted Docker API port and should never be used on a real network. Port 2376 is the convention for the TLS-secured API. Using 2376 with tlsverify means traffic is encrypted and only clients holding a valid CA-signed certificate can connect.
How do I secure the Docker daemon?
Keep it on the local Unix socket unless you truly need remote access. If you must expose it, bind only to a private interface, enable TLS on port 2376, and turn on tlsverify with CA-signed server and client certificates so encryption and authentication are both enforced via daemon.json.
Related reading
- Topic 4: The 4Cs & a Kubernetes attack chain (the exposed Docker port in action)
- Topic 11: Kubelet security (another root-level API to lock down)
- Topic 7: TLS, certificates & PKI (the certs behind tlsverify)
- Browse the whole Kubernetes Journey