docs(self-host): say what any private network must supply, not just Tailscale - #531
docs(self-host): say what any private network must supply, not just Tailscale#531nedtwigg wants to merge 3 commits into
Conversation
…st meet SELF_HOST.md read as though Tailscale were a dependency. It is not — nothing in the server or the Host is Tailscale-aware, `DORMOUSE_ORIGIN` takes any absolute URL and `DORMOUSE_REMOTE_CONNECT_SRC` any source list. What Tailscale supplies is a front door: a stable DNS name, a certificate the phone already trusts, and a reverse proxy in front of the loopback server. Say that in the intro, and add "Bring your own private network" with the five rules another network has to satisfy — a DNS name rather than an IP (an IP literal is not a valid WebAuthn RP ID, so no passkey can be registered at all), a trusted certificate, reachability nothing checks for you now that the Funnel assertion has no generic equivalent, the loopback bind intact, and an origin stable for the life of the passkey — plus what is forfeited by leaving the scripted path. Tailscale stays the one supported path and the title still names it: the whole runbook body is Tailscale commands, and the installers refuse without the CLI. The word budget rises 7450 -> 8050; the file had five words of headroom. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UabNVcotSFB6R9WJpCxjmk
Cleanup pass over the previous commit. The contract said `DORMOUSE_ORIGIN` takes any absolute URL — `requireOrigin` demands an `http(s)` scheme — and the intro's "three things Tailscale supplies" contradicted the section's "all five", so a reader provisioning from the intro shipped an all-interfaces bind. The loopback and durable-origin bullets were third copies of invariants the Installer contract and `SECURITY.md` already own; they collapse into the one thing that is new off the scripted path, which is that nothing holds them for you. Two silent failures were missing and are now stated: an origin over 103 characters fails at pairing-QR mint time, and a non-443 origin must carry its port in `DORMOUSE_REMOTE_CONNECT_SRC`, since `parseSource` defaults a portless source to 443 and matches on exact equality. The IP-literal rule cited `server/src/app.ts` as though the server refused one; it does not — it boots and answers, and the failure appears in the phone's browser at passkey registration. The Headscale paragraph recommended a control plane nobody here has run. It now states that it is untested and names the unknown, along with the fact that makes guessing expensive: Serve requests the certificate only after the release switch has landed. Retitled from "Run the Dormouse server behind Tailscale", which contradicted the new framing on the one line every reader sees. The body still opens on the tailnet origin, so the scripted path is established immediately. Nothing referenced the old title; both lints key off the path. Net -106 words, and the budget ratchets 8050 -> 7975 rather than holding the slack the first commit claimed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UabNVcotSFB6R9WJpCxjmk
Deploying mouseterm with
|
| Latest commit: |
af1b09a
|
| Status: | ✅ Deploy successful! |
| Preview URL: | https://043cb42a.mouseterm.pages.dev |
| Branch Preview URL: | https://generic-vpn.mouseterm.pages.dev |
dormouse-bot
left a comment
There was a problem hiding this comment.
Two claims in the new section don't hold as written. Both are the same shape the section exists to prevent — a constraint an operator reads as satisfied, whose violation fails silently or late.
The 103-character cap bounds DORMOUSE_ORIGIN, not the DNS name. formatPairingInvitationUrl (server-lib-common/src/security/pairing-invitation.ts) builds ${origin}/#pair?${fragment} where the fragment is fixed at PAIRING_FRAGMENT_LENGTH = 146, so the throw at PAIRING_QR_URL_MAX_LENGTH = 256 leaves 256 − 153 = 103 for the whole origin string — https:// and any explicit port included. A 100-character DNS name reads as inside the budget under the bullet as written and mints a 108-character URL; the bullet's own "a non-default port is fine" makes it worse, since the port comes out of the same 103.
The state/ sentence is unqualified where SECURITY.md → "Credentials at rest" is not. That section says server/src/state.ts's 0o700/0o600 "buys nothing on Windows, where modes are a no-op" and that "what actually protects the installed self-host server's state is the installer's directory permissions". On the BYO path there is no installer, so a Windows operator who takes "the server still does its own half" at face value leaves state/hosts.json (a live hostToken) and vapid.json under whatever the parent directory hands down. run/ is also missing from the "yours" list.
Both suggestions together land the file at 7974 words against the 7975 budget, and pnpm lint:specs is green on them — I ran it. If you'd rather have the headroom back, the run/ mention in the second one is the cheapest thing to drop.
Everything else checked out against the code: requireOrigin really does check scheme and host only (an IP literal boots green), rpId = new URL(origin).hostname carries no port, parseSource defaults a portless secure source to 443 and matches exactly, all three installers die without the Tailscale CLI, bindHost defaults to undefined so nothing pins the loopback bind outside the installer, and no reference to the old title survives the rename.
Review findings, both verified against the code.
The 103-character cap bounds `DORMOUSE_ORIGIN` whole, not the DNS name:
`formatPairingInvitationUrl` builds `${origin}/#pair?${fragment}` over a fixed
146-character fragment, so the throw at 256 leaves 103 for scheme, host and
port together. A 100-character name read as inside the budget under the old
wording and mints a 108-character URL, and "a non-default port is fine" made it
worse by implying the port was free.
`state/` is not the server's own half everywhere: `state.ts`'s `0700`/`0600`
are a no-op on Windows, where the installer's DACL was the only thing holding
it — so on the BYO path a Windows operator owns `state/` too, holding a live
`hostToken` and the VAPID private key. `run/` was missing from the same list.
Budget 7975 -> 8000 rather than dropping the `run/` mention to fit; an unspent
enrollment offer redeems for a Host enrollment, so it belongs in the list.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UabNVcotSFB6R9WJpCxjmk
|
meh, maybe someday, not worth it rn |
SELF_HOST.mdread as though Tailscale were a dependency. It is not — nothing inserver/,lib/src/host/orserver-lib-common/is Tailscale-aware,DORMOUSE_ORIGINtakes anyhttp(s)origin, andDORMOUSE_REMOTE_CONNECT_SRCis a free-form source list. What Tailscale actually supplies is a front door: a stable DNS name, a certificate the phone already trusts, and a reverse proxy in front of the loopback server.The installers stay Tailscale-coupled, deliberately —
tailscale serveis all three of those things, and theSECURITY.mdFAIL IFlines thatscripts/deploy-lint.mjsholds them to are pinned to concrete Tailscale controls. This genericizes the framing only.What changed
## Bring your own private network— the contract a front door must meet, as four invariant bullets: a DNS name (never an IP, at most 103 characters), a certificate the phone already trusts, reachability no wider than that network, and the installer's loopback/durable-origin invariants held by hand. Then what is forfeited by leaving the scripted path.Constraints documented for the first time
Each of these fails silently today, and none was written down anywhere a self-hoster would look:
requireOriginchecks scheme and host only,createApptakes10.0.0.4asrpId, the server boots green, and the failure appears in the phone's browser at passkey registration.PAIRING_QR_URL_MAX_LENGTH).DORMOUSE_REMOTE_CONNECT_SRC—parseSourcedefaults a portless source to 443 and matching is exact equality.Notes for review
pnpm lint:specs,pnpm lint:deploy(lint + selftest +installer-verify-test) andgit diff --checkare green.ConfigErrorinrequireOriginrejecting IP-literal origins so this fails at boot instead of on the phone; adocs/specs/server.mdGuardrail pinning "never make the server or a Host Tailscale-aware", which is the invariant the whole new section rests on and which nothing currently guards; and rewordingSECURITY.md's accepted-risk premise from "tailnet-only" to the operator's private network.docs/specs/server.md. That spec owns the generic claim and has a rationale file to shed weight into, whichSELF_HOST.mdstructurally cannot (spec-lint.mjsglobsdocs/specs/*.rationale.mdonly) — but this is the file a self-hoster opens. The section is sized as though it can never shrink.🤖 Generated with Claude Code
https://claude.ai/code/session_01UabNVcotSFB6R9WJpCxjmk