Skip to content

Identity-Aware Proxies (Pomerium, Authentik Outpost, oauth2-proxy)

When you self-host a community service that doesn’t natively support OIDC/SSO — say a URL shortener, a video host, or a metrics dashboard — you don’t have to expose it to the internet anonymously and hope for the best. You can put an identity-aware proxy (sometimes called a forward-auth proxy or zero-trust proxy) in front of it, and require users to authenticate against your existing IdP (e.g., Authentik) before any request reaches the upstream app.

This page covers what these tools are, when to choose which one, and how IrregularChat actually uses them.

Browser ──TLS──> Cloudflare ──Tunnel──> IAP ──HTTP──> Upstream App
└──OIDC──> Authentik (IdP)

The IAP sits between your users and the protected app. For every request:

  1. IAP checks for a valid session cookie
  2. No cookie? Redirect to your IdP (Authentik) for login
  3. After IdP auth, IAP issues its own session cookie scoped to the protected app’s domain
  4. With a valid session, IAP forwards the request to the upstream — optionally injecting headers like X-Forwarded-User or a signed JWT so the upstream knows who’s calling

The upstream app doesn’t need to speak OIDC. It just needs to trust traffic from the IAP, optionally read identity headers, and ideally only be reachable through the IAP (not directly).

ToolLicenseFootprintBest for
PomeriumApache 2.0~150-500MB RAM/instance (embeds Envoy)Per-route policy, layer-4 TCP routes (RTMP), policy-as-code
Authentik Proxy OutpostMIT~50-100MB RAMYou already run Authentik; want native integration with no separate config surface
oauth2-proxyMIT~30-50MB RAMMinimal Go binary; want generic OIDC client without vendor lock-in. Pin ≥7.15.2 (recent CVEs).
AutheliaApache 2.0~30MB RAMStandalone (it’s also an IdP itself) — overlaps with Authentik if you already run it
Caddy + caddy-securityApache 2.0tinySingle-maintainer plugin; Trail of Bits found SSO flaws (2023) — vet carefully
TinyauthMITtinyNewer, immature for production SSO
Ory OathkeeperApache 2.0mediumAPI-gateway-shaped, overkill for most app-protection use cases

Already running Authentik? Default to Authentik Proxy Outpost. One config surface (Authentik admin UI), no separate OIDC client to wire up per app, ~10x lighter than Pomerium per protected app.

Need layer-4 TCP routing (RTMP ingest, raw socket forwarding)? Pomerium can do it; Authentik Proxy Outpost is HTTP/HTTPS only.

Need per-route or per-path policies beyond simple “must be in group X” — e.g., business-hours-only access, IP-range restrictions, custom claim expressions? Pomerium’s PPL is the most expressive language.

Want minimal moving parts and don’t need vendor-specific features? oauth2-proxy — boring, mature, fits in any reverse proxy chain.

IrregularChat runs two Pomerium instances as the IAP layer in front of community apps that don’t natively OIDC:

Pomerium instanceApps protectedAuth hostnameCookie domain
peertube-pomeriumPeerTube, SearXNG, RTMP ingestauthenticate.irregularchat.comirregularchat.com
shlink-pomeriumURL shortener (url.irregular.chat)authenticate.irregular.chatirregular.chat

Both authenticate via Authentik using the OIDC redirect flow.

Operator note: the rest of this page documents what we’ve learned the hard way running these. If you’re setting up your own IAP for a community app, the patterns here transfer to any of the IAP options above.

videos.irregularchat.com ─┐
search.irregularchat.com ─┼─> Pomerium ─> upstream apps
rtmp.irregularchat.com ───┘ │
authenticate.irregularchat.com ─> Authentik

