Reverse proxy: trusted forwarded headers
Use this when chmonitor sits behind a proxy that has already authenticated the user — oauth2-proxy with Dex, Authelia, Traefik ForwardAuth, nginx + auth-url, or similar. The proxy forwards the user’s identity as HTTP headers; chmonitor trusts them and builds a full principal (name, email, avatar, groups) from those headers.
When to use
Section titled “When to use”| You have | Use |
|---|---|
| oauth2-proxy + Dex (or any OIDC provider) | trusted provider |
| Authelia / Authentik in front of chmonitor | trusted provider |
Traefik ForwardAuth or nginx auth_request | trusted provider |
| Cloudflare Access (signed JWT) | proxy provider |
| nginx + a shared-secret header only (no full profile) | proxy provider / trusted-header |
The trusted provider differs from the proxy provider:
proxyis Cloudflare Access-centric (verifies aCf-Access-Jwt-AssertionJWT) or carries a bare subject via a single identity header.trustedextracts a full profile (name, email, avatar, groups, custom claims) from multiple forwarded headers, supports group-based access gating, and exposes that profile in the sidebar and/api/v1/auth/me.
How it works
Section titled “How it works”- The upstream proxy authenticates the user and copies their identity into request headers before forwarding to chmonitor.
- chmonitor checks the trust gate — either a shared secret header or an explicit allow-insecure flag. If the gate does not pass, the request is treated as unauthenticated regardless of what headers are present.
- If the gate passes, chmonitor reads the configured header names to build a principal: subject, email, display name, avatar URL, and groups/roles.
- Optional group gating: if
CHM_TRUSTED_ALLOWED_GROUPSis set, the user’s forwarded groups must intersect (case-insensitive) or access is denied. - A trusted-authenticated user counts as
authenticatedfor the feature permission matrix, which unlocksauthenticated-access features (AI agent, writes) on that request.
The identity is available at GET /api/v1/auth/me and displayed in the sidebar user menu.
Trust gate
Section titled “Trust gate”You must configure exactly one of these. Without a trust gate, the provider fails closed and denies all requests.
| Variable | Default | Description |
|---|---|---|
CHM_TRUSTED_AUTH_SECRET | — | Shared secret. The proxy must send it in CHM_TRUSTED_SHARED_SECRET_HEADER. Constant-time compared. Recommended. |
CHM_TRUSTED_ALLOW_INSECURE | false | When true, headers are trusted with no secret check. Only safe when the worker/Service is unreachable except via the proxy (e.g. k8s ClusterIP behind an ingress). |
CHM_TRUSTED_SHARED_SECRET_HEADER | X-Chm-Proxy-Secret | Header name carrying the shared secret. |
Set the secret out-of-band — never in plaintext env files committed to source control:
wrangler secret put CHM_TRUSTED_AUTH_SECRET## orkubectl create secret generic chm-trusted-secret \ --from-literal=value=<secret>All environment variables
Section titled “All environment variables”Provider activation
Section titled “Provider activation”| Variable | Default | Description |
|---|---|---|
CHM_AUTH_PROVIDER | none | Set to trusted. Server-side. |
VITE_AUTH_PROVIDER | none | Set to trusted. Build-time client mirror. Must match CHM_AUTH_PROVIDER. |
Trust gate
Section titled “Trust gate”| Variable | Default | Description |
|---|---|---|
CHM_TRUSTED_AUTH_SECRET | — | Shared secret value (runtime secret). |
CHM_TRUSTED_ALLOW_INSECURE | false | Skip secret check (network-isolation required). |
CHM_TRUSTED_SHARED_SECRET_HEADER | X-Chm-Proxy-Secret | Header name the proxy sends the secret in. |
Identity headers
Section titled “Identity headers”| Variable | Default header | What it populates |
|---|---|---|
CHM_TRUSTED_USER_HEADER | X-Forwarded-User | Subject / user id. Falls back to the email header. Required for a usable identity. |
CHM_TRUSTED_EMAIL_HEADER | X-Forwarded-Email | Email address. |
CHM_TRUSTED_NAME_HEADER | X-Forwarded-Preferred-Username | Display name shown in the sidebar. |
CHM_TRUSTED_AVATAR_HEADER | X-Forwarded-Avatar | Avatar URL. |
CHM_TRUSTED_GROUPS_HEADER | X-Forwarded-Groups | Comma- or space-separated group/role list. |
CHM_TRUSTED_ROLE_HEADER | X-Forwarded-Role | Single role value; merged into the groups list. |
CHM_TRUSTED_CUSTOM_HEADERS | — | Extra claims. Comma-separated field:Header-Name pairs, e.g. team:X-Forwarded-Team,dept:X-User-Dept. Available in the principal’s custom claims. |
Access control
Section titled “Access control”| Variable | Default | Description |
|---|---|---|
CHM_TRUSTED_ALLOWED_GROUPS | — | Comma-separated group names. When set, the user’s forwarded groups must include at least one (case-insensitive). Users with no matching group are treated as unauthenticated (401). |
Combines with the existing per-feature vars:
CHM_FEATURE_AGENT_ACCESS=authenticated # agent requires sign-inCHM_FEATURE_SETTINGS_ACCESS=authenticatedCHM_TRUSTED_ALLOWED_GROUPS=sre,ops,adminA trusted-authenticated user satisfies authenticated, so CHM_FEATURE_*_ACCESS=authenticated features are unlocked for them automatically.
Forwarded headers map
Section titled “Forwarded headers map”What each header becomes in the principal:
These are the fields of the JSON returned by GET /api/v1/auth/me (principal):
| Header (default name) | Principal field |
|---|---|
X-Forwarded-User | subject |
X-Forwarded-Email | email |
X-Forwarded-Preferred-Username | name |
X-Forwarded-Avatar | avatarUrl |
X-Forwarded-Groups | roles[] |
X-Forwarded-Role | merged into roles[] |
Custom headers (via CHM_TRUSTED_CUSTOM_HEADERS) | custom.<field> |
Homelab example: k3s + Traefik + oauth2-proxy + Dex
Section titled “Homelab example: k3s + Traefik + oauth2-proxy + Dex”Critical detail: With Traefik ForwardAuth, Traefik copies oauth2-proxy’s response headers back to the upstream request via the middleware’s authResponseHeaders list. oauth2-proxy uses X-Auth-Request-* headers, not X-Forwarded-*. Map accordingly.
oauth2-proxy flags
Section titled “oauth2-proxy flags”oauth2-proxy must emit the auth-request headers and request the groups scope from Dex:
--set-xauthrequest=true--scope=openid email profile groups--pass-authorization-header=falseDex must include groups in its connector config (e.g. an LDAP connector with groupSearch, or GitHub connector with orgs).
Traefik Middleware
Section titled “Traefik Middleware”apiVersion: traefik.io/v1alpha1kind: Middlewaremetadata: name: oauth2-proxy-auth namespace: monitoringspec: forwardAuth: address: http://oauth2-proxy.monitoring.svc.cluster.local/oauth2/auth trustForwardHeader: true authResponseHeaders: - X-Auth-Request-User - X-Auth-Request-Email - X-Auth-Request-Preferred-Username - X-Auth-Request-GroupsApply the middleware to your chmonitor IngressRoute:
apiVersion: traefik.io/v1alpha1kind: IngressRoutemetadata: name: chmonitor namespace: monitoringspec: entryPoints: - websecure routes: - match: Host(`chmonitor.example.com`) kind: Rule middlewares: - name: oauth2-proxy-auth services: - name: chmonitor port: 3000chmonitor env vars
Section titled “chmonitor env vars”CHM_AUTH_PROVIDER=trustedVITE_AUTH_PROVIDER=trusted
## oauth2-proxy sends X-Auth-Request-* headers, not X-Forwarded-*CHM_TRUSTED_USER_HEADER=X-Auth-Request-UserCHM_TRUSTED_EMAIL_HEADER=X-Auth-Request-EmailCHM_TRUSTED_NAME_HEADER=X-Auth-Request-Preferred-UsernameCHM_TRUSTED_GROUPS_HEADER=X-Auth-Request-Groups
## Secret (store in a k8s Secret, inject as env; not plaintext here)CHM_TRUSTED_AUTH_SECRET=<long-random-secret>CHM_TRUSTED_SHARED_SECRET_HEADER=X-Chm-Proxy-Secret
## Gate access to the sre and admin groups onlyCHM_TRUSTED_ALLOWED_GROUPS=sre,adminThe proxy must set X-Chm-Proxy-Secret: <long-random-secret> on every forwarded request. In Traefik you can do this with a second middleware or a plugin that adds a static header from a Kubernetes Secret. Alternatively, run chmonitor as a ClusterIP only reachable by the oauth2-proxy pod and set CHM_TRUSTED_ALLOW_INSECURE=true instead of a shared secret.
nginx variant
Section titled “nginx variant”For nginx-ingress with auth-url / auth-snippet:
## In your server blocklocation / { auth_request /oauth2/auth; auth_request_set $auth_user $upstream_http_x_auth_request_user; auth_request_set $auth_email $upstream_http_x_auth_request_email; auth_request_set $auth_name $upstream_http_x_auth_request_preferred_username; auth_request_set $auth_groups $upstream_http_x_auth_request_groups;
proxy_set_header X-Forwarded-User $auth_user; proxy_set_header X-Forwarded-Email $auth_email; proxy_set_header X-Forwarded-Preferred-Username $auth_name; proxy_set_header X-Forwarded-Groups $auth_groups; proxy_set_header X-Chm-Proxy-Secret "<same-secret>";
proxy_pass http://chmonitor_upstream;}
location /oauth2/ { proxy_pass http://oauth2-proxy_upstream;}Or with nginx-ingress annotations:
nginx.ingress.kubernetes.io/auth-url: "https://oauth2-proxy.example.com/oauth2/auth"nginx.ingress.kubernetes.io/auth-response-headers: >- X-Auth-Request-User, X-Auth-Request-Email, X-Auth-Request-Preferred-Username, X-Auth-Request-Groupsnginx.ingress.kubernetes.io/configuration-snippet: | proxy_set_header X-Forwarded-User $http_x_auth_request_user; proxy_set_header X-Forwarded-Email $http_x_auth_request_email; proxy_set_header X-Forwarded-Preferred-Username $http_x_auth_request_preferred_username; proxy_set_header X-Forwarded-Groups $http_x_auth_request_groups; proxy_set_header X-Chm-Proxy-Secret "<same-secret>";Keep the default CHM_TRUSTED_*_HEADER names in this case (they match X-Forwarded-*).
Security notes
Section titled “Security notes”Header forgery. Without a trust gate, any client that can reach the chmonitor service directly can send arbitrary headers and forge any identity. Always:
- Use
CHM_TRUSTED_AUTH_SECRETwith a strong random value, or - Ensure the Service is network-isolated (k8s ClusterIP, not exposed externally) and set
CHM_TRUSTED_ALLOW_INSECURE=true.
Fail closed. If neither CHM_TRUSTED_AUTH_SECRET nor CHM_TRUSTED_ALLOW_INSECURE=true is configured, the trusted provider rejects all requests with 401. There is no open fallback.
Secret hygiene. Never put the secret value in YAML manifests, .env files, or source control. Use a k8s Secret (or wrangler secret put) and inject as an environment variable at runtime.
CHM_TRUSTED_ALLOW_INSECURE. Only use this when the Worker or container is genuinely unreachable except through the proxy. If the port is exposed on a node or load balancer, use the shared secret instead.
Combining with API keys
Section titled “Combining with API keys”CHM_API_KEY_SECRET works alongside CHM_AUTH_PROVIDER=trusted. Programmatic clients (MCP, scripts, CI) can authenticate with a chm_ Bearer token without going through the proxy:
CHM_API_KEY_SECRET=<secret>## mint a keycurl -X POST https://chmonitor.example.com/api/v1/auth/api-key \ -H "Authorization: Bearer <secret>"See API keys.