← Writing

Notes on a zero-inbound VPS

  • #infra
  • #tailscale
  • #cloudflare
  • #security

I run several small apps on a single Hetzner VPS — a few internal tools, a couple of public sites. The goal I landed on: zero open inbound ports. The machine should be invisible to a scanner and still serve everything.

The trick is that both access paths are outbound.

Two outbound doors

                    ┌─ Tailscale (outbound) ───────→ Caddy (bind 100.x:443) ──→ PRIVATE apps
VPS (0 ports in) ───┤
                    └─ Cloudflare Tunnel (outbound) → Caddy (127.0.0.1:8080) ──→ PUBLIC apps

Caddy stays the single internal router for both — it dispatches by host and terminates TLS for the private side (via DNS-01, since an HTTP-01 challenge can’t reach a 100.x address).

The firewall

With both doors outbound, the firewall becomes blunt and safe:

ufw default deny incoming
ufw default allow outgoing
ufw allow in on tailscale0   # critical — don't lock yourself out
ufw enable

The part nobody warns you about

You are sawing the branch you sit on. If the change that closes inbound also kills the channel you’re connected through, you’re stranded.

So two rules I now treat as non-negotiable:

  1. Keep an out-of-band break-glass. A web console (Hetzner’s, in my case) that survives the firewall and the tailnet both going down.
  2. Make the risky cutover self-healing. Apply, health-check, and auto-rollback if the check fails — don’t flip the switch blind.

A scanner sees nothing. I still reach everything. That’s the whole idea.