Key rules:

  1. Use a separate authenticate.<parent> subdomain for the IAP’s own auth UI. Don’t reuse the same hostname as a protected app — that causes the IAP’s internal routes (/.pomerium/*, /oauth2/*) to collide with the upstream’s /, and users land on the IAP dashboard after login instead of the app they wanted.
  2. Set cookie_domain to the parent domain. A cookie set during auth on authenticate.irregularchat.com is then visible to videos.irregularchat.com and search.irregularchat.com because they share the parent irregularchat.com.
  3. One IAP instance per parent domain. Cookies cannot span unrelated parents. irregular.chat and irregularchat.com are different parents — they need their own IAP instance each.
  4. Whitelist exact callback URLs in your IdP for both the authenticate hostname and any other callback paths the IAP uses (/.pomerium/callback AND /oauth2/callback for Pomerium 0.32+).

Pitfall 1: same-hostname all-in-one mode loses the original URL after auth

Section titled “Pitfall 1: same-hostname all-in-one mode loses the original URL after auth”

Symptom: User logs in successfully but lands on /.pomerium/ (or /oauth2/sign_in) instead of the app they were trying to reach.

Cause: IAP is configured with authenticate_service_url set to the same hostname as a proxy route’s from: URL. The IAP’s internal routes claim / of that hostname.

Fix: dedicate a separate authenticate.<parent> subdomain for the IAP and set cookie_domain to the parent.

Pitfall 2: 5-minute access tokens cause re-login bounces every few minutes

Section titled “Pitfall 2: 5-minute access tokens cause re-login bounces every few minutes”

Symptom: Users get bounced through the auth flow every 3-5 minutes, even though they just logged in.

Cause: IdP’s default access token validity is too short (Authentik defaults to 5 min). The IAP refreshes the token before expiry; any single network hiccup or IdP latency loses the session.

Fix: in Authentik, set the OAuth2Provider’s access_token_validity to something generous like hours=24. The access token is exchanged only between IAP and IdP — never exposed to users — so a long TTL doesn’t widen the user-visible attack surface. End-user session length is governed by the IAP’s own cookie TTL (cookie_expire), not the IdP token.

Pitfall 3: Client secrets with shell-special chars get silently dropped

Section titled “Pitfall 3: Client secrets with shell-special chars get silently dropped”

Symptom: Authentik logs Invalid client secret, IAP logs oauth2: invalid_client. The secret is in your .env file but the container doesn’t see it.

Cause: Docker Compose’s env_file: parser silently drops values containing characters it can’t parse — backticks, dollar signs, parens, quotes, brackets, semicolons. Authentik’s default secret generator (generate_key(128)) produces such characters.

Fix: use only alphanumeric secrets for anything passed through env files. In Authentik, regenerate with generate_id(64) instead — produces base62 ([A-Za-z0-9]) only.

Pitfall 4: AppArmor blocks Envoy in Pomerium containers

Section titled “Pitfall 4: AppArmor blocks Envoy in Pomerium containers”

Symptom: Pomerium container crash-loops with unable to bind domain socket ... errno=9.

Cause: Default Docker apparmor=docker-default profile permits Unix stream sockets but denies Unix dgram sockets. Envoy’s primary→child IPC needs dgram. Same class of bug bites Outline (Node.js spawn) and other containers.

Fix: add to your docker-compose.yml:

security_opt:
- apparmor:unconfined
- no-new-privileges:true

Pitfall 5: image: pomerium/pomerium:latest doesn’t auto-update

Section titled “Pitfall 5: image: pomerium/pomerium:latest doesn’t auto-update”

Symptom: Running months-behind versions because the latest tag is only checked when you explicitly docker compose pull.

Fix: add a quarterly checklist item to docker compose pull <iap-service> && docker compose up -d <iap-service> for each protected app. Most security fixes ship as bundled Envoy bumps in routine point releases — the GitHub Security Advisories page alone misses them.

Pitfall 6: In-memory session store wipes on restart

Section titled “Pitfall 6: In-memory session store wipes on restart”

Symptom: All users get a “please sign in” prompt right after you restart the IAP container.

Cause: Most IAPs default to in-memory session storage. Restart = sessions lost.

Fix: for production, use a persistent session store. Pomerium supports Postgres (Redis is not supported, despite some older blog posts). oauth2-proxy supports Redis. Authentik Proxy Outpost reuses Authentik’s existing Redis. Persistent storage is also a prerequisite for running multiple IAP replicas behind a load balancer (HA).

If you’re using Authentik as your IdP (which is what IrregularChat does):

  • Add the groups scope mapping to your OAuth2Provider if you want the IAP to gate access by group membership. The default OIDC scopes (openid email profile) don’t include groups.
  • Request groups in your IAP’s OIDC scope list explicitly: idp_scopes: ["openid", "email", "profile", "groups"]
  • Brand-aware redirects — Authentik supports multiple Brands, one per hostname. Users hitting sso.irregularchat.com will see IrregularChat-branded login; the same Authentik instance can serve other community brands on different hostnames.
  • Token validity per provider — different services may want different access_token_validity. Pomerium-fronted apps can run at hours=24 safely. Mail providers (SOGo) work better at hours=1 due to client refresh patterns. API-only clients can stay at the minutes=5 default.
  1. Run a synthetic auth-flow probe in monitoring per protected app, not just an “is the container up” health check. Phantom OIDC credentials (a deleted IdP application) won’t show up in container health — only when a user actually tries to log in.

    Terminal window
    curl -s -o /dev/null -w 'HTTP %{http_code}\n' \
    "https://sso.irregularchat.com/application/o/<slug>/.well-known/openid-configuration"
    # 200 = OK, 404 = client deleted/renamed
  2. Document your IAP’s cookie_domain strategy — future-you will appreciate knowing which apps share an IAP because they share a parent.

  3. Test failure mode visibly — log out from the IdP, then try to access a protected app. You should land on the IdP login, not a vague proxy error. If the failure mode is a 500 instead, your IAP isn’t handling token expiration gracefully.

  4. Keep public paths public — don’t gate static assets, healthcheck endpoints, or video streaming chunks behind auth. PeerTube needs /static/streaming-playlists/, /socket.io/, /tracker/socket, and /client/ to be unauthenticated; only the main UI needs OIDC. List public-prefix routes BEFORE the catch-all in your IAP config.

You don’t have to commit forever. The IAPs above all interoperate with the same IdP (Authentik), so migrating from Pomerium → Authentik Proxy Outpost (or vice versa) is a configuration change, not a re-architecture. Things to plan for:

  • Per-route policies translate to “Application → Policy Bindings” in Authentik. PPL features Pomerium has that Authentik Outpost doesn’t include time-of-day, IP-range, regex path matching, and layer-4 TCP — keep Pomerium for the routes that need them, even if you migrate the rest.
  • Identity headers are named differently — Pomerium emits X-Pomerium-*, Authentik emits X-Authentik-*. Upstream apps reading these headers (rare) will need a one-line change.
  • Redirect URIs need updating in your IdP allow-list.
  • Cookie continuity doesn’t transfer — users will re-authenticate once during the cutover.