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:

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:

  1. Confirm Cloudflare proxies the domain name.
  2. Add CDN allow rules to the cloud firewall alongside existing rules.
  3. Test web applications and CLI commands through the CDN.
  4. 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.

← All articles