Skip to content

cloudflare-ufw-sync

CI License: Apache-2.0

The cf-ufw-sync script additively synchronizes a UFW allowlist with Cloudflare's published IP ranges, with validation and dry-run support.

If your origin sits behind Cloudflare, you probably want ports 80/443 reachable only from Cloudflare's ranges. Those ranges change. Most published recipes solve this by flushing the firewall and rebuilding it from a freshly fetched list. That works right up until the fetch returns a truncated file, a captive portal page, or nothing at all — and then the rebuild removes the rules that were keeping your own traffic alive.

cf-ufw-sync takes the opposite position.

Design rules

1. Additive only. The script never deletes a UFW rule. Ranges you allow that Cloudflare no longer publishes are written to the log as STALE-ALERT for a human to review. An extra allow is a small problem; a removed allow can be an outage.

2. Refuse implausible input. Cloudflare publishes roughly 15 IPv4 and 7 IPv6 ranges. If the fetched list parses to fewer than 5 or more than 60 entries, the script aborts and changes nothing. This is the check that catches a truncated response before it becomes a firewall change.

3. Dry run is first-class. --dry-run prints every rule it would add and touches nothing, so you can see the diff before it exists.

Install

curl -fsSLO https://raw.githubusercontent.com/gexiro-global/cloudflare-ufw-sync/main/cf-ufw-sync.sh
chmod +x cf-ufw-sync.sh
sudo mv cf-ufw-sync.sh /usr/local/bin/cf-ufw-sync

Requires bash, curl, ufw, flock, python3, and iptables/ip6tables (read-only, for the stale check).

flock and python3 are not optional: the run is serialised with a lock, and address validation is delegated to python's ipaddress rather than a hand-rolled regex. If either is missing the script refuses to start rather than proceeding without the property it promises.

Usage

cf-ufw-sync --dry-run                 # show what would change
sudo cf-ufw-sync                      # sync IPv4 on ports 80 and 443
sudo cf-ufw-sync --ipv6               # include IPv6 ranges
sudo cf-ufw-sync --ports "443"        # HTTPS only

Run it from cron once a day:

17 4 * * * /usr/local/bin/cf-ufw-sync

Environment

Variable Default Meaning
CF_UFW_LOG /var/log/cf-ufw-sync.log Log destination. Use - for stdout.
CF_UFW_PORTS 80 443 Default port list.
CF_UFW_LOCK_DIR /run/lock/cf-ufw-sync (root) Directory (not a file) flock'd to serialise concurrent runs. Must be a non-symlink directory you own; opened read-only, so it is never truncated.

Log lines

2026-01-01T04:17:00Z ADDED 198.51.100.0/24 port 443
2026-01-01T04:17:00Z STALE-ALERT 203.0.113.0/24 review-manually
2026-01-01T04:17:00Z OK ranges=15 added=1 add_failed=0 stale=1 dry_run=0

Exit codes: 0 success, 1 fetch or sanity failure (nothing changed) or one or more ufw rules failed to install, 2 usage error. A rule that fails to install is logged as ADD-FAIL, is not counted as an addition, and fails the run — a firewall sync that silently reports success while changing nothing is worse than one that errors.

What this tool does NOT do

  • It does not remove firewall rules, ever. Cleaning up stale ranges is a manual decision.
  • It does not configure UFW's default policy. If your default inbound policy is allow, adding Cloudflare ranges achieves nothing — set ufw default deny incoming yourself, deliberately.
  • It does not protect ports other than the ones you name.
  • It does not verify that the traffic reaching you actually came through Cloudflare. IP allowlisting is one layer; consider Cloudflare Authenticated Origin Pulls for the rest.
  • It is not a substitute for keeping your origin's real address out of public DNS history.

Limitations you should read before running this as root

This script edits a firewall. Run --dry-run first. If you administer the machine over SSH on a port covered by --ports, understand that adding restrictive allows to a deny incoming policy can cut your own session. Keep an out-of-band console available the first time you run it.

Testing

./tests/run_tests.sh

The test runs fully offline against a synthetic fixture in examples/ — no network, no firewall access.

License

Apache-2.0. See LICENSE.

Built and maintained by Gexiro Global Enterprises Ltd.

Part of the Gexiro open-source toolkit.

About

Additively synchronize a UFW allowlist with Cloudflare's published IP ranges, with input validation and dry-run support.

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages