mirror of
https://github.com/lukaszraczylo/traefikoidc.git
synced 2026-06-06 22:49:43 +00:00
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:
@@ -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
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user