Skip to content
SECURITY

Authelia + Traefik: SSO for self-hosted services without losing a weekend

The deploy plan that gets you SSO in front of Jellyfin, Immich, Nextcloud, and the arr stack in under four hours. Real config files. One coffee. No frustration.

published
author
read
4 min (~809 words)
Detailed view of a steel keyhole on a door ensuring security and privacy.
0%

Self-hosted SSO has a reputation for being a weekend ruiner. It is not. It is a four-hour job the first time, and a three-line add per new service after that. This essay is the deploy plan I wish I'd had in front of me on the Saturday morning I started. Part of the rig.

The goal, in one sentence

One browser login — a username, a password, a TOTP code — grants me access to Jellyfin, Immich, Nextcloud, Sonarr/Radarr, Home Assistant, the *arr stack, and anything else I put behind Traefik. No per-service credentials. No "which password was Jellyfin again."

The pieces

  • Traefik 3 — reverse proxy, reads labels off containers, handles TLS via Let's Encrypt or internal Step-CA, supports ForwardAuth middleware for delegating auth to an external service.
  • Authelia 4.38 — the external auth service. Handles the login page, sessions, 2FA, password file / LDAP backend. Lightweight Go binary.
  • Redis — session storage for Authelia. Single small container.

That's the whole architecture. No Keycloak, no Authentik, no self-hosted Identity Provider empire. Authelia is the minimum viable thing that does the job.

The deploy plan — four hours, one Saturday

Hour 1 — Traefik

If you already run Traefik, skip this. If you don't: stand up a Traefik container with Docker socket mounted, a labels-based config, and your chosen TLS strategy. Mine uses Step-CA for internal certs and Let's Encrypt only for the two services I expose publicly.

services:
  traefik:
    image: traefik:v3.2
    restart: unless-stopped
    ports: ["80:80", "443:443"]
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock:ro
      - ./traefik.yml:/etc/traefik/traefik.yml:ro
      - ./acme:/acme
    labels:
      - traefik.enable=true
      - traefik.http.routers.api.rule=Host(\`traefik.home\`)
      - traefik.http.routers.api.service=api@internal

Verify: hit https://traefik.home, see the dashboard, TLS green in the browser. If that works, you have a reverse proxy. Move on.

Hour 2 — Authelia

Authelia has three config pieces: the main YAML, the users database (a file or LDAP), and an access control policy. Starting config:

# configuration.yml
server:
  address: "tcp://:9091"

authentication_backend:
  file:
    path: /config/users_database.yml

access_control:
  default_policy: deny
  rules:
    - domain: "jellyfin.home"
      policy: one_factor
    - domain: "immich.home"
      policy: one_factor
    - domain: "sonarr.home"
      policy: two_factor
    - domain: "traefik.home"
      policy: two_factor

session:
  name: authelia_session
  secret: "$(openssl rand -hex 32)"
  expiration: 12h
  inactivity: 1h
  cookies:
    - domain: home
      authelia_url: "https://auth.home"

storage:
  local:
    path: /config/db.sqlite3
  encryption_key: "$(openssl rand -hex 32)"

notifier:
  filesystem:
    filename: /config/notifier.log

Three choices reflected here that matter:

  • Default deny. Every service I add must explicitly be allowed. Better than "default allow, deny specific" — that's how people end up with admin panels accidentally exposed.
  • Two-factor on admin-ish services (Traefik dashboard, Sonarr, Radarr). One-factor on consumer-ish services (Jellyfin, Immich) so my partner and in-law aren't fighting a TOTP app every night.
  • Filesystem notifier for password-reset links during setup. In production I'll swap this for SMTP, but on first deploy it saves me SMTP debugging on day one.

Hour 3 — Wiring Traefik to Authelia

Add the ForwardAuth middleware in Traefik's dynamic config:

# traefik/dynamic.yml
http:
  middlewares:
    authelia:
      forwardAuth:
        address: http://authelia:9091/api/verify?rd=https://auth.home/
        trustForwardHeader: true
        authResponseHeaders:
          - Remote-User
          - Remote-Groups
          - Remote-Name
          - Remote-Email

Then every protected service gets one label:

labels:
  - traefik.enable=true
  - traefik.http.routers.jellyfin.rule=Host(\`jellyfin.home\`)
  - traefik.http.routers.jellyfin.middlewares=authelia@file

That's it. That's the contract. Add the middleware reference, hit the URL, Authelia's login page appears.

Hour 4 — User setup, TOTP, and the password file

Generate a bcrypt hash for each user:

docker run --rm authelia/authelia:4.38 \
  authelia crypto hash generate argon2 --password 'your-password-here'

Put the result in users_database.yml:

users:
  theo:
    displayname: "Theo Marren"
    password: "$argon2id$v=19$m=65536,t=3,p=4$..."
    email: [email protected]
    groups: ["admins"]
  partner:
    displayname: "Partner"
    password: "$argon2id$v=19$m=65536,t=3,p=4$..."
    email: [email protected]
    groups: ["household"]

First login at https://auth.home. Authelia prompts to set up TOTP; scan the QR with Aegis (or any TOTP app, not SMS). You are done.

The one thing that tripped me up

Jellyfin has its own auth and the forward-auth headers don't feed it automatically. I have two options:

  1. Leave Jellyfin's own login in front of Authelia. Two logins. Ugly.
  2. Use Authelia headers to auto-log-in to Jellyfin via the LDAP backend + Jellyfin's LDAP plugin. Nice. More moving parts.

I went with option 1 for now. My partner has her Jellyfin credentials saved in Safari — the Authelia login is extra but not painful.

The next essay will be about Step-CA

Authelia + Traefik closes the auth door. The next hole is that browsers still scream about self-signed certs on internal-only services. Step-CA fixes that — a private CA my household devices trust, automated certs via ACME. That's a separate weekend.

Context for the whole security layer: The self-hosting stack I actually use in 2026.

# issues (0)

$ no issues filed yet. be the first — the form is below.

# add an issue

Comments are moderated. Links are capped. Be kind, be specific.