Skip to content

v0.3.0 — tailnet identity binding, Serve port safety, proxy stream cap

Pre-release
Pre-release

Choose a tag to compare

@gl0bal01 gl0bal01 released this 21 Aug 14:59

Pre-release. The VPS test matrix in docs/VPS_TEST_RESULTS.md is still pending on both required rows, so v0.2.4 remains the recommended install until that evidence lands. This tag exists so the matrix runs have a pinned ref to test.

Upgrading breaks two things on purpose

  • Every client must re-pair. Session cookies are now bound to the tailnet identity that paired them, so a cookie stolen from one tailnet user is useless to another. Cookies issued before this carry no binding and are rejected. Run sudo pai-anywhere reset-access and pair again.
  • Your tailnet URL may gain a port. Serve no longer assumes 443. If another service already owns that port on the host, install falls back to 10000 and prints the URL with it. Update your ACL grant to match, or pin one with PAI_ANYWHERE_SERVE_PORT.

Pairing from a tagged node now returns 403, because Tailscale Serve stamps no user identity for tagged devices. See "Pairing returns 403" in docs/TAILNET_ACCESS.md for the diagnostic and the PAI_ANYWHERE_REQUIRE_TAILNET_IDENTITY=0 opt-out.

Fixes

  • install.sh aborted mid-install. A comment sat between a \ line continuation and its argument, truncating the runuser call so bash -lc ran with no script. Exit 127 under set -e; Claude Code never installed.
  • Browser pairing always returned 403. The CSRF guard compared full origins, but Serve terminates TLS and forwards plaintext, so the browser's https:// Origin never matched the rebuilt http:// request URL. Now compares host and port.
  • The proxy response cap capped nothing. It ran after arrayBuffer(), so a chunked response was fully buffered before any check, and a live stream never terminated at all. Bytes are now counted through the stream.
  • Off-site backup always aborted. The parent-directory guard used -perm /g-w, which find reads as /000 and matches every file. Now -perm /022.
  • The backup unit could not create its own directory under ProtectSystem=strict. The README creates it; the script reports the exact command if it is missing.
  • Tailscale Serve could black-hole an existing service. Serve intercepts tailnet traffic inside tailscaled before iptables, so claiming a port another proxy owns silently breaks it for every tailnet client while it keeps answering locally. Install now probes the host first and refuses to take an owned port.

Hardening

  • Per-source pairing rate limit (rate-limit.v2), keyed on tailnet identity only when Serve makes that header authoritative, socket address otherwise.
  • Sec-Fetch-Site: same-site no longer accepted — tailnet hosts share the ts.net registrable domain.
  • Hop-by-hop and Authorization headers stripped before the Pulse upstream.
  • /__gateway/healthz no longer names the service.
  • Constant-time pairing comparison over fixed-length digests.
  • Live secrets excluded from backup archives; reset-access required after restore.
  • pin-bot is manual dispatch only, and its pinned URL is derived from install.sh so the hash can never certify a different origin than the installer fetches.

Full changelog: v0.2.4...v0.3.0