Skip to content

feat(web): sign in from Tailscale Serve identity headers (phase 1) - #457

Merged
bbingz merged 1 commit into
mainfrom
feat/web-tailscale-identity-auth-20261008
Oct 8, 2026
Merged

bbingz merged 1 commit into
mainfrom
feat/web-tailscale-identity-auth-20261008

Conversation

@bbingz

@bbingz bbingz commented Oct 8, 2026

Copy link
Copy Markdown
Owner

Summary

Implements phase 1 of docs/superpowers/specs/2026-10-08-web-tailscale-identity-auth-design.md: the Web reader can sign in from the identity that the local tailscale serve proxy asserts instead of a shared credential.

  • EngramRemoteWebConfig: new AuthMode (credential(viewerDigest:editorDigest:) or tailscaleServe(viewers:editors:)), ENGRAM_REMOTE_WEB_AUTH (credential default, tailscale-serve opt-in), ENGRAM_REMOTE_WEB_VIEWERS / ENGRAM_REMOTE_WEB_EDITORS exact-login allowlists, five new ConfigError cases. Identity mode rejects any *_CREDENTIAL key.
  • EngramRemoteServerConfig: identity mode requires a loopback ENGRAM_REMOTE_HOST (the Serve proxy is the only legitimate source of the header).
  • WebAuthSessionStore: WebIdentity, login(identity:) (no attempt window; same capacity and collision guards), LoginResult.forbidden, per-session login, actor(sessionToken:). Each store answers .unavailable to the other mode's entry point.
  • WebRequestBoundary.tailscaleIdentity(in:): exactly one well-formed Tailscale-User-Login, read only in identity mode.
  • WebAuthRoutes: identity-mode POST /web/api/auth accepts only {} with the existing exact-Origin check (401 no header, 403 unlisted, 204 cookie); identity-mode GET /web/api/auth adds login. Credential-mode responses are unchanged.
  • WebUIRoutes.mount(tailscaleServeIdentity:): same HTML with <body data-auth-mode="tailscale-serve"> and the credential form shipped hidden; the script signs in silently on a 401 probe, shows Signed in as <login>, hides form and logout, re-mints once after an expired read with a five-second floor, and shows the not-permitted copy on 403.
  • Ledger entry "Web Reader and Editor Authority" rewritten to cover both modes; cutover-web-editor-hardening-1 annotated.
  • CHANGELOG.md / MEMO.md: the 2026-10-08 HQ status check, the old local Service CPU finding, and the owner-authorized Serve header pre-check (PASS on HQ, Tailscale 1.102.5).

Decisions made during implementation

  1. The UI learns the mode from the document attribute rather than from a 401 body, so credential-mode responses stay byte-identical and no request is spent on mode discovery.
  2. The identity check in the script is defensive (document.body && document.body.dataset) because tests/scripts/collector-web-ui.test.ts runs the shipped script in a fake DOM without body; the first draft failed 173 of its tests for that reason.
  3. The credential form is hidden at the HTML level in identity mode as well as by the script, so it cannot flash before the script runs and stays hidden on a 403.

Phase 2 (audit actor through the Web write envelope, replacing mcp) is a separate PR.

Tests

  • WebConfigTests: testAuthModeDefaultsToCredentialAndRejectsUnknownValues, testTailscaleServeModeParsesExactAllowlistsAndKeepsNoCredentialDigest, testTailscaleServeModeForbidsSharedCredentialsAndRequiresViewers, testIdentityAllowlistEntriesMustBeExactPrintableLoginsWithoutDuplicates.
  • WebAuthSessionTests: testIdentityLoginMintsViewerOrEditorByExactLoginAndRecordsActor, testIdentityLoginsIgnoreTheAttemptWindowButRespectCapacityAndExpiry, testEachStoreAnswersOnlyItsOwnModeAndCredentialSessionsHaveNoActor.
  • WebAuthRouteTests: testIdentityLoginMintsFromTheServeHeaderWithEmptyBodyAndExactOrigin, testIdentityLoginRejectsCredentialBodiesMissingDuplicateAndUnlistedLogins, testCredentialModeIgnoresForgedServeHeadersAndKeepsItsStatusBody, testIdentityViewerSessionCannotReachWriteRoutesEvenWithAnEditorHeader.
  • WebUIRoutesTests: testIdentityModeMarksTheDocumentAndTheScriptSignsInSilently.
  • WebServerIntegrationTests: testEnvironmentIdentityModeRequiresLoopbackBindAndNoCredential.
  • tests/scripts/collector-web-ui.test.ts: harness authMode option and four behavioral tests (silent sign-in, 403 copy with the form hidden, one re-mint with the five-second floor, credential mode never posts an identity login).

