Securing the remote Docker instance

Now that we've made our Docker instance accessible from a remote machine, we need to secure it.

Now that we've made our Docker instance accessible from a remote machine, we need to secure it.

The way that Docker does authentication is by using certificates. The user is allowed access to the Docker instance if the certificate supplied is issued by the specified certificate authority. The authorizing certificate authority is specified in the docker startup command.

In one of my previous posts I went over setting up remote access with plain and simple HTTP and no security. I also show how to make your Docker CLI automatically connect to the other server, we'll do that at the end of this one as well.

The way I secured the port on my setup is create a new certificate authority (CA) on my master node, docker1. From that CA I created the Docker server certificate and the client certificate to authenticate to the Docker server.

In the following steps, commands and examples, replace docker1.example.com with the fully qualified server name of your master server or DNS entry pointing to the correct system. There will be some questions asked during a few of the openssl commands, answer them with what makes sense for your environment.

Here's how to create a new set of certs for the certificate authority.

  1. Create your CA certificate directory structure:
    mkdir -p ~/ca/cacerts
    mkdir ~/ca/certs
    
  2. Go into your CA foldercd ~/ca
  3. Create the CA key: openssl genrsa -aes256 -out cacerts/ca-key.pem 4096
  4. Sign the CA key, creating the public key: openssl req -new -x509 -days 3650 -key cacerts/ca-key.pem -sha256 -out cacerts/ca.pem
  5. Make the CA private/public keys read only so you don't accidentally modify them. chmod 0400 cacerts/*.pem

Now that we have a CA certificate key pair, we need to create a certificate for the server. I could not find a way of specifying the password for the key when assigning the certificate to the Docker daemon, so when if it asks for a password, just press enter to assign no password.

  1. Generate the key: openssl genrsa -out certs/server-key.pem 4096
  2. Generate the certificate signing request (CSR): openssl req -subj "/CN=docker1.example.com" -sha256 -new -key certs/server-key.pem -out certs/server.csr
  3. Create the config file for signing the server certificate
    echo subjectAltName = DNS:docker1.example.com > extfile.cnf
    echo extendedKeyUsage = serverAuth >> extfile.cnf
    
    Note: If you want to access your docker host by other names, like just the hostname, or ip, add them to the subjectAltName. For example: echo subjectAltName = DNS:docker1.example.com,DNS:<host>,IP:10.0.0.11 > extfile.cnf
  4. Sign the server certificate: openssl x509 -req -days 3650 -sha256 -in certs/server.csr -CA cacerts/ca.pem -CAkey cacerts/ca-key.pem -CAcreateserial -out certs/server.pem -extfile extfile.cnf
  5. Remove the CSR and CNF files: rm certs/server.csr extfile.cnf

Now that we have the certificate, we need to configure Docker to use it.

Copy ca.pem, and move server.pem, server-key.pem to /etc/docker/certs and lock them down.

sudo mkdir /etc/docker/certs
sudo cp cacerts/ca.pem /etc/docker/certs
sudo mv certs/server.pem /etc/docker/certs
sudo mv certs/server-key.pem /etc/docker/certs
sudo chmod 0600 /etc/docker/certs/*
sudo chown root:root /etc/docker/certs/*

The required certificates are now ready for Docker to use to secure the port. Lets configure it to use them.

sudo systemctl edit docker.service

If you are following this series of posts, then this will probably have something like this:

[Service]
ExecStart=
ExecStart=/usr/bin/dockerd -H fd:// -H tcp://0.0.0.0:2375

Otherwise it may be empty.

Change the content to this:

[Service]
ExecStart=
ExecStart=/usr/bin/dockerd -H fd:// -H tcp://0.0.0.0:2376 --tlsverify --tlscacert=/etc/docker/certs/ca.pem --tlscert=/etc/docker/certs/server.pem --tlskey=/etc/docker/certs/server-key.pem

We are telling the service to listen on port 2376 using the specified certificate, require TLS and verify the client cert was issued by the specified certificate authority.

Now that we have Docker configured to use the certificate, reload the systemctl config, and restart the Docker service.

sudo systemctl daemon-reload
sudo systemctl restart docker.service

Now that Docker is configured to use a certificate and verify the TLS chain we need to build some client certificates. Since always typing the commands to create a client certificate becomes cumbersome and forgotten over time, lets create a script to do it. For this we'll name it buildcert.sh.

#!/bin/sh

openssl genrsa -out temp.key 4096
openssl req -subj "/CN=$1" -new -key temp.key -out temp.csr
echo extendedKeyUsage = clientAuth > ext.cnf
openssl x509 -req \
             -days 365 \
             -sha256 \
             -in temp.csr \
             -CA cacerts/ca.pem \
             -CAkey cacerts/ca-key.pem \
             -CAcreateserial \
             -out temp.crt \
             -extfile ext.cnf
rm temp.csr
rm ext.cnf

#save the public/private key pair
mv temp.key "certs/${1}-key.pem"
mv temp.crt "certs/${1}.pem"

echo CA Certificate
readlink cacerts/ca.pem
cat cacerts/ca.pem

echo
echo Client Private Key
readlink -f "certs/${1}-key.pem"
cat "certs/${1}-key.pem"

echo
echo Client Public Certificate
readlink -f "certs/${1}.pem"
cat "certs/${1}.pem"

Now, mark it as executable, chmod 0777 buildcert.sh

Now run it, passing in the name of the person this certificate is for, if you use spaces, be sure to put it in quotes, ./buildcert.sh "Test User". It should ask for the password of your CA private key, then dump out the ca public key, client private key and client public key.

Copy the ca public/client private and client public keys to the client desktop. Personally I just copy the correct content and past it into the correct files. Which is why I cat them out. You can also use scp or however else you want to get them. Which is what the readlink is in there for.

If you want the docker commands to automatically pick up the ca, private and public keys without any configs, the filenames are:

  • Certificate Authority Public Key: ~/.docker/ca.pem
  • Client Public key: ~/.docker/cert.pem
  • Client Private key: ~/.docker/key.pem

In theory, you should be able to run the following docker command on the client and connect to your remote Docker instance now.

docker -H tcp://docker1.example.com:2376 --tlsverify version

If it all works, you should get back something similar to:

Client: Docker Engine - Community
 Version:           18.09.0
 API version:       1.39
 Go version:        go1.10.4
 Git commit:        4d60db4
 Built:             Wed Nov  7 00:47:51 2018
 OS/Arch:           windows/amd64
 Experimental:      false

Server: Docker Engine - Community
 Engine:
  Version:          18.09.6
  API version:      1.39 (minimum version 1.12)
  Go version:       go1.10.8
  Git commit:       481bc77
  Built:            Sat May  4 01:59:36 2019
  OS/Arch:          linux/amd64
  Experimental:     false

Just like in my post about remoting into a remote docker instance if you don't want to always set the -H argument in the docker command, set the environment variable DOCKER_HOST to tcp://docker1.example.com:2376. If you do that, you may not want to specify the --tls-verify every time as well. Easy, set the environment variable DOCKER_TLS_VERIFY to 1.

In PowerShell:

$env:DOCKER_HOST="tcp://docker1.example.com:2376"
$env:DOCKER_TLS_VERIFY="1"

In Bash/Shell

export DOCKER_HOST="tcp://docker1.example.com:2376"
export DOCKER_TLS_VERIFY="1"