Reverse proxy: trusted forwarded headers
Authenticate chmonitor via forwarded headers from oauth2-proxy, Authelia, Traefik ForwardAuth, or similar — full user profile with group-based access gating.
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
| 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
- 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.
Prerequisites
A proxy that authenticates users and forwards their identity as HTTP headers (oauth2-proxy, Authelia, Traefik ForwardAuth, nginx auth_request, or similar), plus a trust gate — see below.
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 put CHM_TRUSTED_AUTH_SECRET in YAML manifests, .env files, or source control. Use a k8s Secret (or wrangler secret put) and inject as an environment variable at runtime.
wrangler secret put CHM_TRUSTED_AUTH_SECRET
kubectl create secret generic chm-trusted-secret \
--from-literal=value=<secret>Setup
Provider activation
| Variable | Default | Description |
|---|---|---|
CHM_AUTH_PROVIDER | none | Set to trusted. Canonical name (server + build). |
VITE_AUTH_PROVIDER | none | Auto-derived from CHM_AUTH_PROVIDER at build time; only set explicitly to override. |
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
| 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
| 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-in
CHM_FEATURE_SETTINGS_ACCESS=authenticated
CHM_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
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> |
Proxy examples
Map oauth2-proxy's X-Auth-Request-* headers
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. 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:
apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
name: oauth2-proxy-auth
namespace: monitoring
spec:
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/v1alpha1
kind: IngressRoute
metadata:
name: chmonitor
namespace: monitoring
spec:
entryPoints:
- websecure
routes:
- match: Host(`chmonitor.example.com`)
kind: Rule
middlewares:
- name: oauth2-proxy-auth
services:
- name: chmonitor
port: 3000chmonitor env vars:
CHM_AUTH_PROVIDER=trusted
# oauth2-proxy sends X-Auth-Request-* headers, not X-Forwarded-*
CHM_TRUSTED_USER_HEADER=X-Auth-Request-User
CHM_TRUSTED_EMAIL_HEADER=X-Auth-Request-Email
CHM_TRUSTED_NAME_HEADER=X-Auth-Request-Preferred-Username
CHM_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 only
CHM_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.
For nginx-ingress with auth-url / auth-snippet:
# In your server block
location / {
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-Groups
nginx.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-*).
Verify
Sign in through the proxy, then confirm your identity and profile at GET /api/v1/auth/me and in the sidebar user menu. A direct request to the Service — bypassing the proxy, and therefore missing the shared-secret header (unless CHM_TRUSTED_ALLOW_INSECURE=true) — must be rejected with 401.
Troubleshooting
Header forgery risk
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_SECRET with a strong random value, or ensure the Service is network-isolated (k8s ClusterIP, not exposed externally) and set CHM_TRUSTED_ALLOW_INSECURE=true.
Combine 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 key
curl -X POST https://chmonitor.example.com/api/v1/auth/api-key \
-H "Authorization: Bearer <secret>"See API keys.
Related
Authentication overview
Compare all auth providers and the two-layer model.
Cloudflare Access
Zero Trust JWT verification under the proxy provider.
Trusted header (proxy provider)
Bare identity subject via shared secret, no full profile.
API keys
Add chm_ Bearer tokens for programmatic access.
Feature permissions
Gate individual features to authenticated users.
Environment variables — Authentication
Every CHM_* auth variable and its default.
Reverse proxy: trusted header
Authenticate chmonitor via a reverse proxy identity header and shared secret — works with nginx, Kubernetes ingress, and any SSO auth_request setup.
Feature Permissions
Hide or protect specific dashboard sections per-feature using config files or environment variables, without changing application code.