Secure remote access to the Docker daemon

By default, the Docker daemon listens for connections on a Unix socket to accept requests from local clients.

We are going to configure Docker to accept requests from remote clients by configuring it to listen on an IP address/port, in addition to the Unix socket.

There is a lot of conflicting, and incorrect information on the Internet on how to do this, especially when running Ubuntu, but after a bit of testing, I figured out the correct configuration.

This can allow remote, authenticated, access, although we will be configuring Docker to only accept connections on the localhost (127.0.0.1) address.

Modify Docker to listen also listen on an IP/port

Create, or add to, the /etc/docker/daemon.json file:

{
  "hosts": ["unix:///var/run/docker.sock", "tcp://127.0.0.1:2375"]
}

Using the 127.0.0.1 (localhost) address restricts connections from anyone local on the box itself. You can use an IP other than localhost, but I wouldn't recommend it, especially if you have an Internet-facing box...

Docker will further restrict this functionality in a future release:

Remote access without TLS is not recommended, and will require explicit opt-in in a future release.

In Ubuntu 24.04, I found that the docker.service.d directory didn't exist by default, even after installing Docker, although the directory does exist in Ubuntu 22.04.

sudo mkdir -p /etc/systemd/system/docker.service.d/

Create, or modify, the /etc/systemd/system/docker.service.d/override.conf file:

[Service]
ExecStart= 
ExecStart=/usr/bin/dockerd --containerd=/run/containerd/containerd.sock

Reload the Docker daemon and restart the Docker service:

sudo systemctl daemon-reload
sudo systemctl restart docker

Test the configuration changes

Check to see if the Docker daemon is listening for remote connections:

sudo netstat -lntp | grep dockerd
tcp        0      0 127.0.0.1:2375          0.0.0.0:*               LISTEN      2812/dockerd   

Connect to the local Docker service:

docker -H=127.0.0.1:2375 images
REPOSITORY    TAG       IMAGE ID       CREATED       SIZE
hello-world   latest    1b44b5a3e06a   3 weeks ago   10.1kB

This allows for remote access, but we configured the Docker daemon to only listen on the local host (127.0.0.1).

Securing the remote connection to Docker

While this provides some level of protection, anyone with access to localhost, which is pretty much anyone who can log into the box, now has full access to, and over, Docker.

To secure the Docker daemon, we will specify a trusted CA (Certificate Authority), require clients to authenticate using a CA-issued certificate, and encrypt the data using SSL (TLS technically).

From the dockerdocs page on securing the daemon:

If you need Docker to be reachable through HTTP rather than SSH in a safe manner, you can enable TLS (HTTPS) by specifying the tlsverify flag and pointing Docker's tlscacert flag to a trusted CA certificate. In the daemon mode, it only allows connections from clients authenticated by a certificate signed by that CA. In the client mode, it only connects to servers with a certificate signed by that CA.

Create certificates and keys

We are going to use openssl commands to do the following:

  1. Create a CA (Certificate Authority) key and certificate.
  2. Generate a CSR (Certificate Signing Request) for the Docker daemon.
  3. Generate a CSR for the client to use to authenticate with the Docker daemon when connecting.
  4. Use the CA created in step #1 to sign the CSRs for the Docker daemon and the client.

Once that is done, we will modify the Docker daemon to only accept secure (TLS) connections, and only from clients that we can verify their certificate using our CA.

After that, we will test the remote connection to Docker to make sure everything works.

Note that I use the -nodes switch, when creating the private keys, which removes the requirement for encryption (passphrase). In a production system, do not do this!!, but for lab use, this is ok.

Geek note: In openssl terminology, nodes is actually no DES, or no encryption.

Create CA certificate and key using openssl

We are going to create a certificate and key for our CA using this one-liner:

openssl req -new -newkey rsa:2048 -days 3650 -nodes -x509 \
-keyout ca-key.key -out ca.crt \
-subj "/C=US/ST=Colorado/L=Denver/O=Docker/OU=Docker/CN=CA"

Create the CSRs

