Simple Docker HTTP socket certificates
Tags: docker, security
Published: 2020-04-09 21:00:29

I have a couple of Docker servers and I wanted to be able to manage them remotely. Reading about it, adding `-H tcp://0.0.0.0:2375` argument when starting docker daemon was simple enough solution but it also came with a warning that it is not secure.

In addition, there is a [Protect the Docker daemon socket](https://docs.docker.com/engine/security/https/) guide which covers how to expose http socket in a secure manner but it is also marked as an "Advanced topic" so I decided to make a simple shell script out of it.

Tested on a clean Debian 10 after Docker installation. Full `docker-certs.sh` is at the bottom of the post.

## Usage

```
docker-cert.sh [options]
OPTIONS:
  --all           Same as --ca --server --client --service combined, default behaviour
  --ca            Creates only self-signed Root CA
  --server        Creates a server certificate, requires CA to be created first
  --client        Creates a client certificate with a default name, requires CA to be created first
  --client=Name   Creates a client certificate with provided name, requires CA to be created first
  --service       Updates /lib/systemd/system/docker.service with socket listener and tls verification

Note: all files will be created in /etc/docker/certs dir
```

Running it without any options like `sudo docker-cert.sh` will execute default behaviour, same as with the `--all` option. Script will do the following:

1. Check if  `/etc/docker/certs ` directory exists, create if not
2. Create a self-signed root CA with a common name `Docker ($FQDN) CA`. FQDN is `hostname` plus the first suffix from `/etc/resolv.conf` search line, or just hostname if search line is not found.
3. Create a server certificate with a common name  `Docker ($FQDN) Server`, signed by the root CA
4. Create a client certificate  with a common name  `Docker ($FQDN) Client`, signed by the root CA
5. Update `/lib/systemd/system/docker.service` to append socket listener and certificates to the start command. Like the following:

```
-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 
```

6. Reloading systemctl daemon, `systemctl daemon-reload`
7. Restart docker service, `systemctl restart docker`

There are checks in place so if a file that is to be created already exist, script will exit. In the same way if `docker.service` contains `-H tcp://` in the `ExecStart` line, script will exit.

### Additional clients

Additional clients can be created afterwards by running `sudo docker-cert.sh --client=Another` which will create `client_another.key` and `client_another.pem` files, and common name will be `Docker ($FQDN) Client Another`



## Accessing remotely from Windows

Once certificates are created and docker daemon is running, it is time to test connectivity from a client windows machine. Tutorial suggests

```bash
docker --tlsverify --tlscacert=ca.pem --tlscert=cert.pem --tlskey=key.pem \
  -H=$HOST:2376
```

or setting environment variables like DOCKER_HOST or DOCKER_CERT_PATH for a directory where certificates are stored.

First approach looks cumbersome to type every time, and second approach doesn't work for multiple servers with different certificates.

### Batch file

I opted to create a small batch file to interact with docker.

Let's say that my docker server is named `dock1`, domain suffix is `local`. I've created certificates using the script so I have `Docker (dock1.local) Server` certificate.

After that I copied `ca.pem`, `client.key`, `client.pem` from server to my windows machine into the `%USERPROFILE%\.docker\dock1` folder.

Created a batch file named `dock1.bat` with the following content:

```powershell
@echo off
set DATADIR=%USERPROFILE%\.docker\%~n0
if exist "%DATADIR%\host.var" (set /p DOCKERHOST=<"%DATADIR%\host.var") else (set DOCKERHOST=%~n0)
docker --tlsverify --tlscacert="%DATADIR%\ca.pem" --tlscert="%DATADIR%\client.pem" --tlskey="%DATADIR%\client.key" -H=%DOCKERHOST%:2376 %*
```

and put that batch file somewhere in the PATH.

(broken into multiple lines for readability):

```powershell
set DATADIR=%USERPROFILE%\.docker\%~n0

if exist "%DATADIR%\host.var" (
  set /p DOCKERHOST=<"%DATADIR%\host.var"
) else (
  set DOCKERHOST=%~n0
)

docker
--tlsverify
--tlscacert="%DATADIR%\ca.pem"
--tlscert="%DATADIR%\client.pem"
--tlskey="%DATADIR%\client.key"
-H=%DOCKERHOST%:2376
%*
```

So when I run `dock1 info` from the command line that translates into  the following command (broken into multiple lines for readability):

```powershell
docker
--tlsverify
--tlscacert="C:\Users\user\ca.pem"
--tlscert="C:\Users\user\client.pem"
--tlskey="C:\Users\user\client.key"
-H=dock1:2376
info
```

and running it displays docker system-wide information.

It works for all docker commands, like`dock1 images`, `dock1 ps`, `dock1 run ....` etc.



### More fun with batch file

Ok, so the batch file above has some additional lines and variables. Let me explain them:

`%~n0` - this is the name of the file without extension. So if have file named `dock1.bat` it will have `dock1` value. If I name it `dock365.bat` it will have `dock365` value.

`set DATADIR=%USERPROFILE%\.docker\%~n0` - I'm using it to set the path where certificates are stored to a folder named the same as the batch file within my user profile > .docker folder.

`if exist "%DATADIR%\host.var" (set /p DOCKERHOST=<"%DATADIR%\host.var") else (set DOCKERHOST=%~n0)` - if hostname of docker server is different from the batch file name, this allows me to put real hostname into the file which will be read into the variable.
So if `host.var` file does not exist, `%DOCKERHOST%` variable will contain name of the batch file without extension (`dock1` for example). But if I want to access docker using `dock1.local` hostname, I can put `dock1.local` into the `host.var` file and then the `%DOCKERHOST%` variable will contain  `dock1.local` value.

`set /p DOCKERHOST=<"%DATADIR%\host.var"` - set variable to a value being read from the input file.

`%*` - passing all arguments that batch file received.



What this batch script allows me is to have one txt file, let's name it `docker_batch.txt`, instead of the batch file.  And then a symbolic link for each docker server I have. Created like this:

```
mklink dock1.bat docker_batch.txt
mklink dock365.bat docker_batch.txt
```

Pretty cool. 



## docker-certs.sh

{% gist https://gist.github.com/milolav/3f70461370e0c84944f24c3d704007c1 %}