Authentik on handtrixxx.com (auth.handtrixxx.com)
Last updated 05/11/2026 · authentik, handtrixxx-com, upgrade
Deployment (verified 2026-10-25)
- Compose project
/data/docker/auth.handtrixxx.com/:authentik-server,authentik-worker,authentik-db(postgres:16-alpine). - Image
ghcr.io/goauthentik/server:2026.8.5(upgraded from 2026.5.4 on 2026-10-25). - Fronted by Traefik only (the external network named
caddyis Traefik’s bridge,172.19.0.0/16); server port 9000. - Worker mounts
/var/run/docker.sock→ can provision container outposts. .envkeys:POSTGRES_DB/USER/PASSWORD,AUTHENTIK_SECRET_KEY,AUTHENTIK_EMAIL__*,AUTHENTIK_LOG_LEVEL,AUTHENTIK_WEB__BASE_URL,PUID/PGID/DOCKER_GID,AUTHENTIK_POSTGRESQL__HOST. NoAUTHENTIK_LISTEN__TRUSTED_PROXY_CIDRSset.- Gotcha: the
.envhad no trailing newline on its last line, so appending a key with>>glued it ontoAUTHENTIK_LOG_LEVEL=warning. Append with a leading newline or checktail -c 1 .envfirst. - DB dumps for backup in
backups/(pg_dump -U athuser authdb); latest as of 2026-10-25:authentik_backup_20261025.dump.
Upgrade rules (learned 2026-10-25)
- authentik’s lifecycle tooling refuses unsupported major-version skips; the docs require stepping through every
major.minorseries in order, latest patch of each first. Series in use: 2026.2 → 2026.5 → 2026.8. - Procedure that worked cleanly: back up the DB, bump both server+worker images to the newest patch of the current series (
docker compose up -d, migrations run automatically), then bump to the target series. Healthcheck isak healthcheck; containers report healthy once migrations finish. - Check
docs.goauthentik.io/docs/releases/<year>.<minor>for breaking changes before each hop.
2026.8 breaking changes that matter here
- Forwarded headers only from trusted proxies.
AUTHENTIK_LISTEN__TRUSTED_PROXY_CIDRSdefaults include172.16.0.0/12, and Traefik connects from172.19.0.7, so nothing to change as long as that env var stays unset. If it is ever set, it replaces the defaults - forgetting Traefik’s range makes authentik treat HTTPS as HTTP (mixed content, endless loading spinner, auth errors). hash_passwordmanagement command no longer takes a password argument (pipe via stdin:printf '%s' "$PW" | docker compose run --rm server hash_password).- WebAuthn stage
prevent_duplicate_devicesremoved (was already default-off in 2026.5.4). - Server entrypoint and the proxy outpost were rewritten in Rust; webtop/bookmark forward-auth keep working unchanged but are the thing to health-check after an upgrade.
Forward-auth proxy providers (Traefik pattern, verified 2026-10-25)
- Working apps (webtop, bookmark, ai, agent) use Traefik
forwardauthtohttp://authentik-server:9000/outpost.goauthentik.io/auth/traefikplus a router forPathPrefix(/outpost.goauthentik.io/)on the same host pointing athttp://authentik-server:9000. - Gotcha (cause of a stubborn 404): a ProxyProvider only works if it has an
**application**bound ANDauthorization_flow(default-provider-authorization-implicit-consent) +invalidation_flow(default-provider-invalidation-flow). Inserting the provider rows directly in postgres (authentik 2026.x:authentik_core_provider→authentik_providers_oauth2_oauth2provider→authentik_providers_proxy_proxyprovider, joined onprovider_ptr_id/oauth2provider_ptr_id) leaves those NULL, and the embedded outpost (Rust/usr/bin/authentik) then answers 404 for that host while other hosts 302. Fix: through the Django ORM (docker exec authentik-server python manage.py shell) set flows,p.set_oauth_defaults(), save, andApplication.objects.create(name=..., slug=..., provider=p); the outpost starts resolving immediately (no restart needed). - Second cause of the same 404 (found 2026-11-01): the provider must also be in the embedded outpost’s provider list. Provider 42 (pi-agent) had dropped out (list was [13,15,40]) so
agent.handtrixxx.comreturned the authentik “Not Found” page even with correct flows/app. Fix:Outpost.objects.get(name='authentik Embedded Outpost').providers.add(ProxyProvider.objects.get(pk=42))inmanage.py shell; takes effect in seconds. Check withOutpost.objects.all()->providers. - Check:
curl -H "Host: <host>" http://authentik-server:9000/outpost.goauthentik.io/startfrom a container on thecaddynetwork → 302 (provider found) vs 404 (not found). The authentik image has no curl; use pythonurllibwith a no-redirect handler insideauthentik-server. - Providers in use: Webtop(13), Linkding(15), LiteLLM(40, now only the forward-auth anchor for ai.handtrixxx.com / OmniRoute; its OIDC SSO is unused), pi-agent(42, app slug
pi-agent), OmniRoute (OAuth2, app slugomniroute, since 2026-10-31,sub= username; script/data/maintenance/omniroute-oidc/setup.py).
Native OIDC (OAuth2) apps - no outpost
For apps with their own OIDC support (SnapOtter, Gramps, Arcane), create a plain OAuth2Provider + Application instead of a proxy provider:
docker exec authentik-server python manage.py shell->OAuth2Provider.objects.create(name=..., client_id=..., client_secret=<random generated value>, authorization_flow=Flow.objects.get(slug="default-provider-authorization-implicit-consent"), _redirect_uris=[{"matching_mode": "strict", "url": "<callback>", "redirect_uri_type": "authorization"}])thenApplication.objects.create(name=..., slug=..., provider=p).- Gotcha: the authorization flow’s name is “Authorize Application” (two exist: implicit-consent and explicit-consent); query it by slug, not name.
- Scopes/groups: attach the managed mappings -
p.property_mappings.add(*ScopeMapping.objects.filter(managed__in=["goauthentik.io/providers/oauth2/scope-openid","...scope-email","...scope-profile"])). The stock profile mapping already exposes agroupsclaim (user’s authentik groups), so no extragroupsscope is needed. **grant_types**MUST be set (ArrayField, default empty):authorization_code,refresh_token,client_credentials,implicit,hybrid,password,urn:ietf:params:oauth:grant-type:device_code. An empty list makes every authorize request fail withInvalid grant_type for provider-> “The request is otherwise malformed” (cost me a login attempt on 2026-10-25). UI-created providers get these filled.**signing_key**MUST be set too: an ORM-created provider with no signing key signs id_tokens with HS256/client_secret and serves an empty JWKS ({}), so strict OIDC clients fail atjwk.Parse: failed to get "kty" hint. Fix:provider.signing_key = CertificateKeyPair.objects.filter(name="authentik Self-signed Certificate").first()(the shared key UI-created providers use). Arcane login verified working 2026-10-25 after both grant_types and signing_key were set; JWKS athttps://auth.handtrixxx.com/application/o/arcane/jwks/shows 1 RSA/RS256 key (kid 177f3b418a771bb79aea9f22be6feb35).- Issuer is per provider:
https://auth.handtrixxx.com/application/o/<slug>/(discovery advertises the trailing slash). Arcane accepted the slash-lessOIDC_ISSUER_URL(discovery + issuer match passed, verified 2026-10-25 viaPOST /api/oidc/url). - Apps with native OIDC have a config endpoint that reveals the built auth URL - for Arcane
POST https://docker.handtrixxx.com/api/oidc/urlwith{"redirectUri":...};GET /api/oidc/statusshowsenvConfigured/envForced. Useful for testing SSO without a browser. - Arcane (
docker.handtrixxx.com, slugarcane) added 2026-10-25 via/data/maintenance/arcane-oidc/setup.py(idempotent, writesOIDC_CLIENT_ID/OIDC_CLIENT_SECRETto/data/docker/docker.handtrixxx.com/.env(mode 600), never prints the value). Niklas is in groupauthentik Admins- useful group claim for admin role mappings.
Passkey (WebAuthn) login (configured 2026-10-25)
- Passkey works as an optional first factor on the normal SSO login page (
/if/flow/default-authentication-flow/); users without an enrolled passkey just use password. - Mechanism:
IdentificationStage.webauthn_stagepoints to anAuthenticatorValidateStagenameddefault-authentication-webauthn(device_classes=['webauthn'],not_configured_action=skip,webauthn_user_verification=preferred). The identification stage itself generates the challenge (no need to attach the validate stage to the flow). Per docs: Identification stage + WebAuthn-capable Authenticator Validation stage. - Enrollment: auth.handtrixxx.com -> user settings (profile) -> Authenticators tab -> WebAuthn (setup flow
default-authenticator-webauthn-setupexists). - The existing MFA stage
default-authentication-mfa-validation(static,totp,webauthn,duo,sms,email;not_configured_action=skip) runs after password as second factor; passkey-at-identification skips the password step entirely. - Caveat from docs: passkey login skips MFA validation - if a second factor is wanted even with passkey, adjust the policy on the MFA stage.
Follow-ups / requirements
- Base URL: done 2026-10-25 -
AUTHENTIK_WEB__BASE_URL=https://auth.handtrixxx.comin.env, referenced in compose.yml for both server and worker. Stored per-tenant in postgres (columnbase_urlonauthentik_tenants_tenant); verify withdocker exec authentik-db psql -U athuser -d authdb -c 'select base_url from authentik_tenants_tenant;'. Required from 2026.11. - OIDC endpoints in use by consumers:
/application/o/<app-slug>/,/application/o/authorize/,/application/o/token/,/application/o/userinfo/, discovery at/application/o/<slug>/.well-known/openid-configuration. Consumers: LiteLLM-UI (ai.handtrixxx.com, generic endpoints), snapotter (OIDC_ISSUER_URL=.../application/o/snapotter/). - Key-bound ID tokens and OAuth token exchange are opt-in per provider.
Source: Wiki.js