We are going to create two CSRs, one for the Docker daemon to use, and one for the client to use to connect to the Docker daemon.

In theory, we could just create one CSR, and use the resulting single certificate and key for both the Docker daemon and the client, but it's good practice to keep the separate.

The CSR for the Docker daemon:

openssl req -new -newkey rsa:2048 -nodes \
-keyout docker-key.key -out docker.csr \
-subj "/C=US/ST=Colorado/L=Denver/O=docker/CN=docker" \
-addext "subjectAltName=DNS:docker-tls,DNS:localhost,IP:127.0.0.1"

It is important for the CSR for the Docker daemon to have the added subjectAltName that includes any DNS name(s) you might connect to the Docker daemon using (docker-tls and localhost in this example), along with any IP address(es), which I only included localhost (127.0.0.1).

Without this added information, the secure connection will fail.

The CSR for the client:

openssl req -new -newkey rsa:2048 -nodes \
-keyout client-key.key -out client.csr \
-subj "/C=US/ST=Colorado/L=Denver/O=client/CN=client" 

Use our CA to sign the CSRs

Similar commands are used to sign both the Docker and client CSRs.

Note that these are for my home lab use, so I make the valid dates for the certificates 10 years. In a non-lab environment, you will probably want to have a shorter valid time period.

For the Docker CSR:

openssl x509 -req -in docker.csr -CA ca.crt -CAkey ca-key.key \
-CAcreateserial -out docker-cert.crt -copy_extensions copyall \
-days 3650

The -copy_extensions copyall is needed to add the SAN information into the actual certificate.

For the client CSR:

openssl x509 -req -in client.csr -CA ca.crt -CAkey ca-key.key \
-CAcreateserial -out client-cert.crt -days 3650

Now that the CSRs are signed by the CA, we can delete them:

rm *.csr

Organize our certificates and keys

Correct the permissions of the certificates (0444) and keys (0400):

chmod 0444 *.crt
chmod 0400 *.key

Change ownership of the ca-key.key, docker-key.key, and docker-cert.crt files:

sudo chown root:root ca.crt ca-key.key docker-key.key 

Resulting file permissions for our certificates (0444) and keys (0400):

ls -l
total 24
-r-------- 1 root root 1704 Aug  2 20:17 ca-key.key
-r--r--r-- 1 root root 1318 Aug  2 20:17 ca.crt
-r--r--r-- 1 tom  tom  1180 Aug  2 20:17 client-cert.crt
-r-------- 1 tom  tom  1704 Aug  2 20:17 client-key.key
-r--r--r-- 1 tom  tom  1334 Aug  2 20:17 docker-cert.crt
-r-------- 1 root root 1704 Aug  2 20:17 docker-key.key

Files we created:

├── ca-key.key        << CA key
├── ca.crt            << CA certificate
├── client-cert.crt   << client certificate
├── client-key.key    << client key
├── docker-cert.crt   << Docker daemon certificate
└── docker-key.key    << Docker daemon key

Move client's client-cert.crt and client-key.key user's home directory, and copy the ca.crt into that same directory:

mkdir ~/.docker
mv client-cert.crt client-key.key ~/.docker/
cp ca.crt ~/.docker/

Personally, I like to keep the keys and certificates for Docker in the /etc/docker/ssl directory:

sudo mkdir -p /etc/docker/ssl/keys /etc/docker/ssl/certs

Move the ca.crt and docker-cert.crt certificates into the cert directory:

sudo mv ca.crt docker-cert.crt /etc/docker/ssl/certs/

Move the ca-key.key and docker-key.key keys into the keys directory:

sudo mv ca-key.key docker-key.key /etc/docker/ssl/keys

Modify the Docker daemon config for secure access

We will need to add the TLS options to the /etc/docker/daemon.json file, and change the listening port to 2376:

{
  "tlscacert": "/etc/docker/ssl/certs/ca.crt",
  "tlscert": "/etc/docker/ssl/certs/docker-cert.crt",
  "tlskey": "/etc/docker/ssl/keys/docker-key.key",
  "tlsverify": true,
  "hosts": ["unix:///var/run/docker.sock", "tcp://127.0.0.1:2376"]
}

