Manage Docker-Registry auth with Keycloak

This tutorial will show how to use Keycloak to authenticate a docker registry with Token Auth. I tested it on a tiny CentOS 7.3 Kimsufi server from OVH with 2GB RAM & 4Threads of 2GHz.

You can find all files used in this tutorial at the following github project

We need :

  • 2 subdomains (keycloak.yourdomain.ovh & registry.yourdomain.ovh)
  • A server with a routable IP on the internet

We will use :

  • Traefik.io as reverse proxy
  • Docker CE to run apps
  • Docker-Compose to manage all these containers
  • PostgreSQL as database for Keycloak
  • Keycloak To authenticate the registry
  • Let’s Encrypt as Certificate Authority
  • A Docker registry

The Following schema shows the infrastructure of what we will try to setup.

As Backend :

  • A Docker-Registry that will manage user authentication through Keycloak.
  • A Keycloak Server that manage users and authentications.
  • A PostgreSQL server that store persistent datas from Keycloak.

As Frontend :

  • A Traefik reverse proxy that uses Let’s encrypt to manage ssl connections when a user try to join our 2 backend servers

Install DockerCE & Docker-Compose

This command will install the latest version of DockerCE:

sudo curl -sSl https://get.docker.com |sh

This command will install the lastest version of Docker-Compose:

sudo curl -L https://github.com/docker/compose/releases/download/1.16.1/docker-compose-`uname -s`-`uname -m` -o /usr/local/bin/docker-compose &&\
sudo chmod +x /usr/local/bin/docker-compose

Prepare the Docker-Compose file

First you have to create a docker-compose.yml file.

touch docker-compose.yml 

Now we will add the version to use, the networks to create and open the services section.

Edit your docker-compose.yml file and add these lines

version: "2.3"
networks:
lb:
db:
services:

Create the Reverse-Proxy entry

In this section we will create everything needed to run the traefik.io reverse proxy

Add this entry to the services section of our docker-compose.yml file

  traefik:
image: "traefik:alpine"
command: "--debug \
--entryPoints='Name:http Address::80 Redirect.EntryPoint:https' --entryPoints='Name:https Address::443 TLS' \
--defaultentrypoints=http,https --docker \
--docker.watch=true --docker.exposedbydefault=false \
--web --acme --acme.acmelogging=true \
--acme.email=you@yourdomain.ovh \
--acme.onhostrule=true \
--acme.storage=/etc/acme/acme.json \
--acme.entrypoint=https"
restart: "unless-stopped"
ports:
- "80:80"
- "443:443"
- "8080:8080"
volumes:
- /var/run/docker.sock:/var/run/docker.sock:z
- /traefik/acme:/etc/acme:z
networks:
lb:

Why I chose to use Traefik.io as a reverse proxy :

  • It is very simple to integrate in a docker environment, it’s kind of made for that. It will natively read the docker-server events and API and expose my containers through the labels I pass in.
  • It is able to automatically manage SSL certificates with Let’s Encrypt (in my DNS entries I just have *.myTestDomain.ovh redirected to Traefik, and now all my services expositions and security are dynamic)

Now an explanation of all the new lines we added:

traefik: #is the name of the service
image: "traefik:alpine" #is the image from the dockerhub to deploy with alpine version because it makes lighter images.
command: "-- debug \ # run traefik in debug mode
-- entryPoints='Name:http Address::80 Redirect.EntryPoint:https'\ #
Make traefik listen port 80 and redirect it to https entrypoint
-- entryPoints='Name:https Address::443 TLS' \ #
Create and entrypoint named https that listen on 443 port
-- defaultentrypoints=http,https\
-- docker \
#Make traefik watch docker on the shared unix socket
-- docker.watch=true \
#Enable to watch docker changes
-- docker.exposedbydefault=false \
#disable exposing service by default
-- web\
#Expose a web dashboard at port 80
-- acme \
#Enable ACME use
-- acme.acmelogging=true \ #
display debug log messages from the acme client library
-- acme.email=you@yourdomain.ovh \#
Email address used for registration
-- acme.onhostrule=true \ #
Enable certificate generation on frontends Host rules
-- acme.storage=/etc/acme/acme.json \ #
File used for certificates storage
-- acme.entrypoint=https' #
Entrypoint to proxy acme challenge/apply certificates to
restart: "unless-stopped" #Container will automatically restart on stop when it crashes but not if stopped
ports: #All ports mapping
- "80:80" #ports 80 of container mapped on port 80 of host
- "443:443"
- "8080:8080"
volumes:
- /var/run/docker.sock:/var/run/docker.sock:z #
Share the docker socket of host with container with RW mode
- /traefik/acme:/etc/acme:z #
Share the acme directory from the host to the container with RW mode
networks: #
network management of container
lb:
#Connect container to the network lb

