Fortigate + CloudFlare Address Lists via Terraform

Maintaining a list of allowed CIDR blocks from Cloudflare requires effort. These lists do not change often but would it not be nicer to automate it for your Fortigate firewall? I think so, so here’s an example set of files and code to maintain the list of address and address group objects for both IPv4 and IPv6.

Let’s just dive in as this is relatively straight forward.

If you follow this guide verbatim you will end up with a list of addresses for both IPv4 and IPv6 (many) and 2 address groups. You can then use the new address groups to allow access through your firewall rules for ports for HTTP and HTTPS while blocking the greater internet from accessing same ports to your servers.

This requires:

  • Terraform 0.13 or above (recommend 1.x or higher)
  • Cloudflare provider: 3.0 or above
  • FortiOS provider: 1.7 or above

Optional:

  • FORTIOS_ACCESS_HOSTNAME environment variable set to your remote host
  • FORTIOS_ACCESS_TOKEN environment variable set to your access token

We need to pull in the providers for both Cloudflare and FortiOS:

provider "fortios" {
  # better to use ENV variables than hard coding in your config (see above)
  # hostname = <remote name or IP>
  # token = <authorization token>

  # this allows you to connect without validating the TLS certificate
  insecure = true
}

terraform {
  required_providers {
    fortios = {
      source  = "fortinetdev/fortios"
      version = "~> 1.7"
    }
    cloudflare = {
      source  = "cloudflare/cloudflare"
      version = "~> 3.0"
    }
  }
  required_version = ">= 1.0"
}

Next we need to use a data call to Cloudflare to return the address list objects:

data "cloudflare_ip_ranges" "cloudflare_address_list" {}

Create resources for the addresses:

Note: using count to iterate over the list of entries in the object(s) returned

resource "fortios_firewall_address" "cloudflare_ipv4_block" {
  count  = length(data.cloudflare_ip_ranges.cloudflare_address_list.ipv4_cidr_blocks)
  name   = "CloudFlare IPv4 ${count.index}"
  type   = "ipmask"
  subnet = data.cloudflare_ip_ranges.cloudflare_address_list.ipv4_cidr_blocks[count.index]
}

resource "fortios_firewall_address6" "cloudflare_ipv6_block" {
  count = length(data.cloudflare_ip_ranges.cloudflare_address_list.ipv6_cidr_blocks)
  name  = "CloudFlare IPv6 ${count.index}"
  type  = "ipprefix"
  ip6   = data.cloudflare_ip_ranges.cloudflare_address_list.ipv6_cidr_blocks[count.index]
}

Create resources for the address groups:

Note: using for_each to iterate over the list of entries in the resources created

resource "fortios_firewall_addrgrp" "cloudflare_ipv4_group" {
  name = "CloudFlare IPv4 Group"

  dynamic "member" {
    for_each = fortios_firewall_address.cloudflare_ipv4_block[*].name
    content {
      name = member.value
    }
  }

  depends_on = [
    fortios_firewall_address.cloudflare_ipv4_block
  ]
}

resource "fortios_firewall_addrgrp6" "cloudflare_ipv6_group" {
  name = "CloudFlare IPv6 Group"

  dynamic "member" {
    for_each = fortios_firewall_address6.cloudflare_ipv6_block[*].name
    content {
      name = member.value
    }
  }

  depends_on = [
    fortios_firewall_address6.cloudflare_ipv6_block
  ]
}

And we’re done!

If you look at your Fortigate firewall you should see additional address objects for both IPv4 and IPv6 subnets and 2 new access groups that you can use in your firewall rules.

Going forward, as Cloudflare adds and/or removes addresses, you can just run Terraform again to keep that list updated instead of doing a lot of copy and paste of entries and manual upkeep of the address group(s).

If you are a fan of all terraform definitions in one file (I am not) you can download the combined file then rename it to have a .tf extension.