# oauth2-proxy Keycloak SSO gate for apps **without native OIDC**: `mlflow`, `portainer-ce`, `rabbitmq`. One sidecar container per protected vhost. nginx-proxy's vhost for each gates user requests with `auth_request /oauth2/auth`, and forwards `/oauth2/*` paths to the sidecar for the OIDC handshake. Cloud-only. Each sidecar binds to port 4180 on the `app` network so nginx can reach it. ## Files ``` stacks/oauth2-proxy/ ├── compose.yml # three services: oauth2-proxy-mlflow, -portainer, -rabbitmq └── README.md ``` ## How a request flows 1. Browser → `https://ml.wbd-rd.nl/` (no cookie) 2. nginx `auth_request /oauth2/auth` subrequest → oauth2-proxy → **401** 3. nginx `error_page 401 = /oauth2/sign_in` → oauth2-proxy → **302** to Keycloak 4. User authenticates at Keycloak → redirected to `https://ml.wbd-rd.nl/oauth2/callback?code=…` 5. oauth2-proxy exchanges the code, sets `_oauth2_` cookie, redirects to the original URL 6. Browser retries `/`, this time `/oauth2/auth` returns **202**, request proxies through to mlflow ## Required Keycloak setup per client Each client must have: - Standard flow enabled, confidential client (`publicClient=false`). - Redirect URI exactly: `https:///oauth2/callback` - **Hardcoded audience mapper** so the access token's `aud` claim includes the client_id. Without this, oauth2-proxy rejects the callback with HTTP 500 because the default Keycloak `aud` is `[realm-management, account]` — see `cloud/README.md` for the kcadm bootstrap script. ## nginx vhost shape ```nginx server { listen 443 ssl; server_name .wbd-rd.nl; # … cert directives … location /oauth2/ { proxy_pass http://oauth2-proxy-:4180; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Auth-Request-Redirect $request_uri; } location = /oauth2/auth { internal; proxy_pass http://oauth2-proxy-:4180; proxy_set_header Host $host; proxy_set_header X-Original-URI $request_uri; proxy_pass_request_body off; proxy_set_header Content-Length ""; } location / { auth_request /oauth2/auth; error_page 401 = /oauth2/sign_in; auth_request_set $auth_cookie $upstream_http_set_cookie; add_header Set-Cookie $auth_cookie; proxy_pass http://; # … app-specific headers … } } ``` ## Caveats - **No fine-grained authorization.** oauth2-proxy only enforces "is this Keycloak user authenticated"; restricting to a subset (e.g. only `app-admin`) is a separate config (`OAUTH2_PROXY_ALLOWED_GROUPS` / `OAUTH2_PROXY_ALLOWED_EMAIL_DOMAINS`). - **Cookies are HUGE.** oauth2-proxy embeds the JWT + refresh token in the session cookie. Without raising `proxy_buffer_size` to ≥ 16k in nginx, the callback returns 502 "upstream sent too big header". The nginx-proxy stack already sets this globally. - **DNS caching on nginx restart.** When oauth2-proxy containers are recreated they get new Docker bridge IPs. nginx will keep using the old IPs until reloaded. The nginx-proxy stack now ships `resolver 127.0.0.11 valid=30s;` so this self-heals on the resolver TTL. - **Cookie secrets must be exactly 16, 24, or 32 bytes** (raw, not base64). `openssl rand -hex 16` gives 32 hex chars = the right shape. ## TODO - Restrict access per app via `OAUTH2_PROXY_ALLOWED_GROUPS=app-admin` (or similar) once we have proper realm groups - Consider consolidating to a single oauth2-proxy with a wildcard cookie domain `.wbd-rd.nl` once we trust the shared-cookie tradeoff