Zero inbound: a firewall that blocks the internet
Our production firewall has no rule for HTTP or HTTPS. There is no load balancer with a public listener and no ingress controller on an open port. Customer storefronts are still served all day.
The usual shape, and ours
Most production environments start from "the internet can reach us on 443" and then add layers in front: a WAF, a load balancer, rate limits. Those are sensible, but the starting point is still a host somewhere that accepts connections from anyone. Every packet a stranger sends is parsed by your network stack before any of those layers get a say.
We started from the other end. The cloud firewall in front of every node is default-deny inbound, applied at the provider's network edge, so unwanted packets are dropped before they reach a server at all. No web ports, no API ports, no database ports. The only exception is a single administrative path, locked to an allowlisted address.
How traffic gets in when nothing is listening
The trick is to reverse the direction of the connection. A small connector,
Cloudflare Tunnel (cloudflared), runs inside the cluster and
dials out to Cloudflare's edge. Visitors connect to Cloudflare, which terminates
their TCP and TLS sessions and passes the HTTP requests back down the tunnel that our side
opened.
- The origin never accepts a connection from the internet. The only sockets our servers deal with are ones they opened themselves.
- Origin addresses are irrelevant to an attacker. Knowing a node's IP gets you a firewall that drops everything.
- Internal tools ride the same tunnels. Dashboards and admin consoles are published by hostname, with identity-aware access policies in front of the sensitive ones, rather than exposed on ports.
Everything else pulls
Tunnels solve customer traffic. The harder part of getting to zero inbound is making sure nothing operational needs a way in either. A lot of platforms quietly keep a port open for deploys, CI or webhooks. Ours are all pull-based:
- Deploys — Flux runs inside the cluster and pulls the desired state from Git on an interval. Nothing pushes to the cluster. See GitOps on ARM.
- CI — build runners inside the cluster poll the CI server for jobs. No inbound job dispatch.
- Images and updates — container images, OS packages and Kubernetes upgrades are all fetched, never delivered.
- Alerts — notifications are pushed out to team chat. See Observability on a Budget.
It goes one step further on the worker nodes, which run the actual workloads: they are IPv6-only and have no public IPv4 address at all. Their outbound IPv4 goes through a NAT gateway, and their inbound is covered by the same default-deny firewall.
The firewall itself is code. It lives in OpenTofu alongside the servers it protects, so a change to it is a reviewed diff with a plan, not a click in a console that nobody remembers making.
Why it matters on CVE day
A large class of serious vulnerabilities shares one precondition: an attacker has to get packets to the vulnerable code. Bugs in the kernel's TCP/IP stack. Pre-authentication flaws in an SSH daemon or VPN appliance. A database, cache or dashboard that ships with a listening port and a weak default. Each year brings a handful of these with critical scores, and each one starts the same scramble: are we exposed, and how fast can we patch?
With zero inbound, the first question usually answers itself. The packets that would trigger the bug are dropped at the provider edge, so they never reach a node. The fix still goes in, but on the normal schedule: OS updates are applied automatically and nodes reboot one at a time in an overnight window. Nobody needs to patch at 2am.
It also fails closed. If a new workload opens a debug port, a service is
misconfigured as a NodePort, or a package starts a daemon nobody asked for, it
isn't exposed to the internet by accident. Being reachable is something you have to add
on purpose, and that is a change someone reviews.
What it doesn't do
Zero inbound removes a whole category of risk, not all of it. It's worth being plain about the limits:
- Application bugs still matter. HTTP requests still reach the application through the tunnel, so injection, auth and logic flaws are as relevant as ever.
- Outbound is not zero. A compromised dependency can still dial out. Pinned images, review and egress awareness still apply.
- The edge is a dependency. You are trusting the tunnel provider with your traffic and your availability. For us that trade is well worth it, but it is a trade.
- You still patch. Zero inbound buys you time and calm, not exemption. Defence in depth means the next layer is ready when one fails.
Getting there
You don't have to rebuild from scratch to get most of the benefit:
- Put an outbound tunnel connector in front of your web services and move DNS to it.
- Switch deploys and CI to pull-based agents, so nothing needs to reach in.
- Move admin tools behind identity-aware access, and lock any remaining management path to known addresses.
- Then flip the provider firewall to default-deny inbound, and keep that firewall in code.
The last step is the satisfying one. Once nothing depends on inbound connections, closing them all is a one-line change.