By default, the Docker API is exposed over a local UNIX socket. If you want to control Docker from a remote host, you can configure Docker to expose its API over a TCP socket instead. However, Docker itself doesn’t implement authentication. We will see here how we can use SSL certificate authentication to encrypt and authenticate the Docker API.
The plan
This is a very simple recipe, using socat in front of the Docker
API. socat will accept HTTPS connections, make sure that the client
shows an appropriate certificate, and relay the connection to the
UNIX socket. The client should either use socat as well to wrap
a normal connection into a SSL connection; or use OpenSSL (or
a similar crypto library) to do the wrapping directly.
A few words about certificates
I won’t do a full intro do public key crypto; but the basic idea is the following:
- the server (i.e. Docker) and each client connecting to it have to generate their own private key;
- they get a certificate authority to sign those keys, delivering them a certificate;
- when a client connect to the server, each party asks the other one to present its certificate, and is able to verify the validity of the certificate.
In other words: the client will know for sure that it’s talking to the server, and the server will know for sure that it’s talking to an authorized client.
In this example, we will cut corners. The client, server, and certificate authority will actually be the same entity. They will use the same key and certificate.
Get prepared
We need to install socat on both the client and server; and we
need openssl somewhere (doesn’t matter where exactly: it’s purely
for generation of the key material).
apt-get install socat openssl
socat is a very common tool, so it should be available for
your distro, even if it’s an exotic one.
Generate key and certificate
Here is my quick-and-dirty recipe to generate a RSA key (stored
in key.pem) and a self-signed certificate (stored in cert.pem),
valid for 100 years:
openssl genrsa -out key.pem 2048
openssl req -new -key key.pem -x509 -out cert.pem -days 36525 -subj /CN=WoopWoop/
Run that anywhere, then copy both key.pem and cert.pem
on client and server.
On server (running Docker)
Docker should run as usual. Then start socat like this:
socat \
OPENSSL-LISTEN:4321,fork,reuseaddr,cert=cert.pem,cafile=cert.pem,key=key.pem \
UNIX:/var/run/docker.sock
fork means that socat will fork a new child process for each incoming
connection (instead of handling only one connection and exiting right away).
reuseaddr is a useful socket option, so that if you exit and restart
socat, it won’t tell you that the address is already taken.
By default, OPENSSL connections made with socat require the other end
to show a valid certificate; unless you add verify=0. In that case,
we want to encrypt connections and check certificates (to deny unauthorized
clients), so the defaults are good.
On client (running e.g. Docker CLI)
The symmetrical invocation of socat looks like this:
socat \
UNIX-LISTEN:/tmp/docker.sock,fork \
OPENSSL:$SERVERADDR:4321,cert=cert.pem,cafile=cert.pem,key=key.pem
Now you can point your Docker CLI to the server through the tunnel, like this:
docker -H unix:///tmp/docker.sock run -t -i busybox sh
On client (using an HTTP client API)
If you want to connect to the Docker daemon with a regular HTTP client (which maybe cannot connect to a UNIX socket to do HTTP requests), try this version:
socat \
TCP-LISTEN:4321,bind=127.0.0.1,fork \
OPENSSL:$SERVERADDR:4321,cert=cert.pem,cafile=cert.pem,key=key.pem
The Docker API is then available on http://127.0.0.1:4321.
Enjoy!
What’s next?
It would obviously be much better to use a separate certificate authority, and generate different keys and certificates for the server and for each client. “This is left as an exercise for the reader,” as we say! :-)