feat(self-host): local console sign-in without Cloudflare Access - #106
feat(self-host): local console sign-in without Cloudflare Access#106b3nw wants to merge 2 commits into
Conversation
|
Codex review: needs real behavior proof before merge. Reviewed August 2, 2026, 7:10 PM ET / 23:10 UTC. ClawSweeper reviewWhat this changesThe branch adds opt-in local admin-token browser sessions for the Docker self-host console, including a login screen, revocable hashed cookies, and local session support for existing admin APIs. Merge readiness⛔ Blocked until stronger real behavior proof is added - 7 items remain Keep this PR open: current main still lacks local self-host console sign-in, but the proposed HTTPS reverse-proxy path has a blocking CSRF-origin defect. The shared check compares the browser’s external HTTPS Origin with the internally forwarded HTTP Worker URL, so legitimate local login and logout return 403. Priority: P2 Review scores
Verification
How this fits togetherThe self-host profile runs ClawRouter’s Worker without Cloudflare Access and currently relies on an admin bearer token for management. This PR adds a browser session layer so the dashboard and existing session-protected APIs can work for an explicitly enabled local deployment. flowchart LR
Operator[Self-host operator] --> Dashboard[Dashboard sign-in form]
Dashboard --> Login[Local login endpoint]
Login --> TokenCheck[Admin token verification]
TokenCheck --> SessionStore[Hashed session in policy storage]
SessionStore --> SessionCheck[Session resolution]
SessionCheck --> AdminAPIs[Dashboard and admin APIs]
Decision needed
Why: The implementation adds a durable authentication mode and environment configuration beyond the v0.1.0 API-only self-host posture; the prior self-host discussion anticipated this possibility but does not establish explicit maintainer acceptance. Before merge
Findings
Agent review detailsSecurityNeeds attention: The new local-session boundary fails closed but incorrectly rejects the intended HTTPS reverse-proxy request path. Review metrics
Root-cause clusterRelationship: Members:
Proposal only: this assessment does not dispatch repair, suppress jobs, mutate sibling items, close, or merge anything. Merge-risk optionsMaintainer options:
Copy recommended automerge instructionTechnical reviewBest possible solution: Preserve the opt-in default, repair same-origin validation for a trusted TLS-terminating proxy, add login/logout regression coverage including malicious-Origin rejection, and land the feature only if maintainers sponsor local admin-token sessions as a first-party self-host capability. Do we have a high-confidence way to reproduce the issue? Yes. Send an external HTTPS Origin with x-forwarded-proto set to https to an internally HTTP Worker request; the proposed same-origin comparison returns access_session_required before local login or logout. Is this the best way to solve the issue? No. The opt-in local-session approach is bounded, but it must reconstruct the externally visible proxy origin before it can safely support its documented deployment path. Full review comments:
Overall correctness: patch is incorrect AGENTS.md: found and applied where relevant. Codex review notes: model internal, reasoning high; reviewed against c4002ddb154e. LabelsLabel justifications:
EvidenceSecurity concerns:
Acceptance criteria:
What I checked:
Likely related people:
Rank-up movesOptional improvements that raise the rating; they are not merge blockers.
Rating scale
Overall follows the weaker of proof and patch quality. Workflow
HistoryReview history (31 earlier review cycles; latest 8 shown)
|
|
Addressed the review:
On the maintainer decision (default-on vs opt-in): both modes are implemented and covered by tests, and the entrypoint validates the flag at startup — flipping the default is a one-line change in |
…e Access Add an opt-in local-auth mode for the Docker/workerd profile: POST /v1/session/login exchanges the admin token for a rate-limited, KV-backed 12-hour session cookie (HttpOnly, SameSite=Lax) that verifiedAccessSession resolves alongside Cloudflare Access. Sessions honor live user-record state (disable, delete, or demote revokes); sign-in failures are capped per client and globally since client addresses are spoofable on a bare workerd host; the Secure cookie flag follows x-forwarded-proto for TLS-terminating proxies. The dashboard shell serves unauthenticated in local mode; logout revokes the stored session. Off by default: the self-host entrypoint validates CLAWROUTER_LOCAL_AUTH at startup and only activates on an explicit "enabled", and the Worker refuses local auth whenever Cloudflare Access variables are configured. The bearer-token admin fallback session is now labeled admin_token instead of cloudflare_access.
When the session bootstrap fails with access_session_required and the service index advertises /v1/session/login, the console renders a sign-in form instead of falling back to localhost demo data. Successful sign-in re-runs the normal refresh; the session cookie itself stays HttpOnly and invisible to the SPA.
|
Resolved the open decision from the contributor side by taking the conservative option: local sign-in is now opt-in ( |
|
Thanks @b3nw — this is careful work and the shape (opt-in, fail-closed, refused whenever 1. Origin validation fails on the documented secure deployment path (P1). Behind a TLS-terminating reverse proxy, the browser sends an external Direct works and the malicious cross-origin case is correctly rejected; the supported proxy case is the one that fails. A fix needs an explicit trusted external-origin contract (e.g. validating against the proxy-facing origin via 2. Brute-force caps are bypassable with concurrency (P2). The contract advertises 10/client and 50/global, but attempts are counted after awaiting request processing, so parallel requests all slip through: 100 concurrent wrong-token logins all returned 401; only the next sequential attempt saw 429. The reservation needs to happen atomically before the await. Both paths are green in the current tests, so coverage for them is the other gap: proxy login/logout, malicious-Origin rejection, and a concurrent-limiter test. Since this adds a new authentication mode, the final call is Peter's — I'm leaving it open with these findings rather than closing it. If you want to push a revision addressing the two items above plus the tests, that would be very welcome. |
|
Superseded by #113, which is now merged — thank you @b3nw, your commits Peter's call was explicit: "we absolutely need a way to self-host this and not use Cloudflare login." So the feature is in. #113 keeps your product boundary exactly as designed (opt-in, refused whenever Cloudflare Access is configured, KV-backed revocable sessions) and adds the hardening the review turned up:
Verified live against the real self-host image behind Caddy over TLS: sign-in 200 with |
Summary
Local console sign-in for the Docker self-host profile — the token-based login #104 anticipated ("could follow if demand materializes"; closes #105). Two focused commits, no new secrets:
worker/local-auth.ts—POST /v1/session/loginverifies the raw admin token against the existingCLAWROUTER_ADMIN_TOKEN_SHA256digest and mints a 256-bit opaque session token, stored SHA-256-hashed inPOLICY_KVwith a 12-hour TTL and delivered as anHttpOnly; SameSite=Laxcookie (Secureon https origins and behindx-forwarded-proto: httpsproxies).POST /v1/session/logoutrevokes. Failed sign-ins are capped per client and globally (client addresses are spoofable on a bare workerd host). The sign-in identity (CLAWROUTER_LOCAL_ADMIN_EMAIL, defaultadmin@local, validated at container startup) auto-provisions a user record, so policy bindings, usage attribution, and feat(access): self-service maintainer keys; fix(proxy): pre-stream upstream error normalization #103's self-service maintainer keys attach to a real principal — and sessions honor live record state: disabling, deleting, or demoting the user revokes/downgrades on the next request.verifiedAccessSessiondispatches Cloudflare Access first, then the local cookie — every session consumer (dashboard shell, playground,/v1/session/*, admin CSRF) works unchanged. The dashboard shell serves unauthenticated in local mode so the SPA can present sign-in; every API behind it stays session-gated.CLAWROUTER_LOCAL_AUTH=enabled(validated at container startup; without it the profile keeps the 0.1.0 API-only posture), and local auth is refused wheneverCLAWROUTER_ACCESS_*is configured, so a stray flag cannot open a login form on a managed deployment.access_session_requiredand/v1advertisessessionLogin(only emitted in local mode), the SPA renders a sign-in form instead of the localhost demo fallback; success re-runs the normal refresh. The cookie is never visible to page JS.admin_tokeninstead of the incorrectcloudflare_access(visible only in session echoes).Verification
worker:check(127 tests + typecheck; 10 new local-auth tests: cookie/session lifecycle, disabled/deleted/demoted-user revocation, expiry, CSRF, per-client and spoofed-address rate limiting, forwarded-proto Secure flag, Access-configured refusal),admincheck/test/build (26 tests), scripts tests incl. new entrypoint local-auth coverage,worker:e2e, and both Playwright projects (screenshot baselines unchanged)./dashboard/homegets the sign-in card; wrong token → inlinelogin_invalid; correct token → full admin console (admin@local, maintainer-keys panel live); cookieHttpOnly SameSite=Lax; reload keeps the session; logout revokes server-side; 11th bad attempt → 429 including for the correct token; cross-origin login → 403access_csrf_required; disabled mode → login 404, dashboard 401, previously-minted cookies rejected,/v1stops advertising login; bearer-token admin API unchanged.x-forwarded-protoSecure-cookie handling, startup validation of the admin email, and session-literal dedup. Per-isolate limiter state and the per-request user resolve (parity with the Access path) documented as accepted.Deployment status
No production impact. The Cloudflare deployment path is untouched — without
CLAWROUTER_LOCAL_AUTHthe new module is inert, and it hard-refuses to activate when Access variables exist. Self-host upgrades keep the API-only 0.1.0 posture unchanged; operators opt in withCLAWROUTER_LOCAL_AUTH=enabled.Remaining risk
ACCESS_CONTROLuser records are the natural phase 2 if wanted.POST /v1/session/logoutworks today.Live proof
Redacted transcripts from the self-host profile on local workerd (
wrangler dev --localwith the rendered self-host config, exactly as the container runs it). Admin token value redacted; the session cookie value is redacted 64-hex.Opt-in enabled (CLAWROUTER_LOCAL_AUTH=enabled) — sign-in lifecycle, protected APIs, logout, brute-force throttle
Browser drive — headless Chromium against /dashboard/home
Disabled (the default) — 0.1.0 posture retained