# 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

| 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](/authentication/cloudflare-access) |
| nginx + a shared-secret header only (no full profile) | [`proxy` provider / trusted-header](/authentication/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.

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

```bash
wrangler secret put CHM_TRUSTED_AUTH_SECRET
## or
kubectl create secret generic chm-trusted-secret \
  --from-literal=value=<secret>
```

## All environment variables

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

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

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

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

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

```yaml
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:

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

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

## nginx variant

For nginx-ingress with `auth-url` / `auth-snippet`:

```nginx
## 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:

```yaml
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-*`).

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

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

```bash
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](/authentication/api-keys).

## Related

- [Authentication overview](/authentication)
- [Cloudflare Access](/authentication/cloudflare-access)
- [Trusted header (proxy provider)](/authentication/trusted-header)
- [API keys](/authentication/api-keys)
- [Feature Permissions](/advanced/feature-permissions)
- [Environment Variables — Authentication](/reference/environment-variables#authentication)
