Cloudflare provides a Terraform Provider to automate nearly everything, including fetching the IP Ranges Cloudflare’s Reverse Proxy is coming from so you can put their IP ranges into a firewall to allow no other traffic.

I’ve got a cloud instance at Linode and Hetzner and wanted to configure their cloud firewalls so that only Cloudflare IP Addresses are allowed to connect to certain ports and no other addresses. The Cloudflare cloudflare_ip_ranges modules can be used to get the IP ranges used by Cloudflare to create firewall rules automatically.

Unfortunately, this requires that the Cloudflare Terraform Provider is configured and authenticated as well. The API Endpoint is available for the public without any authentication, however, for Terraform authentication is required and it probably makes sense if you have everything in code. The Cloudflare configuration could or should also be stored in Terraform then, so you’d need to configure the API anyway.

However, I don’t have my Cloudflare configuration in Terraform and don’t want to create another API token just to get a publicly available list of IP addresses. So let’s just take the Terraform http module to call the Cloudflare API and create a list of IP ranges for ourselves.

Here’s a GitHub gist with the code to use it.

You can then reference in your firewall configuration either local.cloudflare_ipv4_cidrs just for IPv4 ranges, or local.cloudflare_ipv6_cidrs for IPv6 ranges and local.cloudflare_ips_combined for IPv4 and IPv6 combined.

Hetzner for example takes IPv4 and IPv6 combined, Linode on the other hand separates IPv4 and IPv6. Here are examples for the two providers:

hetzner:

resource "hcloud_firewall" "cloudflare-only" {
name = "cloudflare-in"
rule {
direction = "in"
protocol = "icmp"
source_ips = [
"0.0.0.0/0",
"::/0"
]
}

rule {
direction = "in"
protocol = "tcp"
port = "443"
source_ips = local.cloudflare_ips_combined
}
}

linode:

resource "linode_firewall" "cloudflare_only" {
label = "cloudflare-in"

inbound {
label = "https"
action = "ACCEPT"
protocol = "TCP"
ports = "443"
ipv4 = local.cloudflare_ipv4_cidrs
ipv6 = local.cloudflare_ipv6_cidrs
}

inbound {
label = "icmp"
action = "ACCEPT"
protocol = "ICMP"
ipv4 = ["0.0.0.0/0"]
ipv6 = ["::/0"]
}

inbound_policy = "DROP"
outbound_policy = "ACCEPT"

linodes = [1234]
}