This stack implements defense-in-depth authentication with multiple layers:
- Network-level IP filtering — Only allowed IPs/ranges can access services
- VPN mesh — Private Tailscale network for remote access without exposing ports
- SSO portal — Centralized login via Authelia
- Per-service OIDC — Some services bypass forward-auth and authenticate directly
- LDAP directory — Single source of truth for users & groups via LLDAP
| Component | Role |
|---|---|
| Traefik | Reverse proxy — TLS termination, middleware chains (IP allowlist, forward-auth) |
| Authelia | SSO portal & OIDC provider — login, sessions, 2FA, token issuance |
| LLDAP | Lightweight LDAP directory — user identities, group memberships |
| Tailscale / Headscale | WireGuard VPN mesh — encrypted tunnel to access from anywhere |
| PostgreSQL | Persistent storage for Authelia (prefs, TOTP, WebAuthn) |
| Redis | Session store (cookie-based with inactivity/absolute timeouts) |
Services protected by forward-auth redirect unauthenticated users to the SSO portal:
sequenceDiagram
participant B as Browser
participant T as Traefik
participant A as Authelia
participant L as LLDAP
participant S as Service
B->>T: HTTPS request to service.example.com
T->>T: lan middleware — check IP in allowlist
alt IP not in ALLOW_IP_RANGES
T-->>B: 403 Forbidden
else IP allowed
T->>A: Forward-auth check (session cookie)
alt Valid session
A-->>T: 200 + Remote-User / Remote-Groups headers
T->>S: Proxy request with identity headers
S-->>B: Response
else No session or expired
A-->>T: 302 Redirect to login
T-->>B: Redirect to https://auth.example.com
B->>A: User submits credentials
A->>L: LDAP bind (verify password)
L-->>A: Bind success + group memberships
A->>A: Evaluate access policy (one_factor / two_factor)
opt two_factor required
B->>A: TOTP code or WebAuthn assertion
end
A-->>B: Set session cookie + redirect back
B->>T: Original request (with cookie)
T->>A: Forward-auth check (valid cookie)
A-->>T: 200 + identity headers
T->>S: Proxy request
S-->>B: Response
end
end
Services that support OpenID Connect authenticate directly against Authelia:
sequenceDiagram
participant B as Browser
participant S as Service (e.g. Nextcloud)
participant A as Authelia (OIDC Provider)
participant L as LLDAP
B->>S: Click "Login with SSO"
S-->>B: 302 to Authelia /authorize (client_id, redirect_uri, scope)
B->>A: Authorization request
alt Already authenticated (session cookie)
A->>A: Check consent & policy
else Not authenticated
A->>A: Show login form
B->>A: Submit credentials
A->>L: LDAP bind + group lookup
L-->>A: Identity + groups
end
A-->>B: 302 back to Service with authorization code
B->>S: Callback with code
S->>A: Exchange code for tokens (server-to-server)
A-->>S: ID token (JWT, RS256) + access token + refresh token
S->>S: Verify JWT signature, provision/update user
S-->>B: Authenticated session
Registered OIDC clients:
| Client | Scopes | Auth Method | Consent | Policy | Notes |
|---|---|---|---|---|---|
| Nextcloud | openid profile email groups offline_access | client_secret_post | implicit | one_factor | Group provisioning enabled |
| Immich | openid profile email | client_secret_post | implicit | one_factor | Mobile app callback |
| Beszel | openid profile email | client_secret_basic | implicit | one_factor | PKCE (S256) enabled |
| Dockhand | openid profile email groups | client_secret_post | implicit | admin_only | Callback /api/auth/oidc/callback |
| Headplane | openid profile email | client_secret_basic | implicit | two_factor | VPN admin (stricter) |
Every incoming request passes through Traefik's middleware stack:
flowchart LR
R[Request] --> TLS["TLS termination"]
TLS --> Compress["gzip compression"]
Compress --> Headers["Security headers\n(HSTS, X-Frame-Options,\nX-Content-Type-Options)"]
Headers --> LAN{"lan middleware\n(IP allowlist)"}
LAN -->|Denied| Block[403 Forbidden]
LAN -->|Allowed| Auth{"authelia middleware\n(forward-auth)"}
Auth -->|No session| Login["Redirect to\nauth portal"]
Auth -->|Valid session| Backend["Backend service"]
Security headers applied:
Strict-Transport-Security: max-age=31536000— Force HTTPS for 1 yearX-Frame-Options: DENY— Prevent clickjackingX-Content-Type-Options: nosniff— Prevent MIME type sniffingReferrer-Policy: strict-origin-when-cross-origin— Limit referrer leakage
| Service | lan Middleware |
authelia Middleware |
Own OIDC | Protection |
|---|---|---|---|---|
| Authelia portal | — | — | — | Public (login entry point) |
| Nextcloud | — | — | ✓ | OIDC + LAN implicit |
| Immich | ✓ | — | ✓ | LAN-only + OIDC |
| Beszel | ✓ | — | ✓ | LAN-only + OIDC |
| n8n | ✓ | — | — | LAN-only + own auth |
| Ntfy | ✓ | — | — | LAN-only + own auth |
| Homepage | ✓ | ✓ | — | LAN-only + SSO |
| Uptime Kuma | ✓ | ✓ | — | LAN-only + SSO |
| Traefik dashboard | ✓ | ✓ | — | LAN-only + admin + 2FA |
| Pi-hole | ✓ | ✓ | — | LAN-only + admin + 2FA |
| Backrest | ✓ | ✓ | — | LAN-only + admin + 2FA |
| LLDAP | ✓ | ✓ | — | LAN-only + admin + 2FA + own auth |
| Headplane | ✓ | — | ✓ | LAN-only + OIDC + admin + 2FA |
| Dockhand | ✓ | — | ✓ | LAN-only + OIDC-only + admin + 2FA |
Authelia enforces group-based rules per domain:
| Domain Pattern | Required Group | Policy | Description |
|---|---|---|---|
auth.* |
— | bypass | SSO portal itself |
homepage.*, uptime.* |
users | one_factor | General services (login only) |
backrest.*, pihole.*, traefik.* |
admin or lldap_admin | two_factor | Admin tools |
lldap.* |
users, admin, or lldap_admin | two_factor | Directory itself |
*.* (catch-all) |
users | deny | All others blocked by default |
Group management:
- users — Standard users (can log in to general services)
- admin — Full admin access (requires 2FA)
- lldap_admin — LDAP directory admins (requires 2FA)
Create/manage groups in LLDAP admin UI, assign users to groups.
2FA is enforced for admin interfaces (Traefik, Pi-hole, Dockhand, Backrest, LLDAP) and Headplane (VPN admin).
Supported 2FA methods:
- TOTP — Time-based codes (Google Authenticator, Authy, etc.)
- WebAuthn — Hardware/platform security keys (FIDO2)
Users enroll in Authelia portal → Security:
- Enable TOTP or register WebAuthn
- Save backup codes for account recovery
- On next login, Authelia prompts for 2FA
Secrets are auto-generated on first start and stored with restricted permissions:
${DATA_LOCATION}/authelia-config/secrets/
├── jwt_secret # Identity token signing
├── session_secret # Session cookie signing
├── storage_encryption_key # Database credential encryption
├── oidc_hmac_secret # OIDC token HMAC signing
├── oidc_private_key.pem # RSA-2048 for JWT RS256
├── oidc_nextcloud_secret.txt # Per-client shared secrets
├── oidc_immich_secret.txt
├── oidc_beszel_secret.txt
├── oidc_dockhand_secret.txt
├── oidc_headplane_secret.txt
└── ldap_password # LLDAP bind passwordGenerated by: scripts/authelia-pre-start.sh
Permissions: 600 (read/write by owner only)
Storage: Never committed to git, always in ${DATA_LOCATION}
OIDC client secrets are injected into services via read-only Docker volumes — no secrets in environment or images.
Session storage:
- Medium: Redis (fast, volatile)
- Fallback: PostgreSQL (persistent)
Cookie settings:
- Secure flag — HTTPS only
- SameSite=Lax — CSRF protection
- HttpOnly — JavaScript cannot access
- Inactivity timeout — Default 1 hour (configurable)
- Absolute timeout — Default 24 hours (configurable)
Idle sessions expire automatically; users must re-authenticate.
flowchart LR
Internet["Internet\n(attacker)"]
LAN["Home LAN\n(trusted)"]
VPN["Tailscale VPN\n(trusted)"]
Internet -->|80, 443| Firewall["Firewall\n(only Traefik\nexposed)"]
LAN -->|DNS 53\nHTTP/HTTPS| Firewall
VPN -->|encrypted tunnel| Headscale
Firewall -->|IP allowlist| Traefik
Traefik -->|frontend network| Services["Services"]
Services -->|auth network| AuthServices["Auth\n(LLDAP, Authelia)"]
AuthServices -->|no internet| Secure["🔒 Isolated"]
Traefik -->|DNS| PiholeDNS["Pi-hole DNS\n(isolated network)"]
Isolation layers:
- Firewall — Only expose Traefik; hide internal services
- IP allowlist — Block non-LAN/non-VPN traffic
- Docker networks — Internal services isolated from internet
- No default route — DNS network has no internet gateway
- Read-only volumes — Backrest accesses app data read-only
- Set strong passwords — LLDAP admin & Authelia system account
- Enable 2FA — Especially for admin users
- Use WebAuthn — More secure than TOTP (resistant to phishing)
- Rotate credentials — Change passwords regularly, especially after compromises
- Monitor logs — Check Authelia logs for brute-force attempts
- Keep secrets secure — Never share
.env,authelia-config/secrets/, API tokens - Backup verification — Test restores from backups periodically
- Update regularly — Pull latest images, rebuild stack
- TLS 1.2+ — All HTTPS connections
- Certificate — Auto-provisioned by Traefik + Cloudflare DNS challenge
- LDAP encryption — STARTTLS supported for external LDAP
- Database encryption — Authelia credentials encrypted at rest
- Backup encryption — Backrest uses restic encryption (optional passphrase)
- VPN encryption — WireGuard 256-bit cipher by Tailscale/Headscale
- Session encryption — Redis & PostgreSQL (unencrypted by default, add your own)
Note: Redis and local PostgreSQL are not encrypted at rest by default. To add encryption, either:
- Mount on encrypted filesystem (LUKS)
- Use external managed PostgreSQL/Redis with encryption at rest
- Add encryption layer (e.g., Nextcloud full-disk encryption for file contents)
See Configuration for detailed security settings and overrides.