Serving the Astro dev server over a Tailscale tunnel (and why the browser says 'Not Secure')
Got the blog’s Astro dev server loading on my phone over Tailscale, then dug into why the phone browser flags the connection “Not Secure.”
The setup (already in place, just verified it works)
astro.config.mjsalready hadserver: { host: true }(binds 0.0.0.0 / all interfaces, including the tailscale one) plus avite.server.allowedHostswhitelist populated from aTAILSCALE_HOSTenv var (the machine’s MagicDNS name). So a plainnpm run devis already tailnet-accessible — no--hostflag needed, becausehost: truein config does the same thing.- There’s a
scripts/dev-remote.sh(npm run dev:remote) that reads the MagicDNS host from.env, prints a banner + a scannable QR code (vianpx qrcode-terminal), then execsastro dev. It’s just the plain dev server plus convenience; the QR renderer only emits into a real TTY, so it comes out blank when run through a piped/non-interactive shell. - Verified reachability both by raw tailnet IP and by the MagicDNS hostname — both return 200 for
/journal. The Host header the phone sends is the MagicDNS name, which is why theallowedHostswhitelist matters (Vite rejects unknown Host headers otherwise). - Devices involved: this Mac, and my phone, both on the tailnet.
tailscale pingto the phone returned a pong via a direct peer path on UDP port 41641 (WireGuard’s port) — i.e. a direct hole-punched connection, not a DERP relay.
The real lesson: “Not Secure” is about the app layer, and Tailscale is NOT TLS
My first guess was that Tailscale “uses TLS between devices” and that’s why it’s fine. That conclusion (traffic is encrypted in transit) is correct, but the mechanism was wrong on three counts:
-
It’s WireGuard, not TLS. Tailscale’s data plane is WireGuard, which uses the Noise protocol framework (Curve25519 + ChaCha20-Poly1305), not TLS/SSL. TLS does appear elsewhere in Tailscale’s architecture — talking to the coordination/control server, and as the outer wrapper on DERP relays when a direct path can’t be established — but the encryption of the actual browser traffic between my two devices is WireGuard, end to end.
-
“SSL over HTTP” is inverted. HTTPS = HTTP over TLS (TLS being the successor to SSL). What I actually have is plain HTTP with no TLS layer at all — confirmed that port 4321 speaks plain HTTP and rejects an HTTPS/TLS handshake (no TLS listener there).
-
The browser can’t see Tailscale. To the phone’s browser the WireGuard tunnel is invisible OS-level plumbing. All it knows is: I made an
http://connection with no certificate → show “Not Secure.” That label is technically accurate about the application layer; it just has no way to know the bytes ride inside an encrypted WireGuard tunnel one layer down. The mental model:phone browser —plain HTTP—> [phone OS] —WireGuard/Noise encrypted (the wire)—> [Mac OS] —plain HTTP—> astro dev
Plaintext HTTP only exists within each device; across the network it’s WireGuard-encrypted. For a dev server on a private tailnet, the warning is cosmetic.
How to actually get the padlock (if I want it)
tailscale serve --bg 4321 fronts the dev server with a real Let’s Encrypt cert for the .ts.net name, giving genuine https:// (TLS + WireGuard stacked) — then browse to the MagicDNS host on 443 with no port. tailscale serve status to inspect, tailscale serve --https=443 off to tear down. Didn’t set this up yet; left it as an option.