Trust / Chapter 13

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 #

The authorisation code flow The application redirects the browser to the identity provider, which authenticates the user and redirects back with a short-lived code. The application then exchanges that code directly with the identity provider, using its client secret, for an ID token. The password never reaches the application and the token never travels through the browser. Your browser The application Identity provider 1. I would like to log in 2. go and ask them, client_id, redirect_uri 3. password, then second factor the password stops here 4. back to the app, with a short-lived code the code, via the browser 5. code + client_secret, server to server 6. a signed ID token saying who you are The code travels through the browser and is useless alone: exchanging it needs the client secret, which only the application server holds. That is why there are six steps and not four.
The browser carries a short-lived code. The client validates the returned identity token before creating its own session.
  1. Immich redirects the browser to Authentik with a client_id, exact redirect_uri, state, nonce and a Proof Key for Code Exchange (PKCE) challenge.
  2. Authentik authenticates the person and applies multi-factor policy.
  3. The browser returns to the registered URI with a short-lived authorisation code and the original state.
  4. 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.
  5. Authentik returns an ID token and usually an access token.
  6. 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_id identifies Immich and is not secret.
  • client_secret authenticates the confidential client at the token endpoint and is secret.
  • each redirect_uri is an exact allowlisted return address. Use strict entries, never * or an unreviewed regular expression.
Side readingOIDC in one pageEvery parameter in an OpenID Connect login, what it is for, and which one is causing your error.

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
~/homeserver/immich/compose.yaml additions
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:family member 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.

The browser completes Authentik login and returns to Immich, then starts the flow again. I want to inspect the external URLs, cookies, forwarded scheme and state handling without disabling validation.

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.

Authentication succeeds, then Authentik or Immich returns an error. I want to compare the exact strict redirect URI, issuer, client ID, client secret, PKCE and group binding.

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.

I enabled OAuth but intentionally left password login on. The local administrator path now fails. I want to use Immich's supported recovery or maintenance route and verify it before enabling Auto Launch.

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:auth is private to group:family and 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.

Settings

Your values

The book is written with placeholder names so it makes sense to everybody. Put your own in and every chapter, every command and every copy-paste prompt updates to match.

Nothing here is sent anywhere. It is saved in this browser, so it comes back next time. A different browser or a private window gets the placeholders again.

Live preview

$ ssh admin@192.168.1.20
$ sudo ufw allow from 192.168.1.0/24 to any port 22 proto tcp
$ sudo hostnamectl set-hostname homeserver
$ sudo timedatectl set-timezone Europe/Paris

Real commands from chapters 2, 3 and 4. They change as you type.

The account you log in as. Not root, and not necessarily the same name you use on your laptop.

Introduced in Chapter 2, Meet your server

The book's placeholder is admin

What the machine calls itself. You choose it, and it shows up in your shell prompt and your logs.

Introduced in Chapter 3, A safe front door

The book's placeholder is homeserver

The IP address your server has on your home network, from ip -brief addr.

Introduced in Chapter 2, Meet your server

The book's placeholder is 192.168.1.20

The address range and prefix shown by ip route or ip -brief addr, written in CIDR form. Copy the real prefix; do not guess /24.

Introduced in Chapter 2, Meet your server

The book's placeholder is 192.168.1.0/24

The address traffic goes to on its way out of your house, from ip route.

Introduced in Chapter 2, Meet your server

The book's placeholder is 192.168.1.1

In Region/City form, or Etc/UTC if you would rather read logs in UTC.

Introduced in Chapter 3, A safe front door

The book's placeholder is Europe/Paris

A registered name you control. Chapter 8 uses it for the LAN route; chapter 12 uses a separate private Tailscale name remotely.

Introduced in Chapter 8, One door, many rooms

The book's placeholder is example.com

The email identity allowed to administer the tagged server in your Tailscale policy.

Introduced in Chapter 10, Your own private network

The book's placeholder is you@example.com

The mailbox that should receive actionable home-server alerts.

Introduced in Chapter 14, Knowing it is alive

The book's placeholder is alerts@example.com

Once you save, the prose and the commands read with your names, the copy buttons copy your values, and the copy-paste prompts describe your machine accurately. That last one matters: an assistant told your network is 192.168.1.0/24 when it is not will send you chasing the wrong thing.

Anything you leave empty keeps the book's placeholder.