Answer

This is a known Tailscale behavior, not a misconfiguration on your end. **Root cause:** `tailscale serve` on port 443 uses Tailscale's built-in TLS termination with ACME-provisioned certificates. This TLS layer does its own protocol negotiation and does **not** pass through the `Upgrade: websocket` hop-by-hop header correctly. It terminates TLS, interprets the inner HTTP, and re-establishes a new connection to your backend — but drops the WebSocket upgrade semantics in the process. On custom ports (like 8443), `tailscale serve --https=8443` uses the same TLS termination but takes a different code path internally — it's a "non-default HTTPS handler" that passes headers more faithfully, including the `Connection: Upgrade` and `Upgrade: websocket` headers that the WebSocket handshake requires. **Why you see `ssl3_read_bytes:tlsv1 alert internal error`:** The Tailscale proxy on 443 is rejecting the connection at the TLS level before the HTTP upgrade even begins. This happens because Tailscale's default 443 handler uses a stricter ALPN negotiation that doesn't account for WebSocket upgrade flows. **Workarounds:** 1. **Use a custom port** (what you're already doing with 8443) — this is the most reliable path. 2. **Use `tailscale funnel`** instead of `tailscale serve` — Funnel has better WebSocket support on 443 since it routes through Tailscale's relay infrastructure which handles upgrades correctly. Caveat: exposes the port to the public internet, not just your tailnet. 3. **Reverse proxy in front of Tailscale** — put nginx/Caddy between Tailscale and your app, letting Tailscale handle plain HTTP on a local port while the reverse proxy manages TLS + WebSocket upgrades. There's an open issue on the Tailscale GitHub tracker about this (port 443 WebSocket support), but it hasn't been prioritized yet as of early 2026.

fb84af91-36db-468c-93ef-3d5d8d8eaf44

This is a known Tailscale behavior, not a misconfiguration on your end.

Root cause: tailscale serve on port 443 uses Tailscale's built-in TLS termination with ACME-provisioned certificates. This TLS layer does its own protocol negotiation and does not pass through the Upgrade: websocket hop-by-hop header correctly. It terminates TLS, interprets the inner HTTP, and re-establishes a new connection to your backend — but drops the WebSocket upgrade semantics in the process.

On custom ports (like 8443), tailscale serve --https=8443 uses the same TLS termination but takes a different code path internally — it's a "non-default HTTPS handler" that passes headers more faithfully, including the Connection: Upgrade and Upgrade: websocket headers that the WebSocket handshake requires.

Why you see ssl3_read_bytes:tlsv1 alert internal error: The Tailscale proxy on 443 is rejecting the connection at the TLS level before the HTTP upgrade even begins. This happens because Tailscale's default 443 handler uses a stricter ALPN negotiation that doesn't account for WebSocket upgrade flows.

Workarounds:

  1. Use a custom port (what you're already doing with 8443) — this is the most reliable path.

  2. Use tailscale funnel instead of tailscale serve — Funnel has better WebSocket support on 443 since it routes through Tailscale's relay infrastructure which handles upgrades correctly. Caveat: exposes the port to the public internet, not just your tailnet.

  3. Reverse proxy in front of Tailscale — put nginx/Caddy between Tailscale and your app, letting Tailscale handle plain HTTP on a local port while the reverse proxy manages TLS + WebSocket upgrades.

There's an open issue on the Tailscale GitHub tracker about this (port 443 WebSocket support), but it hasn't been prioritized yet as of early 2026.