chmonitor
Authentication

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

How it works

  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.

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.

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

VariableDefaultDescription
CHM_AUTH_PROVIDERnoneSet to trusted. Canonical name (server + build).
VITE_AUTH_PROVIDERnoneAuto-derived from CHM_AUTH_PROVIDER at build time; only set explicitly to override.

Trust gate

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.

Identity headers

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.

Access control

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:

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.

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

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=false

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

chmonitor 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,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-*).

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.

On this page