Octa Engineering Notes Christian Elías Request for Comments: OCTA-3 octa.page Category: Informational May 2026 tailscale serve sets REMOTE_ADDR to 127.0.0.1 for every request Abstract tailscale serve is a reverse proxy, not a network filter — it collapses all caller identities to 127.0.0.1, silently bypassing any REMOTE_ADDR-based auth guard. The only reliable identity signal is the Tailscale-User-Login header it injects. Status of This Memo This memo documents runtime notes for the Internet community. It does not specify an Internet standard of any kind. Distribution of this memo is unlimited. The canonical version lives at https://octa.page/doc/tailscale-serve-remote-addr/ and the machine-readable source at https://octa.page/doc/tailscale-serve-remote-addr.md "tailscale serve" terminates HTTPS at the Tailscale layer and forwards plain HTTP to a local backend. From the application's perspective, every request originates at "127.0.0.1" — regardless of which Tailscale node made the request. This collapses the identity of all callers to the loopback address. 1. Why it matters The typical pattern for Tailscale-gated PHP apps is to check "REMOTE_ADDR" against the Tailscale CGNAT range ("100.64.0.0/10") or to trust the "Tailscale-User-Login" header injected by "tailscale serve". These two signals are mutually exclusive: o If you check "REMOTE_ADDR" for Tailscale IPs → you get the real node IP, but "tailscale serve" is not in the picture (you're listening directly on the Tailscale interface or binding to the node's IP). o If you rely on "Tailscale-User-Login" → the proxy is in the picture, and "REMOTE_ADDR" is "127.0.0.1". A guard like this silently bypasses all auth when "tailscale serve" is the deployment method: $isLocal = in_array($_SERVER['REMOTE_ADDR'], ['127.0.0.1', '::1'], true); if ($isLocal) return; // exits before checking Tailscale-User-Login The "Tailscale-User-Login" branch is never reached. Every request is treated as a trusted local caller. 2. The fix Drop the "REMOTE_ADDR" shortcut entirely. When "tailscale serve" is the proxy, the only reliable identity signal is the injected header: function requireTailscaleAuth(): void { $login = $_SERVER['HTTP_TAILSCALE_USER_LOGIN'] ?? ''; if ($login !== 'allowed@example.com') { http_response_code(403); header('Content-Type: text/plain'); echo 'Forbidden'; exit; } } The "isLocal" escape hatch is often added for local development. It's unnecessary: dev environments using "php -S" route through a custom router that doesn't call the auth function, so removing the bypass doesn't break anything. 3. The underlying mechanic "tailscale serve" is a reverse proxy, not a network-layer filter. It listens on the Tailscale interface, handles TLS, injects identity headers, and makes a new outbound connection to "localhost:PORT". The application sees a fresh TCP connection from the loopback. There is no X-Forwarded-For or equivalent by default — the node IP is only available in the injected "Tailscale-User-*" headers. This is architecturally correct behavior for a proxy, but it breaks assumptions borrowed from traditional firewall-based access control where IP address implies identity. Christian Elías Informational [Page 1]