Validation

CHECKS_RUN:

  • Full EngramRemoteServerCore scheme on the final source (from macos/, xcodebuild ... build-for-testing then test-without-building, CODE_SIGNING_ALLOWED=NO): ** TEST EXECUTE SUCCEEDED **, 519 tests, 0 failures.
  • npx vitest run tests/scripts/collector-web-ui.test.ts: 180 passed; tests/scripts/invariants-ledger.test.ts: 12 passed.
  • npm run lint exit 0; bash scripts/check-swift-conventions.sh and bash scripts/check-swift-module-boundaries.sh exit 0.
  • Owner-authorized pre-check on HQ (recorded in the design doc): a loopback header-echo server published with tailscale serve --bg --https=8444, requested from HQ and from the Daily Mac with spoofed Tailscale-User-Login; both echoes carried exactly one Serve-asserted login and the spoofed values were removed; mapping removed afterwards.

CHECKS_NOT_RUN:

  • Real identity-mode end to end on HQ: needs the receiver package and plist change at deploy time.
  • The P3 planner's web-env-file check for identity-mode environments (design doc Q4): verify at deploy.

Adds ENGRAM_REMOTE_WEB_AUTH=tailscale-serve: the receiver mints the existing
hardened Web session from the Tailscale-User-Login header that the local
tailscale serve proxy asserts, matched against exact-login viewer/editor
allowlists (ENGRAM_REMOTE_WEB_VIEWERS / ENGRAM_REMOTE_WEB_EDITORS).
Credential mode stays the default and is byte-for-byte unchanged. Identity
mode forbids shared Web credentials, requires a loopback bind, accepts only
a `{}` login body with an exact Origin, and never reads the header in
credential mode. The page ships the credential form hidden, signs in
silently on a 401 probe, shows the login, and re-mints once after an
expired read with a five-second floor.

Design: docs/superpowers/specs/2026-10-08-web-tailscale-identity-auth-design.md
Ledger: "Web Reader and Editor Authority" rewritten for both modes.
Also records the 2026-10-08 HQ status check and the Serve header pre-check.
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 8, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-08T03:08:16.636888Z febdf9a PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: febdf9a615

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +272 to +273
if let web, web.usesTailscaleServeIdentity, !Self.isLoopbackBindAddress(host) {
throw EngramRemoteWebConfig.ConfigError.identityRequiresLoopbackBind

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Revalidate the loopback bind when constructing the app

This guard protects only configurations produced by fromEnvironment; EngramRemoteServerConfig also has a public initializer and publicly mutable host/web properties, while EngramRemoteServerApp later binds config.host without repeating this check. An embedding caller can therefore construct an identity-mode config with host: "0.0.0.0" (or mutate a validated config before app initialization), exposing the endpoint to peers that can forge Tailscale-User-Login and mint editor sessions. Reapply the identity-mode loopback validation in EngramRemoteServerApp.init, where the final configuration is consumed.

Useful? React with 👍 / 👎.

@bbingz
bbingz merged commit 1e3b5c3 into main Oct 8, 2026
23 of 25 checks passed
@bbingz
bbingz deleted the feat/web-tailscale-identity-auth-20261008 branch October 8, 2026 04:06
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant