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_GROUPS is set, the user's forwarded groups must intersect (case-insensitive) or access is denied.
A trusted-authenticated user counts as authenticated for the feature permission matrix, which unlocks authenticated-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.