We were recently approached by a user and asked if Portainer supported self-hosted registries that implemented authentication through x509 client certs, and NOT with username/password credentials.
In order to give them an answer, we first wanted to replicate the setup and test it. We found that the process to setup this environment isnt well documented on the internet, so are publishing our steps here, just in case you want to do this too..
Most of this work is executed via the CLI on your host environments, so bear with us..
OK, you need at least 2 VMs for this to work.. one that will host the registry, and one that will act as your client.
Step 1, On the REGISTRY VM (in my case, its Ubuntu). Install Docker.
Follow the instructions here: https://docs.docker.com/engine/install/ubuntu/
- apt-get install ca-certificates curl gnupg lsb-release
- curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg
- echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
- apt-get update
- apt-get install docker-ce docker-ce-cli containerd.io
- systemctl enable docker
- docker info (just to check all started ok)
Step 2, create the self-signed SSL cert that will be used for the registry server instance (will actually be used by the nginx proxy). Note you need to have a FQDN for the registry, so make sure you register one accordingly, and then use in the line 4 below. For this blog, i will use registrydemo.portainer.io
- mkdir -p /opt/registry/
- cd /opt/registry/
- mkdir -p certs
- openssl req -nodes -newkey rsa:8192 -days 365 -x509 -keyout certs/server.key -out certs/server.cert -batch -addext "subjectAltName = DNS:<YOUR FQDN HERE>"
Step 3, create the client CA certificate (the one we will use to generate all client certs that will be used for authentication).
- cd /opt/registry/
- openssl req -nodes -newkey rsa:8192 -days 365 -x509 -keyout client-ca.key -out certs/client-ca.cert -batch -subj "/commonName=docker-registry-client-ca"
Step 4, generate a client cert using the CA cert above.
- openssl genrsa -out client.key 4096
- openssl req -new -key client.key -out client.csr -batch -subj "/commonName=docker-registry-client"
- openssl x509 -req -days 365 -in client.csr -CA /opt/registry/certs/client-ca.cert -CAkey /opt/registry/client-ca.key -set_serial 01 -out client.cert
- rm -f client.csr
Step 5, Configure and start the Docker Registry Container
- create a config file in /opt/registry/config.yml
- version: 0.1
http:
secret: randomsecretgoeshere
addr: 0.0.0.0:80
storage:
filesystem:
rootdirectory: /var/lib/registry
maxthreads: 100
delete:
enabled: true
- docker run -d --restart=always --name registry -v /opt/registry/config.yml:/etc/docker/registry/config.yml registry:2
Step 6, Configure and start the nginx proxy Container
- create a nginx config file in/opt/registry/nginx-registry.conf
- server {
listen 443 ssl;
keepalive_timeout 70;
client_max_body_size 0;
ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
ssl_ciphers ECDH+AESGCM:ECDH+AES256:ECDH+AES128:DH+3DES:!ADH:!AECDH:!MD5;
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 10m;
ssl_certificate /etc/nginx/ssl/server.cert;
ssl_certificate_key /etc/nginx/ssl/server.key;
ssl_client_certificate /etc/nginx/ssl/client-ca.cert;
ssl_verify_client on;
location / {
proxy_pass http://registry;
}
}
- docker run -d --restart=always --name nginx --publish 443:443 --link registry:registry -v /opt/registry/nginx-registry.conf:/etc/nginx/conf.d/default.conf -v /opt/registry/certs:/etc/nginx/ssl nginx
Step 7, make sure nginx and registry are working.
curl -v -k --cert ./client.cert --key ./client.key https://localhost:443/v2/
Step 8, now we need to generate the instructions to be used on the Docker Client machines..
- Change <YOURFQDNHERE> to your registry URL, then Run the following:
- HOST=registrydemo.portainer.io
SERVER_IP="$(wget -q -O- curlmyip.org)"
cat << __EOF__
set -xe
SERVER_CERT="$(cat /opt/registry/certs/server.cert)"
CLIENT_KEY="$(cat client.key)"
CLIENT_CERT="$(cat client.cert)"
mkdir -p /etc/docker/certs.d/$HOST:443
echo "\$SERVER_CERT" > /etc/docker/certs.d/$HOST:443/ca.crt
echo "\$CLIENT_CERT" > /etc/docker/certs.d/$HOST:443/client.cert
echo "\$CLIENT_KEY" > /etc/docker/certs.d/$HOST:443/client.key
echo "\$(cat /etc/hosts | grep -v '$HOST')" > /etc/hosts
echo "$SERVER_IP $HOST" >> /etc/hosts
__EOF__
- Copy the results to your clipboard and take a note of it.
Step 9, OK, so now SWITCH to your Docker Host VMs (repeat this on every docker host that you want to access this registry).
run the script output from before on the host.
Step 10, Switch to the /etc/docker/certs.d/ directory, and check you have a directory for the FQDN of your registry, and inside that directory are 3x certificate files.
Step 11, you should now be able to use the registry. Test it as follows:
- docker pull nginx
- docker tag nginx registrydemo.portainer.io:443/nginx:latest
- docker push registrydemo.portainer.io:443/nginx:latest
- docker rmi registrydemo.portainer.io:443/nginx:latest
- docker rmi nginx
- docker run -d registrydemo.portainer.io:443/nginx:latest
- docker image ls
You now have a private registry, that is secured with HTTPS, and authenticated with client certificates.
Step 12, In Portainer, you simply need to add the registry with no authentication, and Docker will take care of the rest.
For proof, try to open your registry FQDN from a browser, and see you cannot access it.
Done.
Hope this is of use to you.