M18-8: Operator auth via Authelia #169

Closed
opened 2026-04-20 16:03:03 +00:00 by code-lead · 0 comments
Collaborator

As an operator, I want the new UI and all mutating endpoints fronted by the existing Authelia instance (the same auth layer Forgejo already sits behind), so that the architect (which has host filesystem access and can dispatch any agent) is protected without rolling a homegrown token scheme.

Acceptance criteria

Proxy topology

  • The web app and mutating server endpoints are served through the existing Authelia-protected reverse proxy. New vhost (e.g. claude.jacquin.app) points at 192.168.1.164:4500
  • Authelia enforces the existing "operator" access-control rule — no new identity provider, no new user accounts
  • Remote-User / Remote-Groups headers set by Authelia are trusted by the server via a configured trust_proxy CIDR list; requests from outside that list have the headers stripped

Server-side gating

  • New middleware in apps/server extracts Remote-User from the trusted proxy and attaches it to the request context as req.user
  • Every /architect/* route plus mutating /agents/*, /task, /cancel, /breakdown require req.user to match the configured operator_user
  • /health, /events, /history, /queue, /stats, /usage, /storage, /agents (GET only) remain readable on LAN without Authelia — the monitor SSE needs them and they expose no secrets
  • config/agents.json::auth block: { "operator_user": "charles", "trust_proxy": ["10.0.0.0/24"] }. Startup refuses to boot if trust_proxy is empty when any mutating route is wired

UI

  • No in-app login page — Authelia handles it upstream. Hitting /app/* unauthenticated redirects to Authelia and back
  • Web app header shows the current operator username (read from a new GET /whoami that echoes req.user)
  • A "Logout" link points at Authelia's logout URL (configurable in agents.json)

Docs

  • README "Securing the web UI" section documenting:
    • The vhost + Authelia access-control rule (sample config)
    • How to set operator_user + trust_proxy in agents.json
    • How to verify the header chain end-to-end (curl -H 'Remote-User: charles' … from trusted proxy IP succeeds; from outside 403s)

Tests

  • Unit tests: mutating routes 403 when Remote-User is missing or mismatched; accept when it matches operator_user
  • Unit test: requests with Remote-User header from an untrusted origin are ignored (treated as unauthed)
  • Integration test: read-only routes still respond without the header

Out of scope

  • Direct token auth / API key scheme — explicitly rejected; if a non-browser client needs access, file a separate story
  • Multi-operator / team accounts — still single-operator
  • Per-route fine-grained RBAC beyond operator-can-do-everything / read-only-public-on-LAN

Dependencies

  • Blocks on #M18-2 (web app bootstrap).
  • Can parallel with #M18-5, #M18-6, #M18-7.
  • Blocks #M18-9 (sunset requires auth enforced).

References

  • Spec: specs/m18-ui-rewrite-and-architect.md §Story M18-8
As an operator, I want the new UI and all mutating endpoints fronted by the existing **Authelia** instance (the same auth layer Forgejo already sits behind), so that the architect (which has host filesystem access and can dispatch any agent) is protected without rolling a homegrown token scheme. ## Acceptance criteria ### Proxy topology - [ ] The web app and mutating server endpoints are served through the existing Authelia-protected reverse proxy. New vhost (e.g. `claude.jacquin.app`) points at `192.168.1.164:4500` - [ ] Authelia enforces the existing "operator" access-control rule — no new identity provider, no new user accounts - [ ] `Remote-User` / `Remote-Groups` headers set by Authelia are trusted by the server via a configured `trust_proxy` CIDR list; requests from outside that list have the headers stripped ### Server-side gating - [ ] New middleware in `apps/server` extracts `Remote-User` from the trusted proxy and attaches it to the request context as `req.user` - [ ] Every `/architect/*` route plus mutating `/agents/*`, `/task`, `/cancel`, `/breakdown` require `req.user` to match the configured `operator_user` - [ ] `/health`, `/events`, `/history`, `/queue`, `/stats`, `/usage`, `/storage`, `/agents` (GET only) remain readable on LAN without Authelia — the monitor SSE needs them and they expose no secrets - [ ] `config/agents.json::auth` block: `{ "operator_user": "charles", "trust_proxy": ["10.0.0.0/24"] }`. Startup refuses to boot if `trust_proxy` is empty when any mutating route is wired ### UI - [ ] No in-app login page — Authelia handles it upstream. Hitting `/app/*` unauthenticated redirects to Authelia and back - [ ] Web app header shows the current operator username (read from a new `GET /whoami` that echoes `req.user`) - [ ] A "Logout" link points at Authelia's logout URL (configurable in `agents.json`) ### Docs - [ ] README "Securing the web UI" section documenting: - The vhost + Authelia access-control rule (sample config) - How to set `operator_user` + `trust_proxy` in `agents.json` - How to verify the header chain end-to-end (`curl -H 'Remote-User: charles' …` from trusted proxy IP succeeds; from outside 403s) ### Tests - [ ] Unit tests: mutating routes 403 when `Remote-User` is missing or mismatched; accept when it matches `operator_user` - [ ] Unit test: requests with `Remote-User` header from an untrusted origin are ignored (treated as unauthed) - [ ] Integration test: read-only routes still respond without the header ## Out of scope - Direct token auth / API key scheme — explicitly rejected; if a non-browser client needs access, file a separate story - Multi-operator / team accounts — still single-operator - Per-route fine-grained RBAC beyond operator-can-do-everything / read-only-public-on-LAN ## Dependencies - **Blocks on #M18-2** (web app bootstrap). - **Can parallel with #M18-5, #M18-6, #M18-7.** - **Blocks #M18-9** (sunset requires auth enforced). ## References - Spec: `specs/m18-ui-rewrite-and-architect.md` §Story M18-8
Sign in to join this conversation.
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
charles/claude-hooks#169
No description provided.