you are viewing a single comment's thread
view the rest of the comments
[–] [B] 2 points 3 weeks ago

Nice writeup — this is basically the standard pattern for "hide home IP but keep TLS on your own hardware", and the PROXY protocol reasoning is spot on: nginx can receive proxy_protocol but can't send it, so HAProxy (or stunnel, or traefik with proxyProtocol on the entrypoint) has to sit at the edge.

A few notes for anyone following along:

  • mode tcp + send-proxy is correct here since the backend terminates TLS. Don't be tempted by HTTP mode on the VPS — that means decrypting there, which defeats the stated goal.
  • nginx receiving side: the listener needs listen 443 ssl proxy_protocol; plus set_real_ip_from <tailscale-ip>; real_ip_header proxy_protocol;. Gotcha: once a listener is proxy_protocol-only, direct connections to that socket break — don't mix proxied and direct clients on the same listen block.
  • Health checks: check against a tcp-mode backend only tests the TCP handshake; option httpchk makes HAProxy actually verify nginx is serving (still fine through the tunnel).
  • Alternatives & tradeoffs: Cloudflare Tunnel gets the same "no open ports" result but terminates TLS at CF and rewrites client IP into CF headers (fail2ban then needs real_ip on those instead). Plain WireGuard without Tailscale also works and is fully self-hosted — the tailnet mainly saves key management, and the Codeberg-OIDC trick is a nice touch for avoiding GAFAM identity.
  • MTU/keepalive: if large uploads stall later, check tailscale's 1280 MTU vs any container veth MTU mismatch on the home server side.

The listen block shortcut makes sense for single-backend setups; frontend/backend split only pays off when routing multiple hostnames, at which point SNI-based routing (req.ssl_sni) in tcp mode lets you do it all without ever decrypting.

  • source