If you run a server behind Cloudflare, chances are you are already benefiting from better performance, caching, and protection against DDoS attacks. But there is one important step many administrators forget: updating your firewall to allow only Cloudflare’s IP ranges.

In this post, we will deal with UFW — Uncomplicated Firewall, but the same principles apply to any other firewall.

Why Cloudflare IP Updates Matter

When your website or service is proxied through Cloudflare, all traffic hitting your server should appear to come from Cloudflare’s servers — not directly from users.

That means:

  1. Visitors connect to Cloudflare.
  2. Cloudflare forwards the request to your origin server.
  3. Your server only needs to accept requests from Cloudflare IP addresses.

The Security Problem

If your firewall allows everyone to access ports like 80 and 443 directly, attackers can bypass Cloudflare entirely by connecting to your origin server directly.

This can lead to:

  • Exposure of your real server IP;
  • Bypassing Cloudflare WAF protections;
  • Direct brute-force attempts;
  • Unfiltered malicious traffic.

Restricting access to Cloudflare’s networks ensures only Cloudflare can reach your web server.

Automating Cloudflare Updates

Cloudflare maintains a published list of IP ranges here. However, these lists change over time; therefore, it is important to keep them up to date.

Here is a simple weekly script idea:

#!/bin/sh

set -eu

export PATH=/usr/sbin:/usr/bin:/sbin:/bin

IPSET_FILE="/etc/ipset.rules"

CF_V4_URL="https://www.cloudflare.com/ips-v4"
CF_V6_URL="https://www.cloudflare.com/ips-v6"

SET4="cf4"
SET6="cf6"

TMP4="${SET4}_tmp"
TMP6="${SET6}_tmp"

exec 9>/run/lock/update-cf-ipset.lock
command -v flock >/dev/null 2>&1 && flock -n 9 || true

tmpdir="$(mktemp -d)"
trap 'rm -rf "${tmpdir}"' EXIT

v4_file="${tmpdir}/ips-v4.txt"
v6_file="${tmpdir}/ips-v6.txt"

curl -fsSL "${CF_V4_URL}" -o "${v4_file}"
curl -fsSL "${CF_V6_URL}" -o "${v6_file}"

ensure_set() {
  set="${1}"
  family="${2}"

  if ipset list -n 2>/dev/null | grep -qx "${set}"; then
    if ! ipset list "${set}" 2>/dev/null | grep -q "Header: family ${family}"; then
      ipset destroy "${set}" || true
      ipset create "${set}" hash:net family "${family}"
    fi
  else
    ipset create "${set}" hash:net family "${family}"
  fi
}

ensure_set "${SET4}" inet
ensure_set "${SET6}" inet6

ipset destroy "${TMP4}" 2>/dev/null || true
ipset destroy "${TMP6}" 2>/dev/null || true
ipset create "${TMP4}" hash:net family inet
ipset create "${TMP6}" hash:net family inet6

while IFS= read -r cidr; do
    [ -n "${cidr}" ] && ipset add -exist "${TMP4}" "${cidr}"
done < "$v4_file"

while IFS= read -r cidr; do
    [ -n "${cidr}" ] && ipset add -exist "${TMP6}" "${cidr}"
done < "$v6_file"

ipset swap "${TMP4}" "${SET4}"
ipset swap "${TMP6}" "${SET6}"

ipset destroy "${TMP4}" 2>/dev/null || true
ipset destroy "${TMP6}" 2>/dev/null || true

umask 077
ipset save > "${IPSET_FILE}"

Save the script as /usr/local/sbin/update-cloudflare-ufw and make it executable (chmod 0700 /usr/local/sbin/update-cloudflare-ufw).

The script fetches the official Cloudflare network lists from https://www.cloudflare.com/ips-v4 and https://www.cloudflare.com/ips-v6; these lists contain all IP ranges Cloudflare uses when proxying traffic.

The script creates a lock file under /run/lock/ and uses flock to ensure the script cannot run twice at the same time (for example, if triggered by cron or a timer). The flock binary is provided by the util-linux packages in Debian-based distributions.

Then it fetches the official Cloudflare network lists from https://www.cloudflare.com/ips-v4 and https://www.cloudflare.com/ips-v6 to a temporary directory; these lists contain all IP ranges Cloudflare uses when proxying traffic.

The script manages two persistent IP sets:

  • cf4 for IPv4 ranges (SET4 variable);
  • cf6 for IPv6 ranges (SET6 variable).

If the sets do not exist or are the wrong address family, they are recreated correctly.

