Authentication
Opt-in. Without AUTH_ENABLED, nothing changes: no login page, no session, no auth code loaded.

Auth off = no
Audit Log in the sidebar. There is no actor to record.
Which mode?
| You have | Use | Notes |
|---|
| No identity system | None (default) | Nothing to configure |
| A few named people | Local | Static list, no directory |
| Active Directory, OpenLDAP | LDAP / AD | Admin comes from a group |
| Entra ID, Keycloak, Okta, Authentik, Google | OIDC / SSO | Credentials never reach the UI |
Always add a recovery account. See
Locked out.
Use cases
Try it locally
One command. Ships OpenLDAP, a registry and the UI.
git clone https://github.com/VibhuviOiO/docker-registry-ui
cd docker-registry-ui/docker/ldap-auth
docker compose up -d --build
http://localhost:5006 —
alice/
alicepassword (admin),
bob/
bobpassword (viewer).
Stop:
docker compose down
Sign in with an identity provider
docker run -d --name registry-ui -p 5000:5000 \
-v "$PWD/data:/app/data" \
-e AUTH_ENABLED=true \
-e AUTH_MODE=oidc \
-e AUTH_SECRET_KEY="$(openssl rand -base64 32)" \
-e AUTH_PUBLIC_URL=https://registry.example.com \
-e OIDC_ISSUER=https://login.microsoftonline.com/<tenant>/v2.0 \
-e OIDC_CLIENT_ID=<client-id> \
-e OIDC_CLIENT_SECRET=<client-secret> \
-e OIDC_ADMIN_GROUP=registry-admins \
-e REGISTRIES='[{"name":"Mine","api":"https://registry.example.com"}]' \
vibhuvioio/docker-registry-ui:latest
Register this redirect URI with the provider:
<AUTH_PUBLIC_URL>/auth/oidc/callback
Only a few people may delete
-e LDAP_ADMIN_GROUP='registry-admins;platform-admins'
| Rule | |
|---|
| Separator | ; |
| Not | , — that is part of a DN |
| OIDC equivalent | OIDC_ADMIN_GROUP |
Locked out
Checked before any network call. Works while the provider is down.
-e BREAK_GLASS_USER=emergency -e BREAK_GLASS_PASSWORD='<long random value>'
Store it in a secret manager, not in a committed compose file.
Roles
| Role | Can |
|---|
viewer | Browse repositories, tags, analytics, scan results |
admin | Above, plus delete, bulk operations, registry config, audit log |
AUTH_DEFAULT_ROLE defaults to
viewer.
READ_ONLY=true overrides both.
Local login
docker run -d --name registry-ui -p 5000:5000 \
-v "$PWD/data:/app/data" \
-e AUTH_ENABLED=true \
-e AUTH_MODE=local \
-e AUTH_SECRET_KEY="$(openssl rand -base64 32)" \
-e APPLICATION_USERS='[{"username":"alice","password":"alicepassword","role":"admin","displayName":"Alice Example"}]' \
-e REGISTRIES='[{"name":"Mine","api":"https://registry.example.com"}]' \
vibhuvioio/docker-registry-ui:latest
LDAP / AD
The bind account needs read access only.
docker run -d --name registry-ui -p 5000:5000 \
-v "$PWD/data:/app/data" \
-e AUTH_ENABLED=true \
-e AUTH_MODE=ldap \
-e AUTH_SECRET_KEY="$(openssl rand -base64 32)" \
-e LDAP_URL=ldaps://ldap.example.com:636 \
-e LDAP_BASE_DN=dc=example,dc=com \
-e LDAP_BIND_DN='cn=registry-ui,ou=Services,dc=example,dc=com' \
-e LDAP_BIND_PASSWORD_FILE=/run/secrets/ldap_bind_password \
-e LDAP_ADMIN_GROUP=registry-admins \
-e REGISTRIES='[{"name":"Mine","api":"https://registry.example.com"}]' \
vibhuvioio/docker-registry-ui:latest
| Variable | Purpose |
|---|
LDAP_URL | ldap://host:389 or ldaps://host:636 |
LDAP_BASE_DN | Search root |
LDAP_BIND_DN / LDAP_BIND_PASSWORD | Service account. ..._PASSWORD_FILE for Docker secrets |
LDAP_USER_FILTER | Default matches uid. Use sAMAccountName for AD |
LDAP_ADMIN_GROUP | Members become admin |
LDAP_STARTTLS | true upgrades a plain connection |
LDAP_CA_CERT | CA bundle for a private CA |
LDAP_INSECURE | true skips verification. Testing only |
Working example:
docker/ldap-auth/docker-compose.yml
OIDC / SSO
| Variable | Purpose |
|---|
OIDC_ISSUER | Provider base URL. Discovery read from /.well-known/openid-configuration |
OIDC_CLIENT_ID / OIDC_CLIENT_SECRET | From the provider |
AUTH_PUBLIC_URL | Browser-facing URL. The callback is built from it |
OIDC_ADMIN_GROUP | Claim value that grants admin |
OIDC_USERNAME_CLAIM | Default preferred_username |
OIDC_GROUPS_CLAIM | Default groups |
OIDC_CA_CERT | CA bundle for an internal provider |
| Rule | |
|---|
| Flow | Authorization code + PKCE + nonce |
| Tokens | RS256, verified against the provider JWKS |
AUTH_PUBLIC_URL | The browser's address, not the container's |
| Admin group set but claim missing | Everyone becomes viewer. The log has a warning |
Audit log
Every sign-in, failure, sign-out, deletion and denied privileged action lands in audit.log inside DATA_DIR.
| Field | |
|---|
| Actor, role | Who |
| Action, result | What |
| Client IP, timestamp | Where and when |
Admins read it under
Audit Log. Viewers get
403.
Append-only, locked, rotates at 5 MB. Mount it to keep it:
-v "$PWD/data:/app/data"
Multiple workers
| Rule | Why |
|---|
Same AUTH_SECRET_KEY in every worker | Otherwise users are signed out at random |
One shared DATA_DIR | Audit log and configuration live there |
Verify
B=http://localhost:5000
curl -si $B/api/registries | head -1 # 401 without a session
curl -s -c jar.txt -o /dev/null -X POST $B/auth/login \
--data-urlencode username=alice --data-urlencode password=alicepassword
curl -s -b jar.txt $B/auth/me
All variables:
app/auth/config.py
Troubleshooting
| Symptom | Cause |
|---|
503 on sign-in | Directory unreachable. Check LDAP_URL and TLS |
401 for every user | Wrong bind DN/password, or the filter matches nothing |
Always viewer | No groups returned. Set LDAP_GROUP_FILTER, or check OIDC_GROUPS_CLAIM |
| Random sign-outs | AUTH_SECRET_KEY differs between workers |
| Config lost on restart | DATA_DIR not mounted |
| Empty password | Rejected before any bind. Some servers would treat it as anonymous |