docs(oidcgate): expand user guide and cross-link

- Add HAProxy and Envoy ext_authz_http wiring snippets.
- Add full OIDCGATE_* env-var inventory (26 fields).
- Add Security Posture section (X-Forwarded-Uri sanitisation, excludedURLs
  guardrail, callbackURL/logoutURL validation).
- Add Bearer-token (M2M) auth composition section with link to BEARER_AUTH.md.
- Add Operational Guidance section (healthz/readyz ACL, Redis for multi-replica,
  no built-in metrics, graceful shutdown deadline).
- Add Debugging section (sentinel path, silent open-redirect rejections,
  /readyz warm-up).
- Cross-link from docs/CONFIGURATION.md.
This commit is contained in:
2026-05-19 16:59:15 +01:00
parent dc0e7e0238
commit 03a755cb53
2 changed files with 292 additions and 21 deletions
+17
View File
@@ -14,6 +14,7 @@ Complete reference for all Traefik OIDC middleware configuration options.
- [Security Headers](#security-headers)
- [Scope Configuration](#scope-configuration)
- [Advanced Options](#advanced-options)
- [Standalone binary (oidcgate)](#standalone-binary-oidcgate)
---
@@ -664,3 +665,19 @@ sessionEncryptionKey: ${OIDC_SECRET_API}
# Good
sessionEncryptionKey: ${OIDC_SECRET_SVC}
```
---
## Standalone binary (oidcgate)
If you don't run Traefik, the same configuration shape documented above
works for the [`oidcgate`](OIDCGATE.md) standalone forward-auth daemon
under `cmd/oidcgate`. Three extra top-level keys (`listen`, `authPath`,
`startPath`) configure the daemon itself; everything else maps 1:1 onto
the `traefikoidc.Config` fields documented in this reference.
See [`docs/OIDCGATE.md`](OIDCGATE.md) for the full daemon guide including
nginx, Caddy, Traefik ForwardAuth, HAProxy and Envoy wiring snippets,
the `OIDCGATE_*` environment-variable inventory, the security posture
(X-Forwarded-Uri sanitisation, excludedURLs guardrail), and how to layer
M2M [bearer-token auth](BEARER_AUTH.md) on the same daemon.
+275 -21
View File
@@ -2,7 +2,26 @@
`oidcgate` is a single binary that exposes the same OIDC middleware that
powers the Traefik plugin as a forward-auth daemon for nginx, Caddy,
Traefik ForwardAuth, HAProxy, and Envoy ext_authz_http.
Traefik ForwardAuth, HAProxy, and Envoy `ext_authz_http`.
## Table of contents
- [Build](#build)
- [Run](#run)
- [Configuration](#configuration)
- [YAML file](#yaml-file)
- [Environment-variable overrides](#environment-variable-overrides)
- [Endpoints](#endpoints)
- [Reverse-proxy snippets](#reverse-proxy-snippets)
- [nginx (`auth_request`)](#nginx-auth_request)
- [Caddy (`forward_auth`)](#caddy-forward_auth)
- [Traefik (`ForwardAuth`)](#traefik-forwardauth)
- [HAProxy](#haproxy)
- [Envoy (`ext_authz_http`)](#envoy-ext_authz_http)
- [Security posture](#security-posture)
- [Bearer-token (M2M) auth on the same daemon](#bearer-token-m2m-auth-on-the-same-daemon)
- [Operational guidance](#operational-guidance)
- [Debugging](#debugging)
## Build
@@ -16,39 +35,92 @@ go build -o oidcgate ./cmd/oidcgate
./oidcgate --config /etc/oidcgate/config.yaml
```
## Config
The daemon parses `--config`, loads YAML, applies any `OIDCGATE_*` env-var
overrides, validates the result, and binds to `listen`. On SIGINT/SIGTERM it
calls `http.Server.Shutdown` with a 15s deadline, draining in-flight requests.
YAML, mirroring the existing Traefik plugin schema with three extra keys:
## Configuration
### YAML file
The OIDC subtree of the config maps 1:1 onto the [`traefikoidc.Config`](CONFIGURATION.md)
struct — every field documented under "Configuration Reference" works here
verbatim. Three extra top-level keys configure the daemon itself:
| Key | Default | Purpose |
|---|---|---|
| `listen` | _required_ | TCP address (e.g. `:8080`, `127.0.0.1:8080`). |
| `authPath` | `/oauth2/auth` | Silent-probe endpoint (used by nginx `auth_request`). |
| `startPath` | `/oauth2/start` | Visible sign-in endpoint. |
Minimal example (see [`examples/oidcgate.yaml`](../examples/oidcgate.yaml)):
```yaml
listen: ":8080"
authPath: "/oauth2/auth" # default; nginx auth_request subrequest target
startPath: "/oauth2/start" # default; visible sign-in endpoint
providerURL: "https://accounts.google.com"
clientID: "your-client-id"
clientSecret: "your-client-secret"
sessionEncryptionKey: "64-hex-bytes"
sessionEncryptionKey: "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
callbackURL: "/oauth2/callback"
logoutURL: "/oauth2/logout"
# ... any other traefikoidc Config field works here verbatim
```
Secrets can be overridden via environment variables:
`OIDCGATE_CLIENT_SECRET`, `OIDCGATE_SESSION_ENCRYPTION_KEY`,
`OIDCGATE_CLIENT_ID`, etc.
Nested structs (`redis:`, `securityHeaders:`, `dynamicClientRegistration:`)
round-trip cleanly through YAML — same shape as in `.traefik.yml`.
### Environment-variable overrides
Any of the following scalar fields can be overridden at runtime by an
`OIDCGATE_<UPPER_SNAKE_CASE>` environment variable. The env var wins over
the YAML value when set and non-empty. Intended for secret injection
(K8s `valueFrom.secretKeyRef`, systemd `EnvironmentFile=`, etc.).
| YAML key | Env var |
|---|---|
| `listen` | `OIDCGATE_LISTEN` |
| `authPath` | `OIDCGATE_AUTH_PATH` |
| `startPath` | `OIDCGATE_START_PATH` |
| `providerURL` | `OIDCGATE_PROVIDER_URL` |
| `clientID` | `OIDCGATE_CLIENT_ID` |
| `clientSecret` | `OIDCGATE_CLIENT_SECRET` |
| `audience` | `OIDCGATE_AUDIENCE` |
| `callbackURL` | `OIDCGATE_CALLBACK_URL` |
| `logoutURL` | `OIDCGATE_LOGOUT_URL` |
| `postLogoutRedirectURI` | `OIDCGATE_POST_LOGOUT_REDIRECT_URI` |
| `sessionEncryptionKey` | `OIDCGATE_SESSION_ENCRYPTION_KEY` |
| `cookiePrefix` | `OIDCGATE_COOKIE_PREFIX` |
| `cookieDomain` | `OIDCGATE_COOKIE_DOMAIN` |
| `logLevel` | `OIDCGATE_LOG_LEVEL` |
| `revocationURL` | `OIDCGATE_REVOCATION_URL` |
| `oidcEndSessionURL` | `OIDCGATE_OIDC_END_SESSION_URL` |
| `userIdentifierClaim` | `OIDCGATE_USER_IDENTIFIER_CLAIM` |
| `groupClaimName` | `OIDCGATE_GROUP_CLAIM_NAME` |
| `roleClaimName` | `OIDCGATE_ROLE_CLAIM_NAME` |
| `clientAuthMethod` | `OIDCGATE_CLIENT_AUTH_METHOD` |
| `clientAssertionPrivateKey` | `OIDCGATE_CLIENT_ASSERTION_PRIVATE_KEY` |
| `clientAssertionKeyPath` | `OIDCGATE_CLIENT_ASSERTION_KEY_PATH` |
| `clientAssertionKeyID` | `OIDCGATE_CLIENT_ASSERTION_KEY_ID` |
| `clientAssertionAlg` | `OIDCGATE_CLIENT_ASSERTION_ALG` |
| `caCertPath` | `OIDCGATE_CA_CERT_PATH` |
| `caCertPEM` | `OIDCGATE_CA_CERT_PEM` |
Nested-struct fields (Redis, security headers, DCR) are YAML-only — set
them in the config file, not via env.
## Endpoints
| Path | Method | Purpose |
|---|---|---|
| `/oauth2/auth` | GET | Silent probe — `200` if authenticated, `401` if not |
| `/oauth2/start` | GET | Visible sign-in — `302` to IdP, accepts `?rd=` for return target |
| `/oauth2/callback` | GET | IdP callback |
| `/oauth2/logout` | GET/POST | Session terminate |
| `/healthz` | GET | Liveness — `200` while process is alive |
| `/readyz` | GET | Readiness — `200` after first metadata discovery, else `503` |
| `/oauth2/auth` | GET | Silent probe — `200` if authenticated, `401` if not. Never returns `302`; the middleware's redirect-to-IdP is rewritten in-flight to `401` with the original `Location` carried as `X-Auth-Redirect`. |
| `/oauth2/start` | GET | Visible sign-in — `302` to the IdP authorize URL. Accepts `?rd=<safe-path>` (or honours `X-Forwarded-Uri`) for the post-login redirect target. |
| `/oauth2/callback` | GET | IdP `code`+`state` exchange. Path is configurable via `callbackURL`. |
| `/oauth2/logout` | GET/POST | Terminates the session. Path is configurable via `logoutURL`. Honours `oidcEndSessionURL` for RP-initiated logout. |
| `/healthz` | GET | Liveness — `200` while the process is alive. |
| `/readyz` | GET | Readiness — `200` once the OIDC discovery document has been fetched, otherwise `503`. |
## nginx
## Reverse-proxy snippets
### nginx (`auth_request`)
```nginx
location = /oauth2/auth {
@@ -65,7 +137,7 @@ location @oidc_signin {
}
location /oauth2/ {
proxy_pass http://oidcgate:8080;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-Proto $scheme;
}
location / {
@@ -79,13 +151,17 @@ location / {
}
```
## Caddy
### Caddy (`forward_auth`)
```
```caddyfile
example.com {
forward_auth oidcgate:8080 {
uri /oauth2/auth
copy_headers X-Forwarded-User X-Forwarded-Email
@denied status 401
handle_response @denied {
redir /oauth2/start?rd={http.request.uri} 302
}
}
handle /oauth2/* {
reverse_proxy oidcgate:8080
@@ -94,7 +170,7 @@ example.com {
}
```
## Traefik ForwardAuth
### Traefik (`ForwardAuth`)
```yaml
http:
@@ -106,3 +182,181 @@ http:
- X-Forwarded-User
- X-Forwarded-Email
```
Traefik can follow the `X-Auth-Redirect` value via a chained `redirectScheme`
middleware, or you can configure the upstream router to redirect `401`
`/oauth2/start` directly.
### HAProxy
```haproxy
frontend fe_https
bind *:443 ssl crt /etc/haproxy/certs/site.pem
http-request set-var(req.orig_uri) path
http-request send-spoe-group oidc auth-check # or use lua/SPOE; simplest is the lua snippet below
# The simpler pattern: dispatch /oauth2/* to oidcgate, everything else
# goes through a Lua filter that issues a sub-request to /oauth2/auth.
acl is_oidc_endpoint path_beg /oauth2/
use_backend be_oidcgate if is_oidc_endpoint
default_backend be_app
backend be_oidcgate
server oidcgate1 oidcgate:8080
backend be_app
server app1 backend:3000
```
HAProxy does not have a first-class `auth_request` equivalent in pure
config — the canonical patterns are SPOE (Stream Processing Offload Engine),
a Lua filter that issues `/oauth2/auth` and reads the response, or a
sidecar that does the dance. Reach for SPOE for high-throughput
production; Lua is simpler for low-volume.
### Envoy (`ext_authz_http`)
```yaml
http_filters:
- name: envoy.filters.http.ext_authz
typed_config:
"@type": type.googleapis.com/envoy.extensions.filters.http.ext_authz.v3.ExtAuthz
transport_api_version: V3
http_service:
server_uri:
uri: http://oidcgate:8080
cluster: oidcgate
timeout: 2s
path_prefix: /oauth2/auth
authorization_request:
allowed_headers:
patterns:
- exact: cookie
- exact: authorization
- prefix: x-forwarded-
authorization_response:
allowed_upstream_headers:
patterns:
- exact: x-forwarded-user
- exact: x-forwarded-email
allowed_client_headers:
patterns:
- exact: x-auth-redirect
- exact: set-cookie
```
On `401`, the `X-Auth-Redirect` header is surfaced to the downstream client
via `allowed_client_headers`. A small Envoy `router` filter or
`local_reply_config` rule can convert that into a browser-facing `302`
redirect to `/oauth2/start`.
## Security posture
- **`X-Forwarded-Uri` is sanitised.** The daemon forces
`TrustForwardedURI=true` so the middleware honours `X-Forwarded-Uri` for
the post-login redirect target. To prevent open redirects (CWE-601),
the value is rejected unless it is a safe same-origin path: must start
with `/`, must NOT start with `//` (protocol-relative), and must have
no scheme or host after parsing. Absolute URLs or anything that could
redirect off-origin falls through to `req.URL.RequestURI()`.
- **`excludedURLs` cannot bypass the daemon's own paths.** At config
load, the loader rejects any `excludedURLs` entry that is a prefix of
`authPath`, `startPath`, `callbackURL`, `logoutURL`, or the internal
sentinel path. A misconfiguration like `excludedURLs: ["/"]` (common
"allow all then add auth selectively" mistake) is rejected at startup
with a descriptive error.
- **`callbackURL` and `logoutURL` must be paths.** Absolute URLs are
rejected at config load — both because `http.ServeMux.Handle` panics
on non-`/` patterns and because the middleware's path-match would
silently fail.
- **`listen` is required.** Empty or missing `listen` is rejected at
startup rather than failing later at `net.Listen`.
- **Secrets via env vars.** `clientSecret` and `sessionEncryptionKey`
can be supplied via env vars instead of YAML so they don't end up on
disk if you use a secret manager.
## Bearer-token (M2M) auth on the same daemon
oidcgate uses the full `traefikoidc.Config` shape, so the bearer-token
M2M auth path documented in [`BEARER_AUTH.md`](BEARER_AUTH.md) works
out of the box. Add to your YAML:
```yaml
enableBearerAuth: true
audience: "https://api.example.com"
bearerIdentifierClaim: "sub"
# stripAuthorizationHeader: true # default
# bearerOverridesCookie: false # default — cookie wins on collision
```
With this set, the daemon accepts both:
- Browser users hitting `/oauth2/auth` → cookie session flow.
- API clients calling the protected backend with `Authorization: Bearer <jwt>`
→ bearer validation, principal headers, no session.
The bearer path doesn't go through `/oauth2/auth` separately — it's
applied by the middleware on every request the daemon sees, before the
cookie session check. See [BEARER_AUTH.md](BEARER_AUTH.md) for the full
threat model, identifier sanitisation rules, and failure-response
matrix.
## Operational guidance
- **Run behind a fronting proxy on a private network.** The daemon does
not terminate TLS. Put it on a localhost socket or a private subnet
reachable only from your nginx/Caddy/Traefik/HAProxy/Envoy.
- **`/healthz` and `/readyz` are unauthenticated** — correct for
Kubernetes liveness/readiness probes, but **do not expose them past a
load balancer**. Restrict via an ACL: nginx `allow 10.0.0.0/8; deny
all;`, Caddy `@health remote_ip 10.0.0.0/8`, k8s NetworkPolicy, or
your CNI of choice.
- **Multi-replica deployments** need a shared session store. Enable the
`redis:` block in the config (see [`docs/REDIS.md`](REDIS.md)) so
sessions survive a hop between replicas.
- **No built-in Prometheus metrics yet.** If you need request-level
visibility, take it from your fronting proxy's access logs — both
nginx and Envoy can tag `auth_request` / `ext_authz` outcomes.
- **Logs are minimal by default.** Set `logLevel: debug` while
bringing up a new deployment; raise to `info` (default) or higher
once stable. Debug logs include path-match decisions and metadata
refresh outcomes.
- **Graceful shutdown is 15s.** SIGINT or SIGTERM triggers
`http.Server.Shutdown(ctx)` with a 15-second deadline; in-flight
requests are allowed to complete. If your orchestrator's grace
period is shorter, requests can be cut mid-flight.
## Debugging
- **Requests appear as `/__oidcgate_protected__` in middleware debug
logs.** This is the internal sentinel path used when `/oauth2/auth`
and `/oauth2/start` delegate into the traefikoidc middleware. The
upstream client never sees it; it only shows up in the middleware's
own `Debugf` output when `logLevel: debug` is set.
- **`/oauth2/auth` returns `401` with `X-Auth-Redirect` header on
unauthenticated requests.** This is the deliberate translation of the
middleware's `302` to make nginx `auth_request` work. The browser is
redirected via the fronting proxy's `error_page 401 = @oidc_signin;`
pattern, not by following the daemon's response directly.
- **`/readyz` stays `503` after startup.** The middleware fetches the
OIDC discovery document lazily on first request, so `/readyz` returns
`503` until at least one request has triggered metadata discovery.
Hit `/oauth2/auth` once after startup to warm it up — many K8s
setups achieve the same effect because the liveness probe already
goes through the proxy chain.
- **Cookie/session diagnostics.** With `logLevel: debug` the middleware
logs which session manager was selected (in-memory vs Redis), whether
cookies decrypted successfully, and the JWT validation outcome.
- **Open-redirect rejections are silent.** When the daemon ignores an
unsafe `X-Forwarded-Uri` value, it falls back to `req.URL.RequestURI()`
without logging. This is intentional (no recon signal) — if a user
reports "I keep landing on the wrong page after login", inspect
whether the upstream proxy is forwarding a non-canonical
`X-Forwarded-Uri` value.