The script updates the sets atomically: it creates temporary sets, loads the downloaded IP ranges into them, and finally performs an atomic update by swapping the live and temporary sets and destroying the temporary sets. This instantly replaces the old Cloudflare network list with the new one, without leaving the firewall in an inconsistent state.

Finally, it saves the sets into /etc/ipset.rules.

Integrating ipset with UFW

While this script keeps the Cloudflare IP ranges updated inside the cf4 and cf6 collections, UFW will not automatically make use of these sets on its own. UFW is primarily designed around managing individual IP rules, but when working with large dynamic networks (like Cloudflare’s), it is far more efficient to integrate ipset directly into the underlying iptables rules that UFW generates.

To make this work reliably, the first step is ensuring the sets exist whenever UFW starts. This can be done by adding the following lines to /etc/ufw/before.init, which is executed early during firewall initialization:

case "$1" in
start)
  /sbin/ipset create cf4 hash:net family inet -exist
  /sbin/ipset create cf6 hash:net family inet6 -exist

This guarantees that even after a reboot, the required sets are present before any firewall rules attempt to reference them.

Once the sets exist, the next step is telling UFW to allow incoming HTTPS traffic only if it originates from Cloudflare’s networks. This is accomplished by adding custom rules directly into /etc/ufw/before.rules (for IPv4) and /etc/ufw/before6.rules (for IPv6).

For IPv4, the following rules allow port 443 traffic from the cf4 set and drop everything else:

-A ufw-before-input -p tcp --dport 443 -m set --match-set cf4 src -j ACCEPT
-A ufw-before-input -p tcp --dport 443 -j DROP

And similarly for IPv6:

-A ufw6-before-input -p tcp --dport 443 -m set --match-set cf6 src -j ACCEPT
-A ufw6-before-input -p tcp --dport 443 -j DROP

These lines must go after the End required lines comment.

With these rules in place, your server will only accept HTTPS connections via Cloudflare’s proxy infrastructure, preventing direct public access to the origin server and ensuring that all traffic benefits from Cloudflare’s protection layer.

Restoring ipset Rules at Boot with systemd

Although the update script saves the Cloudflare IP ranges into /etc/ipset.rules, Linux does not automatically reload the ipset state after a reboot. Since IP sets live in memory, they start out empty unless explicitly restored.

This becomes a problem when integrating with UFW: the firewall rules in before.rules reference the cf4 and cf6 sets. If those sets are missing or not yet populated when UFW starts, traffic may be blocked unexpectedly, or the firewall may fail to load properly.

To solve this, you can restore the saved IP sets early during the boot process using a dedicated systemd unit.

[Unit]
Description=Restore ipset rules
DefaultDependencies=no
Before=ufw.service
Before=network-pre.target
Wants=network-pre.target

[Service]
Type=oneshot
ExecStart=/sbin/ipset restore -exist -file /etc/ipset.rules
RemainAfterExit=yes

[Install]
WantedBy=multi-user.target

Save this file as /etc/systemd/system/ipset-restore.service, and then reload systemd and enable the unit:

systemctl daemon-reload
systemctl enable --now ipset-restore.service

The service runs once at boot (Type=oneshot), loads /etc/ipset.rules using ipset restore. It starts before UFW, ensuring that the Cloudflare sets are ready when the firewall rules are applied.

Automatically Updating Cloudflare Networks with cron

Cloudflare occasionally adds or changes IP ranges, so it is important to refresh your firewall IP sets periodically. While the boot-time restore ensures the sets are available after reboot, you still need a mechanism to keep them up to date over time.

A simple and reliable solution is to run the update script on a schedule using cron.

Put the code below into /etc/cron.weekly/update-cloudflare-ufw and make it executable:

#!/bin/sh

exec /usr/local/sbin/update-cloudflare-ufw

With this cron job in place, your server benefits from:

  • Regular Cloudflare IP updates without manual intervention;
  • Continuous protection against direct-to-origin bypass attempts;
  • Firewall rules that remain accurate as Cloudflare expands its network.

Along with the systemd restore unit and UFW integration, this creates a fully automated and reboot-safe setup.

Final Result

At this point, the workflow is complete:

  • Boot: systemd restores saved ipsets
  • Firewall: UFW rules allow HTTPS only from Cloudflare
  • Updates: cron refreshes Cloudflare ranges weekly
  • Security: origin server is no longer exposed to the public internet

Cloudflare becomes the only entry point to your web service.

Keeping UFW Updated with Cloudflare Networks
Tagged on:             

Leave a Reply

Your email address will not be published. Required fields are marked *