Authentication & SSO Pattern (Pocket ID + Caddy Security)¶
Pattern Status: ✅ Production Ready Last Updated: 2025-12-05 Version: 3.0.0 (Comprehensive Auth Patterns)
Refer to docs/pocketid-integration-pattern.md for the detailed portal configuration.
Authentication Decision Framework¶
Always research authentication options before implementing a service. Use this priority order:
Priority Order¶
| Priority | Pattern | When to Use | Example |
|---|---|---|---|
| 1 | Native OIDC | Multi-user app with complex roles/permissions | paperless, mealie |
| 2 | Trusted Header Auth | Multi-user app supporting auth proxy (Remote-User) | grafana, organizr |
| 3 | Disable Auth + caddySecurity | Single-user app (PREFERRED) | arr apps, dashboards |
| 4 | Hybrid Auth (SSO + API key) | Auth can't be disabled but has API key | paperless-ai |
| 5 | Built-in Auth Only | Auth can't be disabled, no alternatives | plex |
| 6 | No Auth | Internal S2S services only | webhooks |
Research Checklist¶
Before implementing auth for any service, answer these questions:
- Native OIDC/OAuth2 support? - Does the app support OpenID Connect?
- Trusted header auth? - Does it accept
Remote-User,X-Emailheaders from proxy? - Can auth be disabled? - Look for
auth.enabled = false,DISABLE_AUTH=true, etc. - API key support? - Can we inject an API key header to bypass auth?
- Multi-user needed? - Do different users need different permissions?
Core Components¶
- Pocket ID service – Native passkey-first OIDC provider with encrypted storage and SMTP support for recovery emails.
- Caddy Security portal – Applies policies (admins/users/bypass) and injects the necessary headers before traffic reaches services.
- Service modules – Each service declares its authentication posture through
reverseProxy.caddySecurityoptions.
Implementation Patterns¶
Pattern 1: Native OIDC (Multi-User with Roles)¶
Use when: App has complex per-user permissions (folders, meal plans, document access).
# Example: Mealie with native OIDC
modules.services.mealie = {
oidc = {
enable = true;
configurationUrl = "https://id.${config.networking.domain}/.well-known/openid-configuration";
clientIdFile = config.sops.secrets."mealie/oidc_client_id".path;
clientSecretFile = config.sops.secrets."mealie/oidc_client_secret".path;
autoSignup = true;
autoRedirect = true;
};
};
SOPS secrets required:
Pattern 2: Trusted Header Auth (Auth Proxy)¶
Use when: Multi-user app that trusts proxy-injected headers but doesn't have OIDC.
# Example: Grafana with auth.proxy
services.grafana.settings = {
auth.proxy = {
enabled = true;
header_name = "Remote-User";
header_property = "username";
auto_sign_up = true;
};
server.root_url = "https://grafana.${config.networking.domain}";
};
# Caddy injects Remote-User after PocketID auth
modules.services.grafana.reverseProxy = {
enable = true;
hostName = "grafana.${config.networking.domain}";
caddySecurity = forgeDefaults.caddySecurity.admins;
};
Common trusted headers:
- Remote-User - Standard CGI variable
- X-Forwarded-User - Common proxy header
- X-Email - Email-based identity
- X-Forwarded-Email - Alternative email header
Pattern 3: Disable Auth + caddySecurity (PREFERRED for Single-User)¶
Use when: Single-user/household app where everyone has same permissions.
This is the preferred pattern for most homelab services - provides consistent SSO experience.
# Example: Sonarr with disabled native auth
modules.services.sonarr = {
enable = true;
# Disable native authentication
authenticationMethod = "External"; # or "None", "DisabledForLocalAddresses"
reverseProxy = {
enable = true;
hostName = "sonarr.${config.networking.domain}";
# PocketID SSO via Caddy
caddySecurity = forgeDefaults.caddySecurity.media;
};
};
Environment variable patterns to look for:
- SONARR__AUTH__METHOD = "External"
- DISABLE_AUTH = "true"
- authentication = "None"
- auth.enabled = false
Pattern 4: Hybrid Auth (SSO + API Key Injection)¶
Use when: Auth can't be disabled but service accepts API key header.
# Example: paperless-ai
modules.services.paperless-ai = {
enable = true;
apiKeyFile = config.sops.secrets."paperless-ai/api_key".path;
reverseProxy = {
enable = true;
hostName = "paperless-ai.${config.networking.domain}";
caddySecurity = forgeDefaults.caddySecurity.home;
# Inject API key to bypass internal auth
reverseProxyBlock = ''
header_up x-api-key {$PAPERLESS_AI_API_KEY}
'';
};
};
How it works:
1. User hits https://paperless-ai.holthome.net
2. Caddy redirects to PocketID for SSO
3. After auth, Caddy injects x-api-key header
4. Service accepts request as authenticated
5. User sees UI without additional login
Note: All users share the same API key identity - no per-user permissions.
Pattern 5: Built-in Auth Only (Last Resort)¶
Use when: Auth can't be disabled and no API key/proxy auth support.
# Example: Plex (has its own user system)
modules.services.plex = {
enable = true;
# No caddySecurity - Plex handles its own auth
reverseProxy = {
enable = true;
hostName = "plex.${config.networking.domain}";
# No caddySecurity here
};
};
When this is acceptable: - App has comprehensive user management (Plex, Jellyfin) - Users need app-specific features tied to their account - App doesn't support any proxy auth patterns
Setup Steps¶
1. Enable Pocket ID¶
modules.services.pocketid = {
enable = true;
domain = "id.${config.networking.domain}";
dataDir = "/var/lib/pocket-id";
smtp = {
host = "smtp.mailgun.org";
port = 587;
username = "pocket-id@holthome.net";
passwordFile = config.sops.secrets."pocketid/smtp_password".path;
};
};
2. Configure Caddy Security Policies¶
modules.infrastructure.caddySecurity = {
enable = true;
portalHost = "id.${config.networking.domain}";
policies = {
admins = { allowedGroups = [ "admins" ]; };
users = { allowedGroups = [ "admins" "users" ]; };
media = { allowedGroups = [ "admins" "users" "media" ]; };
home = { allowedGroups = [ "admins" "users" "home" ]; };
};
};
3. Use forgeDefaults Helpers¶
let
forgeDefaults = import ../lib/defaults.nix { inherit config lib; };
in
{
modules.services.myapp.reverseProxy = {
enable = true;
hostName = "myapp.${config.networking.domain}";
caddySecurity = forgeDefaults.caddySecurity.home; # or .media, .admin
};
}
Troubleshooting¶
Passkey Button Missing¶
- Check
modules.services.pocketid.webauthn.enablePasskeyLoginis true - Try incognito/private window (cached session)
- Re-register passkeys if needed
API Bypass Not Working¶
- Verify
allowedNetworksconfiguration - Check request origin IP
- Confirm bypass path matches endpoint
Double Login Prompt¶
- Service has native auth enabled + caddySecurity
- Fix: Disable native auth (Pattern 3) or remove caddySecurity
Remote-User Header Not Trusted¶
- App requires trusted proxy configuration
- Add your Caddy IP to app's trusted proxies list
- Check header name matches what app expects
Best Practices¶
- Prefer Pattern 3 (disable auth + caddySecurity) for single-user apps
- Use Pattern 2 (trusted headers) for multi-user when simpler than OIDC
- Store all secrets in SOPS - never inline credentials
- Guard contributions with
lib.mkIf serviceEnabled - Test auth flow after deployment: logout, clear cookies, re-authenticate
References¶
- Pocket ID Documentation
- Caddy Security Portal
- WebAuthn (W3C)
.github/prompts/nixos/service-module.prompt.md- Service module patterns