Skip to content

A Tailscale-User-Login header is trusted from any proxy on this PC, not only tailscale serve #253

Closed

Description

@devswha

What happens

On a PC where Tailscale runs, the server trusts a Tailscale-User-Login header from any connection that comes from this PC. tailscale serve sets that header for your own devices. Any other reverse proxy on the same PC (nginx, Caddy, a tunnel client) passes a visitor's own copy of the header through unless it is told to drop it, and a visitor who sends the owner's login gets full access. A token does not stop it: the login is checked before the token is asked for (decideAccess in server/access.ts).

It needs three things together: Tailscale logged in on the PC, a second proxy on that PC that faces other people, and a visitor who knows the owner's Tailscale login.

Seen

With real nginx and Caddy in front of a test upstream: a request carrying Tailscale-User-Login: owner@example.com arrives upstream with the header intact unless the proxy config removes it.

Now

The guide's proxy examples drop the header (#252). The server itself still trusts it from any loopback proxy.

Ideas for the server

  • Trust the header only when the request also looks like tailscale serve: a single X-Forwarded-For address inside Tailscale's ranges (100.64.0.0/10, fd7a:115c:a1e0::/48). A proxy that appends or overwrites that header then fails the test. Needs checking against a real tailscale serve first, since a mistake here locks the owner's phone out until it is paired.
  • With a token set, ask for the token whatever the header says.

Activity

  1. rabbitson87 commented on Oct 3, 2026

    @rabbitson87
    Contributor

    Some findings on the first idea, from Tailscale's source (ipn/ipnlocal/serve.go on main). I haven't checked them against a live tailscale serve.

    What tailscale serve sends. Its proxy is an httputil.ReverseProxy with Rewrite, so Go strips any Forwarded and X-Forwarded-* a visitor sent before serve adds its own.

    • addProxyForwardedHeaders sets X-Forwarded-For to the one source address of the tailnet peer, plus X-Forwarded-Host, and X-Forwarded-Proto: https over TLS.
    • addTailscaleIdentityHeaders deletes incoming Tailscale-User-*, Tailscale-Funnel-Request and Tailscale-Headers-Info. It sets the login only for a non-tagged peer that WhoIs resolves, together with Tailscale-Headers-Info: https://tailscale.com/s/serve-headers.

    So requiring exactly one X-Forwarded-For address in 100.64.0.0/10 or fd7a:115c:a1e0::/48 should not lock out tailscale serve users.

    What that test stops, and what it doesn't:

    • nginx ($proxy_add_x_forwarded_for appends the real address, $remote_addr replaces it), Caddy, and Portal's --http-route (Go's Rewrite drops the visitor's copy and sets the relay's address): the test fails, so a forged login is refused.
    • A raw TCP tunnel to the port (a bare portal expose 7317, ssh -R, socat, frp in TCP mode): the visitor writes the whole request, so they can send X-Forwarded-For: 100.x.y.z along with the login and pass. No header test can tell that apart from serve, so the idea narrows the no-token case but doesn't close it.
    • A proxy listening on the tailnet interface with $remote_addr passes a tailnet peer's own address, so on a shared tailnet another member could still send the owner's login.

    A way to close it fully, if you want one: tailscale serve can proxy to a Unix socket (tailscale serve unix:/path/app.sock, Linux, macOS and BSD). The server could listen on a socket that only tailscale serve uses, and honor the identity headers only there, so no other proxy on the PC can produce them. Two catches: serve sends Host: localhost to a socket backend (serve.go:895), so on that listener the origin check would need X-Forwarded-Host; and the installer's serve command changes. So it's your call.

    On the Portal side, Portal's main now has --strip-request-header (gosuda/portal-tunnel#564), so after its next release Portal users can drop the header themselves.

  2. added a commit that references this issue on Oct 14, 2026
    79562d1
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions