feat: add resource access control with forwardAuth #3

Merged
ryan.mcgee merged 16 commits from feat/resource-access into main 2026-10-15 18:06:17 +00:00
Owner

Summary

  • Resource access control through Traefik's built-in forwardAuth: allow/deny rules, organization SSO, shared password, shared PIN, and share links
  • Stacked on #2 (feat/core-routing); this PR targets that branch so the diff shows only the access work. Retarget to main once #2 is merged.

Changes

  • Rules by IP, CIDR, path (exact or prefix) and method: first match by priority wins, allow skips sign-in, deny refuses. Paths are canonicalized before matching (percent-decoding, .., //, backslashes); a non-canonical path never matches an allow rule; deny rules ignore ASCII case
  • Visitor flow: protected request, redirect to the control plane's login, one-time exchange token (60 s, single-use via one conditional delete, bound to the resource) swapped for a host-only HttpOnly SameSite=Lax cookie on the resource's own domain
  • Sessions are re-checked on every request: SSO needs a current membership; changing a secret, disabling a method, removing a member or revoking a share link ends the matching sessions at once
  • Fail closed: a protected resource is never rendered without its middleware; with no usable auth address it is not served at all. The auth address carries an HMAC of the resource id so only nodes that fetched their config can call the endpoint
  • Redirect targets are parsed and rebuilt; only http(s) URLs whose host is exactly the resource's domain are accepted
  • Wrong secrets answer one message and are throttled per client and per resource (429)
  • SSO sessions pass Remote-User/Remote-Email to the backend; Traefik removes client-sent copies
  • Access page for admins (methods with write-only secrets, rules, share links shown once) and Protected badges; tenancy suite extended
  • New settings: TRAEFIK_CONTROL_AUTH_URL, TRAEFIK_CONTROL_TRUST_PROXY; README documents the model and its caveats

Verification

  • mix format --check-formatted, mix compile --warnings-as-errors, mix credo --strict (no issues), mix dialyzer (0 errors): pass on SQLite and PostgreSQL
  • mix test: 434 passed on SQLite and 434 on PostgreSQL
  • Real Traefik (compose): browser without credentials gets 302 to the login page, API client gets 401 with the login URL, /admin returns 403 including /%61dmin, //admin, /x/../admin and /ADMIN; forged Remote-* headers do not authenticate
  • Real Chrome through Traefik: password login (wrong password rejected, correct one lands on the requested page, no token left in the URL, cookie host-only/HttpOnly/SameSite=Lax); SSO via the control plane login and back, backend sees Remote-Email: dev@example.com; forged Remote-User/Remote-Email on a valid SSO session are replaced by the real identity; share link works without an account; revoking it ends the live session (302) and the link returns 404 while the SSO session is unaffected
  • Bugs found by running it and fixed: password/rule form text lingered after a successful submit (LiveView keeps typed values; inputs are now version-keyed, covered by a test)
## Summary - Resource access control through Traefik's built-in `forwardAuth`: allow/deny rules, organization SSO, shared password, shared PIN, and share links - Stacked on #2 (`feat/core-routing`); this PR targets that branch so the diff shows only the access work. Retarget to `main` once #2 is merged. ## Changes - Rules by IP, CIDR, path (exact or prefix) and method: first match by priority wins, allow skips sign-in, deny refuses. Paths are canonicalized before matching (percent-decoding, `..`, `//`, backslashes); a non-canonical path never matches an allow rule; deny rules ignore ASCII case - Visitor flow: protected request, redirect to the control plane's login, one-time exchange token (60 s, single-use via one conditional delete, bound to the resource) swapped for a host-only HttpOnly SameSite=Lax cookie on the resource's own domain - Sessions are re-checked on every request: SSO needs a current membership; changing a secret, disabling a method, removing a member or revoking a share link ends the matching sessions at once - Fail closed: a protected resource is never rendered without its middleware; with no usable auth address it is not served at all. The auth address carries an HMAC of the resource id so only nodes that fetched their config can call the endpoint - Redirect targets are parsed and rebuilt; only http(s) URLs whose host is exactly the resource's domain are accepted - Wrong secrets answer one message and are throttled per client and per resource (429) - SSO sessions pass `Remote-User`/`Remote-Email` to the backend; Traefik removes client-sent copies - Access page for admins (methods with write-only secrets, rules, share links shown once) and Protected badges; tenancy suite extended - New settings: `TRAEFIK_CONTROL_AUTH_URL`, `TRAEFIK_CONTROL_TRUST_PROXY`; README documents the model and its caveats ## Verification - `mix format --check-formatted`, `mix compile --warnings-as-errors`, `mix credo --strict` (no issues), `mix dialyzer` (0 errors): pass on SQLite and PostgreSQL - `mix test`: 434 passed on SQLite and 434 on PostgreSQL - Real Traefik (compose): browser without credentials gets 302 to the login page, API client gets 401 with the login URL, `/admin` returns 403 including `/%61dmin`, `//admin`, `/x/../admin` and `/ADMIN`; forged `Remote-*` headers do not authenticate - Real Chrome through Traefik: password login (wrong password rejected, correct one lands on the requested page, no token left in the URL, cookie host-only/HttpOnly/SameSite=Lax); SSO via the control plane login and back, backend sees `Remote-Email: dev@example.com`; forged `Remote-User`/`Remote-Email` on a valid SSO session are replaced by the real identity; share link works without an account; revoking it ends the live session (302) and the link returns 404 while the SSO session is unaffected - Bugs found by running it and fixed: password/rule form text lingered after a successful submit (LiveView keeps typed values; inputs are now version-keyed, covered by a test)
- resources gain SSO, password and PIN settings; secrets are Argon2
  hashes, never cast by the resource changeset or echoed in forms
- access rules (allow/deny by IP, CIDR, path prefix, method) with
  per-type validation and canonical values
- pure Decision.evaluate_rules/2: priority order, first match wins,
  IPv4/IPv6 and IPv4-mapped addresses, unknown client IP never matches
- Access context: admin-only writes, tenant-checked through the resource
- visitors log in on the control plane and receive a one-time exchange
  token (60 s, single-use via one conditional delete, bound to the
  resource) that is swapped for a per-resource session
- sessions are re-checked on every request: SSO needs a current
  membership, password/PIN/SSO stop when the method is disabled, and
  changing a secret revokes its sessions
- share links (256-bit token, hash only, optional expiry) whose
  sessions never outlive the link and die when it is revoked
- wrong secrets are throttled per client IP and per resource
- protected resources (an auth method or an enabled rule) get a
  forwardAuth middleware on their main router; the HTTPS redirect
  router stays open. A protected resource is never served without it:
  with no usable auth address, it is not rendered at all
- the auth address carries an HMAC of the resource id, so only nodes
  that fetched their config can call the endpoint; everything else 404s
- GET /api/traefik/v1/auth/:resource_id decides: rules first (deny
  beats a session), then a one-time exchange token (swapped for a
  host-only HttpOnly SameSite=Lax cookie and a clean redirect), then
  the session cookie, then the login page or a 401 for API clients
- paths are canonicalized before matching (percent-decoding, dot and
  empty segments, backslashes); non-canonical paths never match allow
  rules, and deny rules ignore ASCII case
- the client IP is the last X-Forwarded-For entry; the forwarded host
  must be the resource's domain, so redirects cannot leave it
- SSO sessions pass Remote-User and Remote-Email to the backend, which
  Traefik clears of client-supplied copies
- TRAEFIK_CONTROL_AUTH_URL overrides the derived auth base URL
- /auth/resource/:id offers the enabled methods (SSO, password, PIN);
  a successful login redirects back to the resource's own domain with
  a one-time exchange token that the auth endpoint turns into a cookie
- redirect targets are parsed and rebuilt: only http(s) URLs whose host
  is exactly the resource's domain are accepted (no userinfo, no
  backslashes or control characters), otherwise the domain root
- wrong secrets answer 401 with one message, throttled to 429; the
  client address is the connection's, or the last X-Forwarded-For
  entry only when TRAEFIK_CONTROL_TRUST_PROXY is set
- SSO sends anonymous visitors through the control plane's login and
  back; non-members are refused
- /share/:token redeems a share link (no-referrer, no-store)
- an end-to-end test drives the journey over HTTP as Traefik would
- /orgs/:org/resources/:id/access (admins only): sign-in methods with
  write-only secrets and per-method removal, rules with enable/disable
  and delete, share links shown once and revocable
- the page states clearly when a resource is public, and that share
  links only matter once it is protected
- protected badge on the resource list and detail pages; the Access
  link is shown to admins only
- tenancy suite covers the new page and the access surfaces: rules,
  links, settings, sessions, SSO, share links and auth signatures
  cannot cross tenants
- README: access control model, rule semantics, fail-closed behavior,
  caveats (session cookie reaches the backend, client IP seen by
  Traefik, share-link redirects), and the new settings
  TRAEFIK_CONTROL_AUTH_URL and TRAEFIK_CONTROL_TRUST_PROXY
- access page: version the form and input ids so a successful submit
  mounts fresh inputs; LiveView otherwise kept typed text, while a
  failed submit still keeps it
ryan.mcgee changed target branch from feat/core-routing to main 2026-10-15 18:06:06 +00:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
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
White_Ghost_Cyber_Systems/traefik-control!3
No description provided.