Home » Homelab Docker Registry
homelab

Homelab Docker Registry

Photo by Ian Taylor on Unsplash

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!

Related posts

FreeNAS makes an excellent homelab NAS

dkowis

OpnSense as a HomeLab Firewall

dkowis

Docker-compose an OpenLDAP server

dkowis
The Rambling Homelabist