Summary
A dashboard session's IP pin is inert behind every tunnel docs/guides/remote-and-mobile.md recommends — cloudflared, ngrok, and tailscale serve alike. This issue tracks the repair. The reporting of the state has landed (PR #1761); the pin itself has not been fixed.
Why it happens
The pin is bound from request.remote at the one bind site in the dashboard auth middleware. Every tunnel above runs on the gateway host and connects to the loopback-bound gateway from 127.0.0.1, so the address the session pins to is the tunnel process — identical for every client behind it.
Consequences, all live on main:
- A leaked session link is a bearer credential. Anyone who reaches the dashboard through the same tunnel satisfies the pin, for the life of the session — up to the 20-hour
MAX_SESSION_TTL_SECS, and kirocrew token defaults straight to 20h.
- The remote-access audit trail cannot name anyone. The SEL
log_api_access(caller=...) sites in the auth middleware all record request.remote, so every proxied request is attributed to 127.0.0.1.
This is not Tailscale-specific and not new. It is a property of "bind loopback + put a local proxy in front", which is the only remote-access shape the product documents.
What is already done
What is owed
Replace the address pin with a verified peer identity wherever one can be obtained from the local proxy, keeping the address pin as the fallback:
- session pin key generalises from
ip:<remote> to a peer key — ts:<login>@<node> when a peer resolves, unchanged ip:<remote> otherwise;
- resolution is daemon-verified, never header-trusted: loopback immediate peer and exactly one
X-Forwarded-For and tailscale whois confirming the address is a tailnet member and the resolved login agreeing with Tailscale-User-Login. Any miss falls back to the existing token path;
- a mandatory login allowlist — "any tailnet member" would be a silently-permissive default on a shared corporate tailnet;
- audit records name the resolved login as
caller when a peer resolved.
Tailscale is the first case because its local daemon can answer "who is this peer". Cloudflare Access (a locally verifiable signed JWT) is the natural second, and is the test of whether the seam generalises — but not before there is a second consumer.
Explicitly not proposed here
Widening is_direct_local_request(). A verified tailnet identity is not the local machine; config-write and secret-reveal endpoints stay closed to proxied requests.
Blocking questions
Both are open in the RFC and one needs a product call:
- Pin scope: node or login? Node is strictly stronger than today and costs a flow that does not work today either (one link, two devices — today's IP pin already refuses the second). Login survives a device's node identity changing. Note this choice only affects what happens after a session leaks; it is not the defence against a hostile tailnet member — the allowlist is.
- Windows. The
tailscale CLI location and the daemon's local API on Windows are unverified. If resolution is not reliable there, the repair is POSIX-only and must degrade to the token path rather than break.
Risk note
This is the only part of the RFC that touches the auth path. It should not land without adversarial review, and it needs explicit tests for X-Forwarded-For injection, multi-value X-Forwarded-For, header/whois disagreement, daemon-absent fallback, and timeout fail-closed.
Until it lands, PR #1761's posture row and guide text are the mitigation: they tell the user the truth so they can size their TTL and exposure accordingly.
Summary
A dashboard session's IP pin is inert behind every tunnel
docs/guides/remote-and-mobile.mdrecommends — cloudflared, ngrok, andtailscale servealike. This issue tracks the repair. The reporting of the state has landed (PR #1761); the pin itself has not been fixed.Why it happens
The pin is bound from
request.remoteat the one bind site in the dashboard auth middleware. Every tunnel above runs on the gateway host and connects to the loopback-bound gateway from127.0.0.1, so the address the session pins to is the tunnel process — identical for every client behind it.Consequences, all live on
main:MAX_SESSION_TTL_SECS, andkirocrew tokendefaults straight to20h.log_api_access(caller=...)sites in the auth middleware all recordrequest.remote, so every proxied request is attributed to127.0.0.1.This is not Tailscale-specific and not new. It is a property of "bind loopback + put a local proxy in front", which is the only remote-access shape the product documents.
What is already done
fix/session-pin-visibility) makes the state honest and visible: ais_proxied_loopback_request()predicate, observation latches in the auth module, a tri-state Security Posture row (not known yet/ per-client /SHARED, not per-client), and the guide correction across all three tunnel providers. It changes no binding, comparison, or auth decision.docs/request-for-change/rfc-tailnet-dashboard-access.md) designs the repair as its Phase 3.What is owed
Replace the address pin with a verified peer identity wherever one can be obtained from the local proxy, keeping the address pin as the fallback:
ip:<remote>to a peer key —ts:<login>@<node>when a peer resolves, unchangedip:<remote>otherwise;X-Forwarded-Forandtailscale whoisconfirming the address is a tailnet member and the resolved login agreeing withTailscale-User-Login. Any miss falls back to the existing token path;callerwhen a peer resolved.Tailscale is the first case because its local daemon can answer "who is this peer". Cloudflare Access (a locally verifiable signed JWT) is the natural second, and is the test of whether the seam generalises — but not before there is a second consumer.
Explicitly not proposed here
Widening
is_direct_local_request(). A verified tailnet identity is not the local machine; config-write and secret-reveal endpoints stay closed to proxied requests.Blocking questions
Both are open in the RFC and one needs a product call:
tailscaleCLI location and the daemon's local API on Windows are unverified. If resolution is not reliable there, the repair is POSIX-only and must degrade to the token path rather than break.Risk note
This is the only part of the RFC that touches the auth path. It should not land without adversarial review, and it needs explicit tests for
X-Forwarded-Forinjection, multi-valueX-Forwarded-For, header/whois disagreement, daemon-absent fallback, and timeout fail-closed.Until it lands, PR #1761's posture row and guide text are the mitigation: they tell the user the truth so they can size their TTL and exposure accordingly.