Breakdown of the TLS options:

  • tlscacert: The trusted CA certificate, we will only accept connections from clients that provide a certificate signed by this CA
  • tlscert: The server certificate to use
  • tlskey: The server key to use
  • tlsverify: Verify any client that attempts to connect, using the above listed tlscacert. This is optional, but without it, you have no control over who can access your Docker daemon.

Restart the Docker daemon:

sudo systemctl restart docker

Now we are listening on port 2376:

sudo netstat -lntp | grep dockerd
tcp        0      0 127.0.0.1:2376          0.0.0.0:*               LISTEN      1185/dockerd     

Rename certificates and keys for use in Docker CLI

The Docker CLI has an environmental variable DOCKER_CERT_PATH that can be set to specify the path to the Docker CLI certificates and keys, which defaults to ~/.docker/.

However, Docker doesn't provide a way of specifying the names of the certificates and key, except for manually specifying the certificates and key, with the full path !

By default, the Docker CLI expects these names for the client certificate and key, and the CA's certificate:

cert.pem      << Client certificate
key.pem       << Client key
ca.pem        << CA certificate

So that leaves us with three choices:

  1. Run Docker CLI commands and specify, with full path, the client/CA certificates and client key.

  2. Run any/all Docker CLI commands from ~/.docker/.

  3. Rename the certificates and key to match what the Docker CLI expects.

Simple solution is #3:

mv ~/.docker/ca.crt ~/.docker/ca.pem
mv ~/.docker/client-key.key ~/.docker/key.pem
mv ~/.docker/client-cert.crt ~/.docker/cert.pem

Resulting files:

ls -l ~/.docker/
total 12
-r--r--r-- 1 root root 1318 Aug  2 19:40 ca.pem
-r--r--r-- 1 tom  tom  1180 Aug  2 19:36 cert.pem
-r-------- 1 tom  tom  1704 Aug  2 19:35 key.pem

Testing the secure connection

Attempting to connect without using the client's certificate and key fails:

docker -H=127.0.0.1:2376 images
Error response from daemon: Client sent an HTTP request to an HTTPS server.

Specifying the names of the client and CA certificates, along with the client's key:

docker --tlsverify --tlscacert=ca.pem --tlscert=cert.pem \
--tlskey=key.pem -H=127.0.0.1:2376 images
REPOSITORY    TAG       IMAGE ID       CREATED       SIZE
hello-world   latest    1b44b5a3e06a   3 weeks ago   10.1kB

If we use the certificate and key names the Docker CLI expects, we can actually omit the names of the certificates and key, and just use the --tlsverify switch:

docker --tlsverify -H=127.0.0.1:2376 images
REPOSITORY    TAG       IMAGE ID       CREATED       SIZE
hello-world   latest    1b44b5a3e06a   3 weeks ago   10.1kB

If you use different names for the CA/client certificates and the client's key, or place them somewhere other than the ~/.docker/ directory, you need to specify the full path to the CA/client certificates and client key:

docker --tlsverify --tlscacert=/root/test/CertificateAuthority.pem \
--tlscert=/root/test/clientCert.pem --tlskey=/root/test/clientKey.pem \
-H=127.0.0.1:2376 images
REPOSITORY    TAG       IMAGE ID       CREATED       SIZE
hello-world   latest    1b44b5a3e06a   3 weeks ago   10.1kB

It makes life a lot easier to just use the certificate and key names listed above, and place them in the ~/.docker/ directory...

References

dockerdocs - Home / Manuals / Docker Engine / Daemon / Configure remote access for Docker daemon
https://docs.docker.com/engine/daemon/remote-access/

dockerdocs - Home / Manuals / Docker Engine / Security / Protect the Docker daemon socket
https://docs.docker.com/engine/security/protect-access/#use-tls-https-to-protect-the-docker-daemon-socket

dockerdocs - Home / Reference / CLI reference / dockerd
https://docs.docker.com/reference/cli/dockerd/