Skip to content

Authentication (tinyauth SSO / Google login)

tinyauth provides single sign-on for the homelab via Caddy's forward_auth — you log in with your Google/Gmail account. It replaced Authelia in July 2026: Authelia is an OIDC provider and cannot consume Google as a login source, so it could never do "Sign in with Google". tinyauth (OpenID-certified) can.

How it fits together

flowchart LR
    Browser --> Caddy[Caddy CT 112]
    Caddy -- "forward_auth /api/auth/caddy" --> Tinyauth[tinyauth<br/>192.168.1.241:9092]
    Tinyauth -- "307 to login if no session" --> Portal[auth.mdhmedia.uk/login]
    Portal -- "Login with Google" --> Google[(Google OAuth)]
    Caddy -- "authenticated (Remote-* headers)" --> App[Protected app]
  • Stack: stacks/security/tinyauth/ — Arcane git-sync stack tinyauth on CT 101, port 9092 (image ghcr.io/tinyauthapp/tinyauth)
  • Portal: auth.mdhmedia.uk
  • Login: Google OAuth, restricted to the address(es) in TINYAUTH_OAUTH_WHITELIST
  • Sessions/users: SQLite DB in the tinyauth_tinyauth-data volume (v5 needs no app secret — sessions live in the DB)
  • Secrets: GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET, TINYAUTH_OAUTH_WHITELIST live in the stack's Arcane project .env at /opt/docker/tinyauth/.env (see the stack's .env.example)

Google OAuth client (one-time)

Create at console.cloud.google.comAPIs & Services → Credentials → OAuth client ID → Web application:

  • Authorized redirect URI: https://auth.mdhmedia.uk/api/oauth/callback/google
  • Authorized JavaScript origin: https://auth.mdhmedia.uk
  • On the OAuth consent screen, add your Gmail as a test user (no need to publish for personal use)

Put the Client ID + secret in the stack's Arcane project .env at /opt/docker/tinyauth/.env (it stays on the host, never in git).

Protecting an app behind SSO

Add one line to the app's site block in edge/caddy/Caddyfile; the merge deploys via caddy-deploy.timer (no manual copy):

grafana.mdhmedia.uk {
    import sso_admin         # <- authenticate AND require an admin identity (must be first)
    import std
    reverse_proxy 192.168.1.241:3300
}

The (sso) snippet does the forward_auth to 192.168.1.241:9092/api/auth/caddy and copies the Remote-User/Email/Groups/Name headers through — that authenticates any allowed identity. (sso_admin) wraps (sso) in a route{}, strips any inbound Remote-* headers first (anti-spoof), then returns 403 unless Remote-Email is an admin — because tinyauth has no per-app access control, authorization is done in Caddy on the identity header. Two tiers today:

  • import sso_admin (admin only): grafana, jarvis, mdhdocs, kicad, wgadmin, pve1, pve2, holly, speedtest, libate.
  • import sso (any allowed identity, incl. family): the AudioBookRequest UI (abr.mdhmedia.uk) and the Homepage root (mdhmedia.uk, gated since the 2026-07-20 consolidation).

Browser vs API: 307 vs 401

tinyauth returns a 307 redirect to the login page for real browser navigations (detected via the Sec-Fetch-Mode: navigate header) and a 401 to API-shaped requests. So a curl without browser headers sees 401 — that's correct, not a fault. Caddy copies whichever response back to the client.

Don't SSO-gate an app with non-browser clients on the same host

Native apps (Plex clients, arr companion apps, API keys) can't complete a browser Google flow. Leave those routes public (the arr apps and Plex already are). Only gate browser-only admin apps.

Managing access

  • Add an OAuth user (Google): append their email to TINYAUTH_OAUTH_WHITELIST (comma-separated, or a /regex/) in the Arcane project .env (/opt/docker/tinyauth/.env), then redeploy the stack. The whitelist covers the Google login.
  • Add a local username/password user (no Google account, e.g. a non-Google mailbox that can't do the Google flow): hash the password with caddy hash-password on CT 112, append username:hash to /opt/tinyauth/users.txt on CT 101, then docker restart tinyauth. The file is bind-mounted read-only via TINYAUTH_AUTH_USERSFILE=/users.txt, lives outside /opt/docker so the git-sync never touches it, and is not in git (it holds bcrypt hashes). It shows a login form beside the Google button.

Authelia (predecessor — fully removed)

Authelia was retired 2026-07-07 and fully torn down 2026-07-08 after the Google flow soaked: Portainer stack + authelia_authelia-data volume deleted, the repo folder stacks/security/authelia/ and the (authelia) Caddy snippet removed. The only remnant is a copy of its users_database.yml at /root/authelia-users-backup.yml on CT 101 (historical record; the argon2 hashes are useless to tinyauth). Re-introducing Authelia would be a fresh install from upstream docs, not a rollback.

Troubleshooting

# tinyauth logs
ssh root@192.168.1.199 "pct exec 101 -- docker logs tinyauth --tail 50"
# confirm the Google provider is wired (returns a Google auth URL)
ssh root@192.168.1.199 "pct exec 101 -- curl -s http://127.0.0.1:9092/api/oauth/url/google"

Common: a redirect loop means the app's block imports (sso) but auth.mdhmedia.uk itself also does (it must not); a login that bounces back to the portal means your email isn't in the whitelist; an "unverified app" Google screen is expected for a personal test-mode OAuth client (continue past it / add yourself as a test user).