Skip to content

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.

You haveUse
oauth2-proxy + Dex (or any OIDC provider)trusted provider
Authelia / Authentik in front of chmonitortrusted provider
Traefik ForwardAuth or nginx auth_requesttrusted 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:

  • proxy is Cloudflare Access-centric (verifies a Cf-Access-Jwt-Assertion JWT) or carries a bare subject via a single identity header.
  • trusted extracts 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.
  1. The upstream proxy authenticates the user and copies their identity into request headers before forwarding to chmonitor.
  2. 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.
  3. If the gate passes, chmonitor reads the configured header names to build a principal: subject, email, display name, avatar URL, and groups/roles.
  4. Optional group gating: if CHM_TRUSTED_ALLOWED_GROUPS is set, the user’s forwarded groups must intersect (case-insensitive) or access is denied.
  5. 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.

You must configure exactly one of these. Without a trust gate, the provider fails closed and denies all requests.

VariableDefaultDescription
CHM_TRUSTED_AUTH_SECRETShared secret. The proxy must send it in CHM_TRUSTED_SHARED_SECRET_HEADER. Constant-time compared. Recommended.
CHM_TRUSTED_ALLOW_INSECUREfalseWhen 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_HEADERX-Chm-Proxy-SecretHeader name carrying the shared secret.

Set the secret out-of-band — never in plaintext env files committed to source control:

Terminal window
wrangler secret put CHM_TRUSTED_AUTH_SECRET
## or
kubectl create secret generic chm-trusted-secret \
--from-literal=value=<secret>
VariableDefaultDescription
CHM_AUTH_PROVIDERnoneSet to trusted. Server-side.
VITE_AUTH_PROVIDERnoneSet to trusted. Build-time client mirror. Must match CHM_AUTH_PROVIDER.
VariableDefaultDescription
CHM_TRUSTED_AUTH_SECRETShared secret value (runtime secret).
CHM_TRUSTED_ALLOW_INSECUREfalseSkip secret check (network-isolation required).
CHM_TRUSTED_SHARED_SECRET_HEADERX-Chm-Proxy-SecretHeader name the proxy sends the secret in.
VariableDefault headerWhat it populates
CHM_TRUSTED_USER_HEADERX-Forwarded-UserSubject / user id. Falls back to the email header. Required for a usable identity.
CHM_TRUSTED_EMAIL_HEADERX-Forwarded-EmailEmail address.
CHM_TRUSTED_NAME_HEADERX-Forwarded-Preferred-UsernameDisplay name shown in the sidebar.
CHM_TRUSTED_AVATAR_HEADERX-Forwarded-AvatarAvatar URL.
CHM_TRUSTED_GROUPS_HEADERX-Forwarded-GroupsComma- or space-separated group/role list.
CHM_TRUSTED_ROLE_HEADERX-Forwarded-RoleSingle role value; merged into the groups list.
CHM_TRUSTED_CUSTOM_HEADERSExtra claims. Comma-separated field:Header-Name pairs, e.g. team:X-Forwarded-Team,dept:X-User-Dept. Available in the principal’s custom claims.
VariableDefaultDescription
CHM_TRUSTED_ALLOWED_GROUPSComma-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:

Terminal window
CHM_FEATURE_AGENT_ACCESS=authenticated # agent requires sign-in
CHM_FEATURE_SETTINGS_ACCESS=authenticated
CHM_TRUSTED_ALLOWED_GROUPS=sre,ops,admin

A trusted-authenticated user satisfies authenticated, so CHM_FEATURE_*_ACCESS=authenticated features are unlocked for them automatically.

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-Usersubject
X-Forwarded-Emailemail
X-Forwarded-Preferred-Usernamename
X-Forwarded-AvataravatarUrl
X-Forwarded-Groupsroles[]
X-Forwarded-Rolemerged 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 must emit the auth-request headers and request the groups scope from Dex:

--set-xauthrequest=true
--scope=openid email profile groups
--pass-authorization-header=false

Dex must include groups in its connector config (e.g. an LDAP connector with groupSearch, or GitHub connector with orgs).

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-Groups

Apply 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: 3000
Terminal window
CHM_AUTH_PROVIDER=trusted
VITE_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,admin

The 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-*).

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_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.

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.

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:

Terminal window
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.