Prerequisites
Before beginning this setup, make sure you have:
- Docker Engine installed and running (version 18.09 or higher)
- OpenSSL for certificate generation (usually pre-installed on Linux systems)
- Root or sudo access on the Docker host
TL;DR Version
Step 1: Generate Keys
# Create a directory for the certs
mkdir -p /etc/docker/certs
cd /etc/docker/certs
# Generate CA private and public keys
openssl genrsa -aes256 -out ca-key.pem 4096
openssl req -new -x509 -days 365 -key ca-key.pem -sha256 -out ca.pem
# Create a server key and certificate signing request (CSR)
openssl genrsa -out server-key.pem 4096
openssl req -subj "/CN=[YOUR_IP]" \
-sha256 -new \
-key server-key.pem \
-out server.csr
# Create a config file for the certificate
cat > server-extfile.cnf <<EOF
subjectAltName = IP:[YOUR_IP]
extendedKeyUsage = serverAuth
[req]
distinguished_name = req_distinguished_name
[req_distinguished_name]
EOF
# Sign the CSR with SANs
openssl x509 -req -days 365 -sha256 \
-in server.csr \
-CA ca.pem \
-CAkey ca-key.pem \
-CAcreateserial \
-out server-cert.pem \
-extfile server-extfile.cnf
# Create a config file for the client certificate
cat > client-extfile.cnf <<EOF
extendedKeyUsage = clientAuth
[req]
distinguished_name = req_distinguished_name
[req_distinguished_name]
EOF
# Generate client key
openssl genrsa -out client-key.pem 4096
# Generate client certificate request
openssl req -subj "/CN=client" -new -key client-key.pem -out client.csr
# Sign the client certificate with your CA
openssl x509 -req -days 365 -sha256 \
-in client.csr \
-CA ca.pem \
-CAkey ca-key.pem \
-CAcreateserial \
-out client-cert.pem \
-extfile client-extfile.cnf
Step 2: Edit your docker daemon
sudo nano /etc/docker/daemon.json
Add these lines:
"tls": true,
"tlscacert": "/etc/docker/certs/ca.pem",
"tlscert": "/etc/docker/certs/server-cert.pem",
"tlskey": "/etc/docker/certs/server-key.pem",
"tlsverify": true,
"hosts": ["unix:///var/run/docker.sock", "tcp://0.0.0.0:2376"]
The end result should look something like this:
{
"log-driver": "journald",
"tls": true,
"tlscacert": "/etc/docker/certs/ca.pem",
"tlscert": "/etc/docker/certs/server-cert.pem",
"tlskey": "/etc/docker/certs/server-key.pem",
"tlsverify": true,
"hosts": ["unix:///var/run/docker.sock", "tcp://0.0.0.0:2376"]
}
Step 3: Edit the docker service
sudo systemctl edit docker.service
Add these lines:
[Service]
ExecStart=
ExecStart=/usr/bin/dockerd
The end result should look like this:
### Anything between here and the comment below will become the new contents of the file
[Service]
ExecStart=
ExecStart=/usr/bin/dockerd
### Lines below this comment will be discarded
...
Step 4: Restart Docker
sudo systemctl daemon-reload
sudo systemctl restart docker
Step 5: Testing
copy your ca.pem, client-cert.pem, and client-key.pem to another computer that has docker, and use the following to test your connection:
docker --tlsverify \
--tlscacert=ca.pem \
--tlscert=client-cert.pem \
--tlskey=client-key.pem \
-H=tcp://[YOUR_IP]:2376 \
version
If you get an output with no error, then everything is working!
Longer Version (Explanation)
Step One: Generate Keys
To connect to your Docker server securely, we need to generate a set of keys. The first is a Certificate Authority private key. This is our master signing key. This is like the master key mold that the building's security company keeps in their vault. It's used to create and validate all other security keys in the building. This should be kept extremely secure and private.
openssl genrsa -aes256 -out ca-key.pem 4096
For our key, we're generating a RSA Key with AES-256 encryption.
genrsa
Specifies a RSA key-aes265
Using AES-256 bit encryption4096
A key length of 4096 bits. 4096 is on the stronger side-out ca-key.pem
The name of our output key file
Generating this key will require you to enter a passphrase for the key. Make sure your remember it!
Next, we'll generate our Certificate Authority (CA) Certificate. This is like the security company's official identification card that proves they are authorized to issue access cards. Everyone can see and verify this ID, but they can't use it to make new keys.
openssl req -new -x509 -days 365 -key ca-key.pem -sha256 -out ca.pem
This certificate uses our CA private key for authorization.
-new
Creates a new certificate request-x509
This tells OpenSSL to create a self-signed certificate instead of a certificate request-days 365
Sets the validity period of the certificate to 365 days-key ca-key.pem
Specifies which private key to use-sha256
Specifies the hashing algorithm to use (SHA-256, which is a secure hashing standard)-out ca.pem
The name of our output certificate file
Now we need a private key for our server. This is like the unique electronic signature inside your building's access card. It proves that you are who you claim to be and can't be copied or duplicated.
openssl genrsa -out server-key.pem 4096
This is very similar to the command that generates the CA key above, but without -aes256
. The reason we don't include it is because if the key were encrypted, Docker would have to input the passphrase every time it reads the key.
And finally, we need to generate our server certificate. This is the actual access card that's been issued to your building. When someone wants to enter, they can verify that this card was legitimately issued by the security company.
This requires some intermediate steps.
The first is to generate a server.csr
. This is like the application form your building submitted to the security company requesting an official access card. It contains information about who you are and what access you need.
openssl req -subj "/CN=[YOUR_IP]" \
-sha256 -new \
-key server-key.pem \
-out server.csr
[YOUR_IP]
would be replaced with your server ip address.
The next is to create a configuration file for our cert.
cat > server-extfile.cnf <<EOF
subjectAltName = IP:[YOUR_IP]
extendedKeyUsage = serverAuth
[req]
distinguished_name = req_distinguished_name
[req_distinguished_name]
EOF
subjectAltName = IP:[YOUR_IP]
This specifies alternative names for the certificate. In this case, it's saying the certificate is valid for a specific IP address.[YOUR_IP]
would again be replaced with your actual server IP address. This is needed for security because modern TLS implementations require the certificate to explicitly list the IP addresses or domain names it's valid for.extendedKeyUsage = serverAuth
This explicitly declares that this certificate is intended for server authentication only. This is a security best practice that stops the certificate from being misused for other purposes and helps with compatibility across different TLS implementations.[req]
This starts a section for certificate request settings[req_distinguished_name]
This section would normally contain settings for the certificate's distinguished name. In this case, we will use defaults.
Putting all this together, we'll generate our sever certificate:
openssl x509 -req -days 365 -sha256 \
-in server.csr \
-CA ca.pem \
-CAkey ca-key.pem \
-CAcreateserial \
-out server-cert.pem \
-extfile server-extfile.cnf
openssl x509
- Base command for X.509 certificate operations-req
- Indicates we're processing a certificate request (server.csr)-days 365
- Certificate will be valid for one year-sha256
- Use SHA-256 hashing algorithm-in server.csr
- Input file containing the Certificate Signing Request-CA ca.pem
- Specifies the CA certificate that will be used to sign-CAkey ca-key.pem
- The CA's private key used for signing-CAcreateserial
- Creates a serial number file (ca.srl) if it doesn't exist-out server-cert.pem
- Where to save the signed certificate-extfile server-extfile.cnf
- Use the extensions from our previously created config file
Step 2: Edit your docker daemon
sudo nano /etc/docker/daemon.json
Add these lines to daemon.json inside surrounding braces:
"tls": true,
"tlscacert": "/etc/docker/certs/ca.pem",
"tlscert": "/etc/docker/certs/server-cert.pem",
"tlskey": "/etc/docker/certs/server-key.pem",
"tlsverify": true,
"hosts": ["unix:///var/run/docker.sock", "tcp://0.0.0.0:2376"]
This configuration sets up secure TLS communication for our Docker daemon. Let's break down each setting:
-
"tls": true
- Enables TLS (Transport Layer Security) for Docker
- Like turning on the security system for your Docker server
-
"tlscacert": "/etc/docker/certs/ca.pem"
- Path to the CA certificate we created
- This lets Docker verify that clients are using certificates signed by your CA
- Like telling the system which security company ID to trust
-
"tlscert": "/etc/docker/certs/server-cert.pem"
- Path to the server's certificate
- This is the server's identity proof to clients
- Like the building's official access card
-
"tlskey": "/etc/docker/certs/server-key.pem"
- Path to the server's private key
- Used to prove the server's identity
- Like the electronic signature in the access card
-
"tlsverify": true
- Enables mutual TLS authentication
- Requires clients to present valid certificates
- Like requiring both the building AND visitors to show proper ID
-
"hosts": ["unix:///var/run/docker.sock", "tcp://0.0.0.0:2376"]
- Specifies how Docker can be accessed:
unix:///var/run/docker.sock
: Local access through Unix sockettcp://0.0.0.0:2376
: Remote access over network (port 2376 is the standard for TLS-enabled Docker)
- Like specifying that the building can be accessed both through the main entrance (TCP) and a staff-only side door (Unix socket)
- Specifies how Docker can be accessed:
Step 3: Edit the docker service
sudo systemctl edit docker.service
This opens a special override configuration for the Docker systemd service. Any settings here take precedence over the default configuration.
Add these lines:
[Service]
ExecStart=
ExecStart=/usr/bin/dockerd
The first empty ExecStart=
line clears any existing ExecStart commands. The second line provides the new basic start command for Docker. We need this because Docker often includes command-line arguments in its default ExecStart command. Since we moved our configuration to daemon.json in the previous step, we want to start Docker without any command-line arguments.
The end result should look like this:
### Anything between here and the comment below will become the new contents of the file
[Service]
ExecStart=
ExecStart=/usr/bin/dockerd
### Lines below this comment will be discarded
...
Step 4: Restart Docker
sudo systemctl daemon-reload
sudo systemctl restart docker
This simply reloads our Docker service configuration and restarts Docker.
Step 5: Testing
for the client side, we need to generate a certificate and key.
# Create a config file for the client certificate
cat > client-extfile.cnf <<EOF
extendedKeyUsage = clientAuth
[req]
distinguished_name = req_distinguished_name
[req_distinguished_name]
EOF
# Generate client key
openssl genrsa -out client-key.pem 4096
# Generate client certificate request
openssl req -subj "/CN=client" -new -key client-key.pem -out client.csr
# Sign the client certificate with your CA
openssl x509 -req -days 365 -sha256 \
-in client.csr \
-CA ca.pem \
-CAkey ca-key.pem \
-CAcreateserial \
-out client-cert.pem \
-extfile client-extfile.cnf
All of these commands are very similar to what we already covered above.
copy your ca.pem
, client-cert.pem
, and client-key.pem
to another computer that has docker, and use the following to test your connection:
docker --tlsverify \
--tlscacert=ca.pem \
--tlscert=client-cert.pem \
--tlskey=client-key.pem \
-H=tcp://[YOUR_IP]:2376 \
version
docker
- The base Docker CLI command--tlsverify
- Enables TLS verification (both client and server must prove their identity)--tlscacert=ca.pem
- Specifies which CA certificate to trust (same one we created earlier)--tlscert=client-cert.pem
- The client's certificate for proving its identity--tlskey=client-key.pem
- The client's private key-H=tcp://[YOUR_IP]:2376
- The address of your Docker server:[YOUR_IP]
should be replaced with your server's actual IP- Port 2376 is the standard port for TLS-enabled Docker
version
- A simple command that returns Docker version info (used here just for testing the connection).
If it fails:
- Double-check that your certificates were copied correctly
- Make sure the IP address is correct
- Check the server's firewall allows connections to port 2376
If successful:
- The command will return version information about your Docker server
- This confirms that:
- The connection is encrypted
- The server's identity was verified
- Your client's identity was verified
- Two-way communication works
- You are ready for remotely connecting to Docker!
Example output:
Client:
Version: 27.5.1
API version: 1.47
Go version: go1.22.11
Git commit: 9f9e405
Built: Wed Jan 22 13:40:02 2025
OS/Arch: linux/amd64
Context: default
Server: Docker Engine - Community
Engine:
Version: 27.5.1
API version: 1.47 (minimum version 1.24)
Go version: go1.22.11
Git commit: 4c9b3b0
Built: Wed Jan 22 13:41:17 2025
OS/Arch: linux/amd64
Experimental: false
containerd:
Version: 1.7.25
GitCommit: bcc810d6b9066471b0b6fa75f557a15a1cbf31bb
runc:
Version: 1.2.4
GitCommit: v1.2.4-0-g6c52b3f
docker-init:
Version: 0.19.0
GitCommit: de40ad0
Bonus Tip!
For easier command line use, you can create some environment variables in your shell profile (like .bashrc
or .zshrc
):
export DOCKER_TLS_VERIFY="1"
export DOCKER_CERT_PATH="/path/to/your/certs"
export DOCKER_HOST="tcp://[YOUR_IP]:2376"
After setting these, you can simply run Docker commands like:
docker version