Finally we need to create the /traefik/acme/acme.json file

mkdir -p /traefik/acme && touch /traefik/acme/acme.json

Create the Database entry

In this section we will create everything needed to run a container of postgresql database to store data from the keycloak server

Add this entry to the services section of our docker-compose.yml file

  db:
image: "postgres:alpine"
environment:
POSTGRES_DATABASE: keycloak
POSTGRES_USER: keycloak
POSTGRES_ROOT_PASSWORD: PgRootPasswd
POSTGRES_PASSWORD: PgKcPasswd
volumes:
- ./data/postgres:/var/lib/postgresql/data:z
networks:
- db
restart: "unless-stopped"
healthcheck:
test: 'PGPASSWORD="PgKcPasswd" psql --host 127.0.0.1 --username keycloak --dbname keycloak -c "select 1" ; [ "0" -eq "$$?" ]; echo $$?'
interval: 30s
timeout: 10s
retries: 3

Now an explanation of all the new lines we added:

db: #service name
image: "postgres:alpine" #is the image from the dockerhub to deploy with alpine version because it makes lighter images.
environment: #Environment variable specification
POSTGRES_DATABASE: keycloak #Database name to create on first boot
POSTGRES_USER: keycloak #User that owns the database
POSTGRES_ROOT_PASSWORD: PgRootPasswd #Password for database root user
POSTGRES_PASSWORD: PgKcPasswd #Password for the $POSTGRES_USER
volumes:
- ./data/postgres:/var/lib/postgresql/data:z #
Share the directory from the host to the container with RW mode to store database's data
networks:
- db
#Connect the db container to db network
restart: "unless-stopped" #
Container will automatically restart on stop when it crashes but not if stopped
healthcheck: #
Healthchecks section let us specify a command supposed to verify if the container is alive
test: 'PGPASSWORD="PgKcPasswd" psql --host 127.0.0.1 --username keycloak --dbname keycloak -c "select 1" ; [ "0" -eq "$$?" ]; echo $$?'
#Command that checks if the postresql server is started and respond
interval: 30s #Interval between 2 checks
timeout: 10s #
If the command executes in more than 10 sec the test is supposed to have failed
retries: 3 #
If the check fails 3times in a row, the container state is not healthy anymore

Of course you should change passwords, and if you do so, don’t forget to update the healthcheck command with the new password.

Finally we need to create the /data/postgres directory

mkdir -p /data/postgres

Create the Keycloak entry

In this section we will create everything needed to run the keycloak server container.

Add this entry to the services section of our docker-compose.yml file

keycloak:
image: "jboss/keycloak:latest"
restart: "unless-stopped"
depends_on:
db:
condition: service_healthy
command: ["-b", "0.0.0.0","-Dkeycloak.profile.feature.docker=enabled"]
environment:
KEYCLOAK_USER: yourAdminUser
KEYCLOAK_PASSWORD: yourAdminUserPasswd
KEYCLOAK_LOGLEVEL: DEBUG
POSTGRES_USER: keycloak
PROXY_ADDRESS_FORWARDING: 'true'
POSTGRES_PASSWORD: PgKcPasswd
POSTGRES_PORT_5432_TCP_ADDR: db
labels:
traefik.frontend.rule: 'Host:keycloak.yourdomain.ovh'
traefik.port: '8080'
traefik.enable: 'true'
traefik.docker.network: 'keycloakregistrytuto_lb'
networks:
lb:
db:

Now an explanation of all the new lines we added:

