Deploy a docker registry, with portus, and clair

Setting up a secure doker registry, behind apache with portus and clair.

I recently wrote an article on how to deploy a secure registry on a kubernetes cluster, at this time I knew PortUs but I thought it was not enough ready for production.

Last week we need to setup a docker registry for a client, so we thoughts we need to give another chance to PortUs, and it was a succes.

There is also a big difference with the previous needs, now we are on a single server, and we will deploy the registry using docker compose.

I have found a lot of help in the examples from the PortUs documentation : https://github.com/SUSE/Portus/tree/master/examples/compose

As we need to use Apache Httpd Server with that client, and that the httpd server is running outside docker, si I needed to adapt the examples.

I also use an SSL certificate from Let’s Encrypt, and a public URL.

I made this setup on a fresh Debian install.

So I assume that you have installed Docker and Docker Compose.

Prepare Docker deployment

I started from the portus docker compose example in the PortUs, and I made some little changes.

So first create a working directory :

mkdir -p ./my-registry/registry ./my-registry/clair ./my-registry/secrets
mkdir -p /var/lib/portus/registry /var/lib/portus/mariadb

Then create a .env file for Docker Compose :

MACHINE_FQDN=registry.me.local
DATABASE_PASSWORD=MXmE4mkesNAtYS6P
PORTUS_PASSWORD=vqgBP7Zuf7r86k3F
SECRET_KEY_BASE=3SL3ZbfLTHBqnJ2Eamd7ygE
  • MACHINE_FQDN is the Full Qualified Domain Name of your registry, mean you will do docker login registry.me.local on the clients.
  • DATABASE_PASSWORD the password for the Portus MySQL Database
  • PORTUS_PASSWORD the password of the special portus user
  • SECRET_KEY_BASE the secret key for Portus, to create Session JSON Web TOkens

Then create certificates for communications between your docker registry and your portus server.

openssl req -x509 -sha256 -nodes -days 365 -newkey rsa:2048 -keyout portus.key -out portus.crt

Then from the portus docker compose example I copied the following files :

wget https://raw.githubusercontent.com/SUSE/Portus/master/examples/compose/clair/clair.yml -o clair/clair.yml
wget https://raw.githubusercontent.com/SUSE/Portus/master/examples/compose/clair/clair.yml -o clair/clair.yml
wget https://raw.githubusercontent.com/SUSE/Portus/master/examples/compose/registry/init -o registry/init

Now here is the adapted docker-compose.yaml file

version: "2"

