Skip to content

Infrastructure

Zero-Ingress Deployment Platform

The home server could run things, but nothing could reach it. This is the layer that made it publishable: a real domain on real HTTPS, served out of a cupboard, without opening a single port on the home router. Every connection into the network is one the server itself started.

In production2026Cloudflare TunnelTailscalenginxZero trust
oscareustate.comPUBLIC INTERNETVisitorhttps://oscareustate.comCloudflare edgeTLS · CACHEhome IP never exposedoutbound onlyHOME NETWORKRouter0 INBOUND PORTSOLD MACBOOK · LINUX MINTcloudflaredoutbound tunnelnginx127.0.0.1:8080next start127.0.0.1:3000ssh · tailscale0 only

01 — The problem

What was actually happening

The usual way to host from home is to forward a port on the router and point DNS at your IP. It works, and it is also how home networks get found. A forwarded port is a permanent invitation published to every scanner on the internet, and it puts the machine — and everything else on that network — one software bug away from being someone else's.

  • Port forwarding advertises your home IP address and hands attackers a fixed target.
  • Exposing SSH to the internet means accepting a constant background of credential-stuffing attempts.
  • A residential IP changes, so DNS pointing at it needs babysitting or a dynamic-DNS workaround.
  • Anything given deploy access to the server can usually reach far more than the one project it needs.

02 — The solution

What I built, and why

Invert the direction of every connection. Instead of the internet reaching in, the server reaches out and holds the door open from the inside — so the router never needs an inbound rule and the home address is never published. Administration moves off the public internet entirely and onto a private network.

  • Cloudflare Tunnel: the server opens an outbound connection to Cloudflare, which terminates TLS at the edge and relays traffic back down it. The origin address stays private.
  • Tailscale as the administration plane — SSH is bound to the VPN interface, so the LAN address refuses connections outright.
  • The tailnet runs default-deny: a policy that grants only SSH and the music server, so nothing else on the box is reachable even from inside the VPN.
  • nginx in front of the application on loopback, adding cache rules for hashed build assets and the usual security headers.
  • pm2 under systemd for process supervision, so the application returns on its own after a power cut.
  • Deployment pulls from a private repository using a read-only key scoped to that one repository — a compromised server cannot reach anything else in the account.
  • ufw default-deny, fail2ban on the SSH jail, and unattended security upgrades underneath all of it.

03 — Process

How the work was sequenced

  1. 01

    Decide what must never be true

    Started from constraints rather than components: no inbound rules on the router, no administration reachable from the public internet, and no credential on the server that unlocks more than the single project it serves.

  2. 02

    Move administration off the internet

    Brought the machine onto a private VPN, then bound SSH to that interface and narrowed the firewall to match. Verified the VPN path worked before removing the local one, because the ordering is the difference between hardening a machine and losing it.

  3. 03

    Build the serving path inward

    Application on loopback, reverse proxy on loopback in front of it, and only then the tunnel — so that at no point in the build was anything listening on an address the network could reach.

  4. 04

    Separate the deploy identity

    Generated a key on the server itself and registered it as read-only against one repository, so the credential that lives on the exposed machine is the least useful one available.

  5. 05

    Audit the network instead of trusting it

    The router is an ISP gateway with no local administration interface, so rather than reading a settings page I queried its UPnP service directly and enumerated the live port-mapping table to confirm what was actually open.

  6. 06

    Pull the power

    Rebooted the machine and timed the recovery rather than trusting that services were marked as enabled. Two of them were not configured the way the status output implied, which only surfaced because of the reboot.

04 — Technology

The stack, as decisions

Edge & delivery

Cloudflare TunnelCloudflare DNSnginx

Private network

TailscaleTailnet ACLsufw

Runtime

Next.jspm2systemd

Delivery

GitPrivate repoRead-only deploy key

05 — Results

What it changed

Zero

Inbound ports open on the router, verified against its live mapping table

~100s

From cold reboot to serving traffic again, with nobody at the keyboard

One repo

The blast radius of the deploy credential on the server

The security of this setup comes from the direction connections travel, not from the amount of configuration in it — once every connection is outbound, a whole category of attack simply has nowhere to land. The other lesson was cheaper to learn here than in production: a service marked enabled is not a service that starts. Rebooting on purpose, while nothing depended on the machine yet, found two problems that would otherwise have waited for a power cut to appear.
What it taught

Have a problem worth solving?

Tell me what the process looks like today and where it breaks. If technology is the right answer, I will tell you what I would build — and if it is not, I will tell you that too.