keycloak:#service name
image: "jboss/keycloak:latest"
restart: "unless-stopped"
depends_on:#
section managing dependencies that must be fulfilled before starting the container
db: #
add a dependency on db container
condition: service_healthy #
Require that the db container's healthchecks are passing before starting keycloak
command: ["-b", "0.0.0.0","-Dkeycloak.profile.feature.docker=enabled"] #
arg added to the start command to enable docker-registry auth in keycloak
environment:
KEYCLOAK_USER: yourAdminUser #
Admin user of the main realm
KEYCLOAK_PASSWORD: yourAdminUserPasswd
#Admin password of main realm
KEYCLOAK_LOGLEVEL: DEBUG
#Enable debug logs
POSTGRES_USER: keycloak #
Postgres user to use to connect on db
PROXY_ADDRESS_FORWARDING: 'true' #
Must be set to true because the application is behind a reverse proxy and will need to use http headers
POSTGRES_PASSWORD: PgKcPasswd #
Password to authenticate on postgresql server
POSTGRES_PORT_5432_TCP_ADDR: db #
name to join the database server
labels: #
Labels are used to specify some extra informations on containers
traefik.frontend.rule: 'Host:keycloak.yourdomain.ovh'#
Specify to traefik what domain name is used to join this container
traefik.port: '8080' #
Specify on what port the server is listening to forward http
traefik.enable: 'true' #
Tells to traefik that it should expose this container
traefik.docker.network: 'keycloakregistrytuto_lb' #
Tells traefik on what network it can join this container since it's connected to two networks
networks:
lb: #
connect this container to lb network
db: #
connect this container to db network

Once more you should change passwords, because even if it’s for test purpose, this container is exposed on the web.

Run the docker-compose file and set up keycloak

In this section we will run the 3 containers (traefik, postgres and keycloak), then login to keycloak and set up a realm and client to let people authenticate to the docker-registry.

One simple command should run all that

docker-compose -p keycloak_registry_tuto up -d traefik keycloak db

Check logs to see when the app is started :

docker-compose -p keycloak_registry_tuto logs -f keycloak

When you see this log line, it means your keycloak server is running

WFLYSRV0025: Keycloak 3.3.0.CR2 (WildFly Core 3.0.1.Final) started in 93968ms — Started 537 of 859 services (570 services are lazy, passive or on-demand)

Open https://keycloak.yourdomain.ovh in a browser

Log in Keycloak

  • Click on Administration Console

Authenticate with the user/password you specified in keycloak’s environment variables in the docker-compose.yml file


Create a new Realm for Docker-Registry management

Click Add realm Button. We need to add one, because Master realm is supposed to be used for administration only


  • Name it docker-registry
  • Click on “Create”

  • Click on Realm Settings
  • Verifify Enabled is toggled to “on”

Add a new user in the realm

  • Click on “Users”
  • Click on “Add user”

  • Name it MyDockerRegistryUser
  • Toggle Email-Verified to off since we didn’t add a mail account
  • Click on “Save”

  • Type twice a new password
  • Toggle temporary to “off” to avoid changing password on first login
  • Click on “Reset Password”

Create a New authentication Client for the Docker-Registry

  • Click on “Clients”
  • Click on “Create

  • Set “docker-registry-test” as new Client ID
  • Chose docker-v2 as protocol
  • Click on “save”

Now we have a User and a Client ready for authenticating Docker-Registry’s access.

Create the Docker-Registry entry

In this section we will create everything needed to run the docker-registry and manage connect it to our Keycloak server.

We need to retrieve some information from the keycloak server to make authentication possible.

Find the environment variable

  • Click on “Clients”
  • Click on “docker-registry-test”

  • Click on “ Installation”
  • Select “Variable Override”

Keep this tab opened, you’ll need these info soon

Find your Realm’s public certificate

  • Click on Realm Settings
  • Click on Keys
  • Click on Certificate

You should now see your realm’s public certificate -> Copy it.
We will write it in the file : certs/registry_trust_chain.pem
You have to add the as first and last lines “BEGIN CERTIFICATE” and “END CERTIFICATE”, if you don’t, the Docker-Registry will fail to read the certificate.


