Protecting a Service with Pocket ID (oauth2-proxy pattern)
Every service that needs SSO protection gets its own
oauth2-proxycontainer in the external-proxies Docker Compose stack on LXC 119 (infra-apps). Caddy then routes the public domain to that proxy instead of directly to the service.
How it works
Browser → Caddy (TLS) → oauth2-proxy (LXC 119) → Service (internal)
↕
Pocket ID (auth.eva-00.network)
- Caddy receives the request for
service.eva-00.networkand forwards it to the oauth2-proxy container running on LXC 119 (infra-apps). - oauth2-proxy checks for a valid session cookie. If missing/expired, it redirects to Pocket ID.
- Pocket ID authenticates the user (passkey / WebAuthn).
- oauth2-proxy sets a session cookie and proxies the request to the upstream service.
All proxies share the same Pocket ID OIDC client (client ID + secret), but each gets its own cookie secret and cookie name so sessions are isolated.
Checklist: adding a new service
1. Create a Pocket ID OIDC client
In the Pocket ID admin UI at https://auth.eva-00.network:
- Create a new OIDC client (or reuse the shared
homelabclient). - Note the client ID and client secret.
- Add the redirect URI:
https://<service>.eva-00.network/oauth2/callback
If you're reusing the shared homelab client, just add the new redirect URI — no new client needed.
2. Generate a cookie secret
oauth2-proxy treats OAUTH2_PROXY_COOKIE_SECRET as raw bytes (string length = byte count). It must be exactly 16, 24, or 32 characters. Common mistakes: openssl rand -hex 32 produces 64 chars, openssl rand -base64 32 produces 44 chars — both will be rejected.
python3 -c "import secrets; print(secrets.token_urlsafe(24)[:32])"
# or
openssl rand -base64 24 # produces exactly 32 chars
3. Store secrets in Vault
Write the secrets to a per-service Vault path or to secret/external-oauth2-proxies via the vault-write workflow in Forgejo.
The key naming convention: <service>_client_id, <service>_client_secret, <service>_cookie_secret
4. Add the oauth2-proxy service to services/external-proxies/docker-compose.yml
Copy an existing service block and update these fields:
| Field | Convention |
|---|---|
container_name |
oauth2-proxy-<service> |
--upstream |
Internal URL of the service |
OAUTH2_PROXY_COOKIE_SECRET |
${<SERVICE>_COOKIE_SECRET} |
OAUTH2_PROXY_REDIRECT_URL |
https://<service>.eva-00.network/oauth2/callback |
OAUTH2_PROXY_COOKIE_NAME |
_oauth2_<service> |
ports |
Next available port (8082, 8583, 8584, …) |
Example block:
myservice:
image: quay.io/oauth2-proxy/oauth2-proxy:v7.7.1
container_name: oauth2-proxy-myservice
restart: unless-stopped
command:
- --upstream=http://192.168.1.XXX:PORT
environment:
- OAUTH2_PROXY_PROVIDER=oidc
- OAUTH2_PROXY_OIDC_ISSUER_URL=https://auth.eva-00.network
- OAUTH2_PROXY_CLIENT_ID=${POCKETID_CLIENT_ID}
- OAUTH2_PROXY_CLIENT_SECRET=${POCKETID_CLIENT_SECRET}
- OAUTH2_PROXY_COOKIE_SECRET=${MYSERVICE_COOKIE_SECRET}
- OAUTH2_PROXY_EMAIL_DOMAINS=*
- OAUTH2_PROXY_HTTP_ADDRESS=0.0.0.0:4180
- OAUTH2_PROXY_REDIRECT_URL=https://myservice.eva-00.network/oauth2/callback
- OAUTH2_PROXY_SKIP_PROVIDER_BUTTON=true
- OAUTH2_PROXY_COOKIE_SECURE=true
- OAUTH2_PROXY_COOKIE_SAMESITE=none
- OAUTH2_PROXY_COOKIE_NAME=_oauth2_myservice
- OAUTH2_PROXY_INSECURE_OIDC_ALLOW_UNVERIFIED_EMAIL=true
ports:
- "XXXX:4180"
5. Update ansible/playbooks/infra-apps.yml
Add the new secrets to the external-proxies .env write task and add a Vault fetch if using a separate path.
6. Update services/caddy/Caddyfile
Change the service's Caddy block to point at the oauth2-proxy port on LXC 119 (192.168.1.119) instead of the service directly:
myservice.eva-00.network {
reverse_proxy 192.168.1.119:XXXX
}
7. Disable the service's own auth (if applicable)
If the upstream service has its own login page, it will double-authenticate. Disable it if the service supports it.
- qBittorrent: use
WebUI\AuthSubnetWhitelistEnabled=true+WebUI\AuthSubnetWhitelist=192.168.1.118/32, 192.168.1.119/32(qbitwebui + oauth2-proxy). Also disableCSRFProtection,ClickjackingProtection, andHostHeaderValidation— in qBittorrent 5.x, CSRF checks fire before the subnet whitelist and will return 401 when the Origin header doesn't match. These checks are redundant behind oauth2-proxy.LocalHostAuth=falseis also needed.
Important: qBittorrent writes its in-memory config to disk on graceful shutdown. If you edit the config file while the container is running and then restart it, qBittorrent will overwrite your changes. Always stop the container first, edit the config, then start it again.
- Others: consult the service's docs for "trusted reverse proxy" or "bypass local auth" settings.
8. Deploy
Run these workflows in order:
- vault-write — store the new secrets in Vault
- Deploy infra-apps — redeploys all oauth2-proxy containers with updated
.env - Deploy Caddy — picks up the Caddyfile change
- Deploy
<service>(if config changed, e.g. qBittorrent.conf)
Current protected services
| Service | Domain | oauth2-proxy port |
|---|---|---|
| filedump | filedump.eva-00.network | 8082 |
| homebridge | hb.eva-00.network | 8583 |
| seedbox (qBittorrent) | seedbox.eva-00.network | 8584 |
| normal (qBittorrent) | normal.eva-00.network | 8585 |
| shoko | shoko.eva-00.network | 8586 |
| grimmory | library.eva-00.network | 8587 |
| romm | romm.eva-00.network | 8588 |
IaC locations
| Artifact | Path |
|---|---|
| oauth2-proxy containers | services/external-proxies/docker-compose.yml |
| infra-apps playbook | ansible/playbooks/infra-apps.yml |
| Caddyfile | services/caddy/Caddyfile |
| Deploy workflow | .forgejo/workflows/infra-apps.yml |
| Vault secrets | secret/external-oauth2-proxies |