services:
  portus:
    image: opensuse/portus:2.3.5
    environment:
      - PORTUS_MACHINE_FQDN_VALUE=${MACHINE_FQDN}
      - PORTUS_SECURITY_CLAIR_SERVER=http://clair:6060

      # DB. The password for the database should definitely not be here. You are
      # probably better off with Docker Swarm secrets.
      - PORTUS_DB_HOST=db
      - PORTUS_DB_DATABASE=portus_production
      - PORTUS_DB_PASSWORD=${DATABASE_PASSWORD}
      - PORTUS_DB_POOL=5

      # Secrets. It can possibly be handled better with Swarm's secrets.
      - PORTUS_SECRET_KEY_BASE=${SECRET_KEY_BASE}
      - PORTUS_KEY_PATH=/certificates/portus.key
      - PORTUS_PASSWORD=${PORTUS_PASSWORD}

      # SSL
      - PORTUS_CHECK_SSL_USAGE_ENABLED='false'

      # Since we have no nginx in insecure mode, portus have to
      # serve the static files
      - RAILS_SERVE_STATIC_FILES='true'
    ports:
      - 3000:3000
    depends_on:
      - db
    links:
      - db
    volumes:
      - ./secrets:/certificates:ro

  background:
    image: opensuse/portus:2.3.5
    depends_on:
      - portus
      - db
    environment:
      # Theoretically not needed, but cconfig's been buggy on this...
      - CCONFIG_PREFIX=PORTUS
      - PORTUS_MACHINE_FQDN_VALUE=${MACHINE_FQDN}
      - PORTUS_SECURITY_CLAIR_SERVER=http://clair:6060

      # DB. The password for the database should definitely not be here. You are
      # probably better off with Docker Swarm secrets.
      - PORTUS_DB_HOST=db
      - PORTUS_DB_DATABASE=portus_production
      - PORTUS_DB_PASSWORD=${DATABASE_PASSWORD}
      - PORTUS_DB_POOL=5

      # Secrets. It can possibly be handled better with Swarm's secrets.
      - PORTUS_SECRET_KEY_BASE=${SECRET_KEY_BASE}
      - PORTUS_KEY_PATH=/certificates/portus.key
      - PORTUS_PASSWORD=${PORTUS_PASSWORD}

      - PORTUS_BACKGROUND=true
    links:
      - db
    volumes:
      - ./secrets:/certificates:ro

  db:
    image: library/mariadb:10.0.23
    command: mysqld --character-set-server=utf8 --collation-server=utf8_unicode_ci --init-connect='SET NAMES UTF8;' --innodb-flush-log-at-trx-commit=0
    environment:
      - MYSQL_DATABASE=portus_production

      # Again, the password shouldn't be handled like this.
      - MYSQL_ROOT_PASSWORD=${DATABASE_PASSWORD}
    volumes:
      - /var/lib/portus/mariadb:/var/lib/mysql

  registry:
    image: library/registry:2.6
    environment:
      # Authentication
      REGISTRY_AUTH_TOKEN_REALM: https://${MACHINE_FQDN}/v2/token
      REGISTRY_AUTH_TOKEN_SERVICE: ${MACHINE_FQDN}
      REGISTRY_AUTH_TOKEN_ISSUER: ${MACHINE_FQDN}
      REGISTRY_AUTH_TOKEN_ROOTCERTBUNDLE: /secrets/portus.crt

      # Portus endpoint
      REGISTRY_NOTIFICATIONS_ENDPOINTS: >
        - name: portus
          url: https://${MACHINE_FQDN}/v2/webhooks/events
          timeout: 2000ms
          threshold: 5
          backoff: 1s
    volumes:
      - /var/lib/portus/registry:/var/lib/registry
      - ./secrets:/secrets:ro
      - ./registry/config.yml:/etc/docker/registry/config.yml:ro
      - ./registry/init:/etc/docker/registry/init:ro
    ports:
      - 5000:5000
      - 5001:5001 # required to access debug service
    links:
      - portus:portus

  clair:
    image: quay.io/coreos/clair:v2.0.6
    restart: unless-stopped
    depends_on:
      - postgres
    links:
      - postgres
    ports:
      - "6060-6061:6060-6061"
    volumes:
      - /tmp:/tmp
      - ./clair/clair.yml:/clair.yml
    command: [-config, /clair.yml]

  postgres:
    image: library/postgres:10-alpine
    environment:
      POSTGRES_PASSWORD: portus

So now you should be able to start your containers :

docker-compose up

Apache VirtualHost

So now we need to configure apache and create SSL certificates.

For creating SSL certificates and HTTPS VirtualHost I’m used to create the HTTP VirtualHost and create certificates with Certbot so it will also create the HTTPS VirtualHost.

Here is the http vhost :

<VirtualHost *:80>
  ServerName registry.me.local

  # This is for letsencrypt
  DocumentRoot /var/www/html
  ProxyPassMatch ^/.well-known !

  ProxyPreserveHost on
  ProxyRequests     off

  # Default IHM is portus
  ProxyPass / http://localhost:3000/
  ProxyPassReverse / http://localhost:3000/

  # Token for Auth
  ProxyPass /v2/token http://localhost:3000/v2/token
  ProxyPassReverse /v2/token http://localhost:3000/v2/token

  ProxyPass /v2/webhooks/events http://localhost:3000/v2/webhooks/events
  ProxyPassReverse /v2/webhooks/events http://localhost:3000/v2/webhooks/events

  # The main registry
  ProxyPass /v2 http://localhost:5000/v2
  ProxyPassReverse /v2 http://localhost:5000/v2

  RewriteEngine on
  RewriteCond %{SERVER_NAME} =registry.btb.ovh
  RewriteRule ^ https://%{SERVER_NAME}%{REQUEST_URI} [END,QSA,R=permanent]
</VirtualHost>

Then create certificates :

certbot --authenticator webroot --installer apache

And answer all questions :

Which names would you like to activate HTTPS for?
-------------------------------------------------------------------------------
1: registry.me.local
-------------------------------------------------------------------------------
Select the appropriate numbers separated by commas and/or spaces, or leave input
blank to select all options shown (Enter 'c' to cancel):1
Obtaining a new certificate
Performing the following challenges:
http-01 challenge for registry.me.local

