A robust docker registry can be more difficult than anticipated to set up. Especially if you want vulnerability scanning. The registry provided by Docker is perfectly acceptable, but does not provide authentication or authorization. Here, I explain how I set up my homelab docker registry using portus and integrated it with my ldap servers.
First, the good stuff, available in entirety on my gitlab. A slightly abbreviated version of the docker-compose.yml follows:
version: '3' services: portus: image: opensuse/portus:head restart: unless-stopped dns: - 10.10.220.231 #environment omitted for brevity, see the repo ports: - 3000:3000 networks: - registry_net volumes: - ./secrets:/certificates:ro - static:/srv/Portus/public background: image: opensuse/portus:head restart: unless-stopped depends_on: - portus dns: - 10.10.220.231 #environment omitted for brevity, see the repo networks: - registry_net volumes: - ./secrets:/certificates:ro registry: image: library/registry:2.6 command: ["/bin/sh", "/etc/docker/registry/init"] restart: unless-stopped dns: - 10.10.220.231 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/bundle.crt # SSL REGISTRY_HTTP_TLS_CERTIFICATE: /secrets/registry.dark.kow.is.crt REGISTRY_HTTP_TLS_KEY: /secrets/registry.dark.kow.is.key # 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 networks: - registry_net nginx: image: library/nginx:alpine restart: unless-stopped dns: - 10.10.220.231 volumes: - ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro - ./secrets:/secrets:ro - static:/srv/Portus/public:ro ports: - 80:80 - 443:443 networks: - registry_net clair: image: quay.io/coreos/clair:v2.0.8 restart: unless-stopped dns: - 10.10.220.231 networks: - registry_net ports: - "6060-6061:6060-6061" volumes: - /tmp:/tmp - ./clair/clair.yml:/clair.yml command: [-config, /clair.yml] volumes: static: driver: local networks: registry_net:
There is a lot going on here, lets get an explanation of what’s going on. Portus is configured entirely via environment variables, and there’s a lot of them, and doubled since you have to run two of them. See my gitlab repo for the full context.
Five services are deployed with this docker-compose.
- portus itself
- portus’ background job runner
- the docker registry
- nginx as a reverse proxy
- clair as a security scanning engine
- A database is also required, but I used my database server, not in a container
Portus and the Background
portus is basically a docker hub that you can run on your own. It is only capable of interacting with a single registry, but it provides authentication and authorization. An extremely useful feature is the “app tokens” that it provides. Docker credentials files contain base64 encoded username/password combinations, not secure at all. If you’re going to integrate your registry with LDAP for single account purposes, you really don’t want to expose that password. The app token solves this problem by being easy to revoke, and easy to implement. The UI could use a bit of help though. The app token displays in a small toast for only a few seconds, and cannot be recovered again!
The background container is also portus, but configured to be in “background mode.” It keeps track of the registry events sent by the registry itself, and polls the registry API ensuring that portus and the registry contents are in sync. In my configuration, it will also fetch vulnerabilities from the security scanner.
Getting LDAP working with this was remarkably easy. Using the same servers I set up in a previous post, I added an application user, and set up the access control for it. It only needs to read from my users group. The first user that logs in via LDAP is also stored as the admin user. My current line for access control follows:
{1}to dn.subtree="ou=users,dc=dark,dc=kow,dc=is" by dn="cn=gitlab,ou=Applications,dc=dark,dc=kow,dc=is" read by dn="cn=sssd,ou=Applications,dc=dark,dc=kow,dc=is" read by dn="cn=portus,ou=Applications,dc=dark,dc=kow,dc=is" read by * none
To control who has access to portus, I created a Access organizational unit, which contains a groupOfUniqueNames
. This, in concert with the memberOf
overlay, provides an easy filter to ensure that only certain users have access to portus.
The Docker Registry
This is just docker.com’s registry. It’s a solid registry that is free to use, but comes with no authentication or authorization. Portus supplements the registry with those things.
Nginx
The nginx here is configured as a reverse proxy to portus and to the registry itself. It makes a single SSL configured endpoint to access both the registry and the portus UI. A nice complete package.
Clair
Clair is a vulnerability static analysis for containers tool. It will poll the vulnerability metadata from a configured list of sources, store it, and then exposes this metadata via an API that portus can talk to. Additionally, updates to that vulnerability metadata trigger an event sent to alert portus that something is now vulnerable.
I haven’t seen any vulnerabilities yet on my portus UI, but it should notify me if something actually happens.
I recommend getting the configuration directly from clair’s site, and using that, as the original example I followed was already out of date when I used it. It goes into ./clair/clair.yml configured appropriately.
Current status
Setting up certificates was a bit tricky. Remember to modify the init
script inside registry/
to ensure that your root certificate bundle is available to the registry container. Without this, the webhooks feeding data from the registry back into portus will not function, and you’ll see a lot of noise in the logs. The portus background job will sync, but it’s much nicer to have the webhooks.
I went through all this work because I wasn’t able to grab containers from my gitlab without logging into the gitlab container registry. My usual workflow is to build a container, and then use it in my build. Without a place to stage that container, I either have to build it every time (slow!) or log the gitlab-runner into my registry itself. That feels like a bad idea, even though I’m running my own gitlab.
I haven’t been able to expose this to the public internet behind traefik. I only have it running on the inside of my homelab network. It works great for my self-hosted gitlab, but I would like to expose a registry.light.kow.is that is available to pull containers from. I don’t think I can simply hook it up to traefik like that. Perhaps I need to distill the nginx configuration into a traefik routing configuration so that traefik will do all the communication and SSL termination for me. The problem: the registry wants SSL to be enabled on itself, making it hard to do reverse proxying. If I had more IP addresses, I could just forward a port directly to it.
If there are any questions, drop em in a comment!