Setup a docker registry for passwordless Docker builds with GitHub/GitLab using authentik
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
This requires version 3 of the official docker registry, which is currently in Alpha. For this setup I used docker.io/library/registry:3.0.0-alpha.1
.
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_JWKS=/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