unix / firewall
I front my web applications and CI servers with Cloudflare. Cloudflare provides DNS, a CDN, and a Web Application Firewall.
My servers run on virtual machines with public IPv4 and IPv6 addresses. The virtual machines in one region share one cloud firewall, so one rule change reaches every one of them.
The origin bypass problem
A CDN protects a public hostname. If the cloud firewall accepts connections from any address, a client can bypass the CDN. An automated scanner finds the origin IP address in certificate transparency logs or network scans.
Direct connections to port 80 or 443 skip edge security rules and consume origin resources.
Restricting ingress to CDN ranges
I lock down the network perimeter with cloud firewall rules. I accept connections on ports 80 and 443 only from Cloudflare IP ranges:
- IPv4 ranges (15 ranges)
- IPv6 ranges (7 ranges)
Two ports across 22 ranges require 44 rules.
The virtual machines also reach each other over their private subnet, without the CDN. CI runners call the CI server on 443 there, with the server's public name, so they still check its certificate. Each other service port, such as Postgres or an internal API, accepts connections only from the private subnet.
Automating firewall rules
I automate firewall synchronization with a small Go program: ubifw.
The tool reads a declarative JSON configuration and fetches IP ranges
from Cloudflare:
{
"firewall": "default-firewall",
"cloudflare_ports": ["80", "443"],
"rules": [
{ "cidr": "0.0.0.0/0", "port_range": "22", "description": "SSH" },
{
"cidr": "10.0.0.0/26",
"port_range": "443",
"description": "private subnet HTTPS"
},
{
"cidr": "10.0.0.0/26",
"port_range": "5432",
"description": "Postgres, private subnet only"
}
]
}
The tool adds one rule for each Cloudflare range and each port in
cloudflare_ports. It compares the desired rules with the live
firewall rules through the cloud API. It supports dry-run, apply, and
prune phases.
Before changing live rules, ubifw writes a local JSON rollback
record. It re-reads the firewall to confirm added rules exist before
deleting obsolete rules.
Safe rollout order
To prevent downtime, I roll out firewall rules in order:
- Confirm Cloudflare proxies the domain name.
- Add CDN allow rules to the cloud firewall alongside existing rules.
- Test web applications and CLI commands through the CDN.
- Delete universal allow rules for each port.
Verifying with curl
Test domain access through the CDN:
curl -sI https://example.com/login
This request returns 200.
Test direct origin access:
curl -sk --max-time 5 --resolve example.com:443:198.51.100.1 https://example.com/login -I
This direct request times out.
Let's Encrypt renewals
Caddy provisions and renews TLS certificates via Let's Encrypt using HTTP-01 challenges on port 80. Cloudflare forwards ACME challenge requests to the origin.
I keep port 80 open to Cloudflare IP ranges on servers that renew certificates. Caddy serves ACME challenges and HTTPS redirects on port 80. It serves no application data on that port.