cat certs/registry_trust_chain.pem
-----BEGIN CERTIFICATE-----
MIICrTCCAZUCBgFfSbAqOjANBgkqhkiG9w0BAQsFADAaMRgwFgYDVQQDDA9kb2NrZXItcmVnaXN0cnkwHhcNMTcxMDIzMTQ0MTQyWhcNMjcxMDIzMTQ0MzIyWjAaMRgwFgYDVQQDDA9kb2NrZXItcmVnaXN0cnkwggEiMA0GCSqGSIb3DQEBAQUAB4IBDwAwggEKAoIBAQCq49HH6WBuXQ4aBNtFcEgh/sgtLbE+/CeyuzqoBWcusZPq5o9QQ7czWjXm1jurn0BeJhGJTA2e8l0ZlhhS5mexIypdUySyY8YX+YQOX/722dNUv5azQBYza9heiNMRv/k1boNS9sy/V9gG4wxud3Rud1uVCOCjEC9+2NKXrR/bNag2Lx7u1whFM1HE4Fgtw0KK6VRnlR8zK4QR+ULUSsjzG1kCtYTQ3JZ8xQrnEMjJ6xdtg0LDfjX1M8ZfPR4ZjIiBrD43vufpfM9uZSW+hi8hnqSYPMHMn5RnCW5ZMxDe5799d00Ojj1bc2S2/3t4pVk47pgr8ljVtlRUG3NIhOp1AgMBAAEwDQYJKoZIhvcNAQELBQADggEBAFLPgn/BaxfYpRUIj8wq1lF62lMaPn+RUOZxolqQrX8It4RvmkvAUsGPlxWGS6W/ehtOEZHDtMiQEcp5NiTHZEkybwuaK5tAL5Nq0RKqItkc9nobHg5jmCeL917GLzgofhnoG7tq35daIo4dVmAR72CM9cRzPkhewvvUSlmln2oNXTllErgxCFWyViyghxIkWOAb6epjv98LHqJEEWHPuyFvkS6e2GPlULs0daG2W/Jn00NolxAlJW2Q86yN9CxQsEEzDPEoU94dJkR3rlD7yHVU5Y0LVb3DZmeb8el9nApLicDefpP4c/ux4WWwHOdzVPc1742xMuNNLyikS1fL730=
-----END CERTIFICATE-----

Add this entry to the services section of our docker-compose.yml file
Don’t forget to change the environment variables with yours from keycloak’s tab you let opened before.

registry:
image: "registry:2"
restart: "unless-stopped"
volumes:
- ./certs:/opt/certs:z
-./data/registry:/data:z
environment:
REGISTRY_STORAGE_FILESYSTEM_ROOTDIRECTORY: /data
REGISTRY_AUTH_TOKEN_ROOTCERTBUNDLE: /opt/certs/registry_trust_chain.pem
REGISTRY_AUTH_TOKEN_REALM: https://keycloak.yourdomain.ovh/auth/realms/docker-registry/protocol/docker-v2/auth
REGISTRY_AUTH_TOKEN_SERVICE: docker-registry-test
REGISTRY_AUTH_TOKEN_ISSUER: https://keycloak.yourdomain.ovh/auth/realms/docker-registry
TZ: Europe/Paris
labels:
traefik.backend: docker
traefik.frontend.rule: Host:docker.yourdomain.ovh
traefik.port: '5000'
traefik.enable: 'true'
networks:
- lb

Now an explanation of some of the new lines we added:

registry:
image: "registry:2"
restart: "unless-stopped"
volumes:
- ./certs:/opt/certs:z #
volume containing certificates
-./data/registry:/data:z #
Data volume
environment:
REGISTRY_STORAGE_FILESYSTEM_ROOTDIRECTORY: /data
#Path to the directory where it should store data
REGISTRY_AUTH_TOKEN_ROOTCERTBUNDLE: /opt/certs/registry_trust_chain.pem
#Path to the Realm's public certificate
REGISTRY_AUTH_TOKEN_REALM:
https://keycloak.yourdomain.ovh/auth/realms/docker-registry/protocol/docker-v2/auth
REGISTRY_AUTH_TOKEN_SERVICE: docker-registry-test
REGISTRY_AUTH_TOKEN_ISSUER:
https://keycloak.yourdomain.ovh/auth/realms/docker-registry
labels: #
No need to specify network to contact since it's connected to only one
traefik.backend: docker
traefik.frontend.rule: Host:docker.yourdomain.ovh
traefik.port: '5000' #
Listening on port 5000 by default
traefik.enable: 'true'
networks:
- lb

Create a directory to use as a data volume

mkdir ./data/registry

Run the Docker-Registry and log in

In this section we will run the previously made docker-registry template, and log in the registry with our user from docker-registry’s Realms from Keycloak.

Run it

docker-compose -p keycloak_registry_tuto up -d registry

Log in

docker login docker.yourdomain.ovh
Username: MyDockerRegistryUser
Password:
Login Succeeded

And you’re done!