Select the webroot for registry.me.local:
-------------------------------------------------------------------------------
1: Enter a new webroot
-------------------------------------------------------------------------------
Press 1 [enter] to confirm the selection (press 'c' to cancel): 1
Input the webroot for registry.me.local: (Enter 'c' to cancel):/var/www/html
Waiting for verification...
Cleaning up challenges
Generating key (2048 bits): /etc/letsencrypt/keys/0002_key-certbot.pem
Creating CSR: /etc/letsencrypt/csr/0002_csr-certbot.pem
Created an SSL vhost at /etc/apache2/sites-available/01.registry.me.local-le-ssl.conf
Deploying Certificate to VirtualHost /etc/apache2/sites-available/01.registry.me.local-le-ssl.conf
Enabling available site: /etc/apache2/sites-available/01.registry.me.local-le-ssl.conf


Please choose whether HTTPS access is required or optional.
-------------------------------------------------------------------------------
1: Easy - Allow both HTTP and HTTPS access to these sites
2: Secure - Make all requests redirect to secure HTTPS access
-------------------------------------------------------------------------------
Select the appropriate number [1-2] then [enter] (press 'c' to cancel): 2
Redirecting vhost in /etc/apache2/sites-available/01.registry.me.local.conf to ssl vhost in /etc/apache2/sites-available/01.registry.me.local-le-ssl.conf

-------------------------------------------------------------------------------
Congratulations! You have successfully enabled https://registry.me.local

You should test your configuration at:
https://www.ssllabs.com/ssltest/analyze.html?d=test1.sso.btb.ovh
-------------------------------------------------------------------------------

IMPORTANT NOTES:
 - Congratulations! Your certificate and chain have been saved at
   /etc/letsencrypt/live/registry.me.local/fullchain.pem. Your cert
   will expire on 2018-12-19. To obtain a new or tweaked version of
   this certificate in the future, simply run certbot again with the
   "certonly" option. To non-interactively renew *all* of your
   certificates, run "certbot renew"
 - If you like Certbot, please consider supporting our work by:

   Donating to ISRG / Let's Encrypt:   https://letsencrypt.org/donate
   Donating to EFF:                    https://eff.org/donate-le

Then you will have to edit the HTTPS VirtualHost :

<IfModule mod_ssl.c>
<VirtualHost *:443>
  ServerName registry.me.local


 
  # This is for enebling letsencrypt
  DocumentRoot /var/www/html
  ProxyPassMatch ^/.well-known !

  # This is needed by the docker registry
  Header set Host "registry.me.local"
  RequestHeader set X-Forwarded-Proto "https"

  ProxyPreserveHost on
  ProxyRequests     off

  ProxyPass /v2/token http://localhost:3000/v2/token
  ProxyPassReverse /v2/token http://localhost:3000/v2/token


  ProxyPass /v2/webhooks/events http://localhost:3000/v2/webhooks/events
  ProxyPassReverse /v2/webhooks/events http://localhost:3000/v2/webhooks/events

  ProxyPass /v2 http://localhost:5000/v2
  ProxyPassReverse /v2 http://localhost:5000/v2

  # Defualt ihm is portus
  ProxyPass / http://localhost:3000/
  ProxyPassReverse / http://localhost:3000/

  SSLCertificateFile /etc/letsencrypt/live/registry.me.local/fullchain.pem
  SSLCertificateKeyFile /etc/letsencrypt/live/registry.me.local/privkey.pem
  Include /etc/letsencrypt/options-ssl-apache.conf
</VirtualHost>
</IfModule>

Remember to restart Apache :

systemctl restart apache2

Then everything should be up and running

Test it

You need to go to https://registry.me.local, then see the PortUs screen to create the admin user.

Create it, then configure the registry :

  • Name of your choice
  • Registry : registry.me.local
  • Use SSL : Checked

Then on a docker client :

docker login registry.me.local

Login with the admin.

Then try push :

docker tag busybox:latest registry.me.local/busybox:latest
docker push registry.me.local/busybox:latest
The push refers to repository [registry.me.local/busybox]
f9d9e4e6e2f0: Pushed
latest: digest: sha256:5e8e0509e829bb8f990249135a36e81a3ecbe94294e7a185cc14616e5fad96bd size: 527

So it works ! Enjoy It.