Trust Chapter 13 120 min
One login for supported applications
Run a current identity provider, protect it with the private mesh, connect Immich, and preserve recovery access.
By the end of this chapter you should be able to
- Follow an OpenID Connect authorisation-code flow and name the validations
- Run the current three-service Authentik core without Docker socket access
- Connect Immich with strict redirect URIs and central multi-factor authentication
- Extend backup and restore coverage before login depends on Authentik
Each application otherwise creates another account, password policy and second-factor setup for every family member. An identity provider centralises those ordinary application identities. SSH, Tailscale enrolment and local emergency accounts remain separate.
Separate authentication from authorisation #
Authentication proves who you are. Authorisation decides what that identity may use.
Authentik authenticates the person. A group binding can authorise access to Immich, while Immich still controls application roles such as administrator. When login fails, identify which layer rejected it.
Follow the OpenID Connect flow #
- Immich redirects the browser to Authentik with a
client_id, exactredirect_uri, state, nonce and a Proof Key for Code Exchange (PKCE) challenge. - Authentik authenticates the person and applies multi-factor policy.
- The browser returns to the registered URI with a short-lived authorisation code and the original state.
- The client exchanges the code at Authentik's token endpoint. A confidential web client authenticates there; PKCE binds the exchange to the request and protects public clients that cannot keep a secret.
- Authentik returns an ID token and usually an access token.
- The client validates the ID-token signature, issuer, audience/client ID, expiry and nonce before starting its own session.
An intercepted code is not categorically useless because of a client secret: mobile and other public clients have no durable secret. Current OAuth security guidance uses authorisation code plus PKCE for both public clients and confidential clients.
The three values you copy during setup have different properties:
client_ididentifies Immich and is not secret.client_secretauthenticates the confidential client at the token endpoint and is secret.- each
redirect_uriis an exact allowlisted return address. Use strict entries, never*or an unreviewed regular expression.
Run the current Authentik core #
Authentik removed Redis in release 2025.10. The current core has three services: PostgreSQL, server and worker. Start from the current official Compose file, select a release, inspect its release notes, pin the image digest, then adapt it as below.
Add these values to secrets/authentik.env and extend chapter 9's renderer:
AUTHENTIK_SECRET_KEY=<long-random-signing-and-session-key>
AUTHENTIK_POSTGRES_PASSWORD=<long-random-alphanumeric-password-under-100-characters>
render secrets/authentik.env '["AUTHENTIK_SECRET_KEY"]' authentik_secret_key
render secrets/authentik.env '["AUTHENTIK_POSTGRES_PASSWORD"]' authentik_db_password
services:
authentik-db:
image: docker.io/library/postgres:16-alpine@sha256:<pin it>
restart: unless-stopped
environment:
POSTGRES_USER: authentik
POSTGRES_DB: authentik
POSTGRES_PASSWORD_FILE: /run/secrets/authentik_db_password
secrets: [authentik_db_password]
volumes: [authentik-db:/var/lib/postgresql/data]
networks: [auth-backend]
healthcheck:
test: [CMD-SHELL, "pg_isready -d authentik -U authentik"]
interval: 30s
timeout: 5s
retries: 5
mem_limit: 1g
authentik-server:
image: ghcr.io/goauthentik/server:<release>@sha256:<pin it>
command: server
restart: unless-stopped
environment:
AUTHENTIK_SECRET_KEY: file:///run/secrets/authentik_secret_key
AUTHENTIK_POSTGRESQL__HOST: authentik-db
AUTHENTIK_POSTGRESQL__NAME: authentik
AUTHENTIK_POSTGRESQL__USER: authentik
AUTHENTIK_POSTGRESQL__PASSWORD: file:///run/secrets/authentik_db_password
AUTHENTIK_OUTPOSTS__DISCOVER: "false"
secrets: [authentik_secret_key, authentik_db_password]
depends_on:
authentik-db:
condition: service_healthy
ports: ["127.0.0.1:9000:9000"]
volumes: [/srv/homeserver/data/authentik:/data]
networks: [auth-backend, auth-egress]
healthcheck:
test: [CMD, ak, healthcheck]
interval: 30s
timeout: 10s
retries: 5
start_period: 10m
mem_limit: 1500m
security_opt: [no-new-privileges:true]
authentik-worker:
image: ghcr.io/goauthentik/server:<release>@sha256:<same pin>
command: worker
restart: unless-stopped
environment:
AUTHENTIK_SECRET_KEY: file:///run/secrets/authentik_secret_key
AUTHENTIK_POSTGRESQL__HOST: authentik-db
AUTHENTIK_POSTGRESQL__NAME: authentik
AUTHENTIK_POSTGRESQL__USER: authentik
AUTHENTIK_POSTGRESQL__PASSWORD: file:///run/secrets/authentik_db_password
AUTHENTIK_OUTPOSTS__DISCOVER: "false"
secrets: [authentik_secret_key, authentik_db_password]
depends_on:
authentik-db:
condition: service_healthy
volumes: [/srv/homeserver/data/authentik:/data]
networks: [auth-backend, auth-egress]
mem_limit: 1g
security_opt: [no-new-privileges:true]
networks:
auth-backend:
internal: true
auth-egress: {}
volumes:
authentik-db:
secrets:
authentik_secret_key:
file: /etc/homeserver/secrets/authentik_secret_key
authentik_db_password:
file: /etc/homeserver/secrets/authentik_db_password
Current Authentik stores file-backed application data under /data; the shared bind mount makes it persistent and places it inside chapter 11's selected /srv/homeserver/data tree. Create it and merge these blocks into the canonical laptop copy of the existing services, networks, volumes and secrets keys; YAML cannot contain duplicate top-level keys. Commit the non-secret configuration and deploy it with chapter 5's rsync, then start only the new services.
$ install -d -m 0750 /srv/homeserver/data/authentik
$ sudo /usr/local/sbin/render-secrets
$ sudo docker compose config --quiet
$ sudo docker compose up -d authentik-db authentik-server authentik-worker
$ sudo docker compose ps
$ sudo docker compose logs --tail=100 authentik-server authentik-worker
The server and worker join an ordinary egress network so the loopback publication works and supported outbound integrations can connect. PostgreSQL remains only on the internal backend. The worker has no Docker socket, and automatic outpost discovery is off. Authentik outposts require container-management permissions including create and remove; this book does not use them. Add an outpost only as a separate host-control decision.
Give Authentik its own private service name #
Define a Tailscale Service named svc:auth with endpoint tcp:443 in the admin console. Grant the family access:
{
"src": ["group:family"],
"dst": ["svc:auth"],
"ip": ["tcp:443"]
}
Host that service from the loopback Authentik port:
$ sudo tailscale serve --service=svc:auth --https=443 127.0.0.1:9000
$ sudo tailscale serve status
Approve the service host if the admin console requests it. Use the exact private HTTPS URL shown for svc:auth; the service has a distinct MagicDNS name and TailVIP from the Immich Serve endpoint. Confirm that an unenrolled device cannot reach it.
Open <auth-service-url>/if/flow/initial-setup/ with the trailing slash, create the administrator, and enrol two recovery methods before connecting an application.
Extend backup and restore coverage now #
The /data bind mount is already selected by chapter 11, but the named database volume is live state and must not be copied while PostgreSQL runs. Add a second atomic dump to /usr/local/sbin/homeserver-backup immediately after the Immich dump, reusing the script's dump_tmp variable:
dump_tmp=$(mktemp "$dump_dir/.authentik.dump.XXXXXX")
docker compose -f "$compose" exec -T authentik-db \
pg_dump -U authentik -Fc authentik >"$dump_tmp"
test -s "$dump_tmp"
docker compose -f "$compose" exec -T authentik-db \
pg_restore --list <"$dump_tmp" >/dev/null
mv -f "$dump_tmp" "$dump_dir/authentik.dump"
dump_tmp=
Run homeserver-backup.service, restore authentik.dump through stdin into a scratch authentik_restore_test database, list tables and compare one meaningful row count. Drop the scratch database. Central login is not complete until this second restore passes.
Connect Immich through its supported settings #
Use the current Authentik Immich integration and Immich OAuth documentation rather than a generic JSON file.
In Authentik, create an OAuth2/OpenID Connect application/provider pair named Immich. Select authorisation code, a signing key and these Strict redirect URIs, replacing the placeholders with the Serve URL printed in chapter 12:
app.immich:///oauth-callback
<immich-serve-url>/auth/login
<immich-serve-url>/user-settings
In Immich, open Administration → Settings → OAuth Authentication and set:
- Enabled: on
- Issuer URL:
<auth-service-url>/application/o/immich/ - Client ID and Client Secret: the values from Authentik
- Scope:
openid email profile - Signing Algorithm:
RS256 - Auto Register: on only if every
group:familymember should receive an account - Auto Launch: off until local recovery has been tested
Immich stores this client secret in its database through the administration UI; it is not a Compose file secret. Protecting and restoring the Immich database is therefore the supported recovery control. Do not claim that chapter 9's renderer writes a value that the application cannot read from a file.
Test web login and the mobile app.immich:///oauth-callback path. Fetch the issuer's .well-known/openid-configuration document and confirm its issuer exactly matches the configured URL.
Authorise with groups and enforce MFA #
Bind the Immich provider to an Authentik family group. Removing membership blocks new authorisations centrally; existing Immich sessions can remain valid until they expire or are revoked in Immich.
Require a phishing-resistant passkey or security key where possible and keep a second enrolled method. Authenticator-app codes improve on a password but can be phished. SMS depends on the mobile operator and is the weakest fallback.
Store recovery codes off the server. Keep Immich password login enabled and retain a strong local administrator password in the household password manager. Test /auth/login?autoLaunch=0 before considering automatic OIDC launch.
Login keeps redirecting between Immich and Authentik
Find why Immich does not keep the authenticated session
Paste this into a new agent session. It carries everything the agent needs to know about where you are, and asks it to walk you through the problem rather than fix it for you.
Authentik returns an error after login
Find which authorisation response check failed
Paste this into a new agent session. It carries everything the agent needs to know about where you are, and asks it to walk you through the problem rather than fix it for you.
The local Immich administrator login no longer works
Recover local access before relying on OIDC
Paste this into a new agent session. It carries everything the agent needs to know about where you are, and asks it to walk you through the problem rather than fix it for you.
Keep local identity where OIDC is unsupported #
Some applications and television clients cannot complete a browser or device-code flow. Use a maintained integration only when you can evaluate its release history and permissions; otherwise retain a strong local password. “One login” applies to supported ordinary application use, not SSH, tailnet enrolment or recovery.
Done when
- PostgreSQL, Authentik server and worker are healthy; no Redis service exists
- Neither Authentik service mounts the Docker socket
-
svc:authis private togroup:familyand not public on the internet - The Authentik database dump and scratch restore pass
- Immich has all three strict redirect URIs and logs in on web and mobile
- The issuer document, client ID, RS256 and scopes match
- The Immich client secret is protected by its tested database backup
- Group removal and existing-session behaviour are documented separately
- MFA and off-server recovery codes are configured
- A local Immich administrator login still works
What you picked up
- Authentication proves identity; authorisation grants application access.
- Validate signature, issuer, audience, expiry and nonce; use authorisation code plus PKCE.
- Current Authentik has PostgreSQL, server and worker, with no Redis requirement.
- Tailscale Services give Authentik a distinct private HTTPS name without a public tunnel.
- Adding state means extending and testing backup before declaring the chapter complete.
- Central revocation blocks new authorisations; application sessions need their own revocation.
- Local recovery accounts remain necessary.