Skip to content

Protecting a Service with Pocket ID (oauth2-proxy pattern)

Every service that needs SSO protection gets its own oauth2-proxy container 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)
  1. Caddy receives the request for service.eva-00.network and forwards it to the oauth2-proxy container running on LXC 119 (infra-apps).
  2. oauth2-proxy checks for a valid session cookie. If missing/expired, it redirects to Pocket ID.
  3. Pocket ID authenticates the user (passkey / WebAuthn).
  4. 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 homelab client).
  • 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.

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 disable CSRFProtection, ClickjackingProtection, and HostHeaderValidation — 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=false is 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:

  1. vault-write — store the new secrets in Vault
  2. Deploy infra-apps — redeploys all oauth2-proxy containers with updated .env
  3. Deploy Caddy — picks up the Caddyfile change
  4. 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