This post will describe how to setup a docker registry using distribution/distribution to allow for “passwordless” authentication. Now of course, this is not actually passwordless, there’s still a password. But we can (ab)use the fact that both GitLab CI and GitHub Actions give you a JWT signed by the platform, valid for the duration of the run.

Setup

Preparing authentik

Setting up federation - GitHub

Create an OpenID Connect Source and set the JWKS URL to https://token.actions.githubusercontent.com/.well-known/jwks. All the other fields can be set to random values

Setting up federation - GitLab

Create an OpenID Connect Source and set the Well-known URL to https://gitlab.com/.well-known/openid-configuration if using GitLab SaaS or https://gitlab.company/.well-known/openid-configuration if using a self-hosted instance. All the other fields can be set to random values

Setting up the provider

Create a scope mapping for the scope docker-registry with this expression

push_group = "acl_docker_push"

scopes = request.http_request.POST.get("scope", "").split()
access = []
for scope in scopes:
    if scope.count(":") < 2:
        continue
    type, name, actions = scope.split(":")
    if not ak_is_group_member(user, name=push_group):
        actions = "pull"
    access.append(
        {
            "type": type,
            "name": name,
            "actions": actions.split(","),
        }
    )
return {
    "access": access,
}

Create an OAuth2 Provider, make sure that a signing key is selected, select the mapping created above, and select the OpenID Source as JWKS Source. Take note of the Client ID.

Create an application and select the provider.

Also make sure to select a signing key, and download the certificate of the selected key.

Preparing the token service

As a “glue” between the Docker distribution itself and any OIDC Provider, we’ll use https://github.com/BeryJu/distribution-oauth.

This service will take the JWT generated by GitHub/GitLab, authenticate to the configured OIDC provider, and return the providers JWT as a docker token. This is required because the way the docker client requests a certificate is close to standard OAuth, but slightly different.

To configure this service, set these environment variables:

TOKEN_URL=https://id.company/application/o/token/ # Token endpoint of authentik
CLIENT_ID=693e60deada0b71e8ecb3d078e4ebaaf08624e55 # Client ID from above
SCOPE=docker-registry # Scope of the mapping from above
PASS_JWT_USERNAME=JWT # Special username that will allow the usage of a JWT as password

This service then needs to be publicly accessible, in this case I created a Kubernetes ingress for the registry that sends /token to this service.

Setup the registry itself

Now, while this PR is still open, you need a custom build of the registry. I built ghcr.io/beryju/registry:jwt for my own setup, so feel free to use that. The complete configuration of the registry will not be covered here, check the reference here.

The downloaded certificate from authentik also needs to be mounted into the container.

The main configuration options required for this setup are these environment variables:

REGISTRY_AUTH_TOKEN_REALM=https://registry.company/token # Full URL to the /token endpoint of the helper service
REGISTRY_AUTH_TOKEN_SERVICE=693e60deada0b71e8ecb3d078e4ebaaf08624e55 # Same client ID as above
REGISTRY_AUTH_TOKEN_ISSUER=https://id.company/application/o/docker-registry/ # Issuer of the JWT,
REGISTRY_AUTH_TOKEN_ROOTCERTBUNDLE=/srv/docker/cert/trusted.pem # Path to the mounted certificate

Before you can use it

Since all the users from this setup will be created automatically when they first authenticate, the first build will fail. This is because by default that user will not be a member in the correct group to allow docker pushes.

Using it - GitHub

Use this snippet in your GitHub actions workflow

permissions:
  contents: read
  id-token: write

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
    # [...] Checkout, etc
    - name: Get GitHub JWT
      id: jwt
      run: |
        JWT=$(curl -H "Authorization: bearer $ACTIONS_ID_TOKEN_REQUEST_TOKEN" "$ACTIONS_ID_TOKEN_REQUEST_URL" | jq -r .value)
        echo "::set-output name=token::${JWT}"        
    - name: Login to Container Registry
      uses: docker/login-action@v2
      with:
        registry: registry.company
        username: JWT
        password: ${{ steps.jwt.outputs.token }}

Using it - GitLab

build:
  stage: build
  image:
    name: gcr.io/kaniko-project/executor:debug
    entrypoint: [""]
  script:
    - mkdir -p /kaniko/.docker
    - echo "{\"auths\":{\"registry.company\":{\"auth\":\"$(printf "%s:%s" "JWT" "${CI_JOB_JWT_V2}" | base64 | tr -d '\n')\"}}}" > /kaniko/.docker/config.json
    - >-
      /kaniko/executor
      --context "${CI_PROJECT_DIR}"
      --dockerfile "${CI_PROJECT_DIR}/Dockerfile"
      --destination "registry.company/my-image:latest"      

References

See https://github.com/BeryJu/k8s/tree/main/clusters/beryjuorg-prd/registry, https://github.com/BeryJu/infrastructure/blob/master/tf/authentik/registry.tf and https://github.com/BeryJu/infrastructure/blob/master/tf/authentik/oidc-federation.tf