No description
Find a file
Paul Git 9888cc3af1
Bump to v2.3: propagate API failures and harden downloads
Fix fail-open paths where a failed Cloudflare PUT or item fetch was
reported as success, enforce the source size limit after transfer and
on cache reads, sanitize remote feed lines before logging, move the
PUT payload off argv, remove token and payload temp files immediately
after use, and guard dig availability and DNS cache-key failures.
Extend the security test suite to cover the new protections.

Co-Authored-By: Claude Fable 5
2026-07-15 19:48:02 +08:00
.gitignore Add cloudflare-iplist tool, docs, and .gitignore 2026-04-29 20:03:50 +08:00
AGENTS.md Condense AGENTS.md, delete CLAUDE.md symlink, add REVIEW.md 2026-06-14 14:40:36 +07:00
cloudflare-iplist.sh Bump to v2.3: propagate API failures and harden downloads 2026-07-15 19:48:02 +08:00
config.example.json Add multi-source configuration support (v2.0) 2026-06-02 14:40:14 +07:00
LICENSE Add WTFPL license and reference in README 2026-04-29 21:14:21 +08:00
README.md Bump to v2.3: propagate API failures and harden downloads 2026-07-15 19:48:02 +08:00
REVIEW.md Condense AGENTS.md, delete CLAUDE.md symlink, add REVIEW.md 2026-06-14 14:40:36 +07:00
test-security.sh Bump to v2.3: propagate API failures and harden downloads 2026-07-15 19:48:02 +08:00

cloudflare-iplist

Version: 2.3

Maintains one or more Cloudflare IP Lists from configurable sources with TTL-based caching. Sources can be URLs, local files, DNS domains, bare IP addresses, or CIDR ranges.

Overview

cloudflare-iplist resolves IP addresses and CIDR ranges from multiple sources, merges and deduplicates them, and updates Cloudflare IP Lists to match. It uses the Cloudflare API v4 and requires curl, jq, and optionally dig (for DNS sources) and sha256sum or shasum (for caching) as dependencies.

Installation

Quick install (single line)

System-wide (requires sudo):

sudo curl -fsSL https://code.paulg.it/paulgit/cloudflare-iplist/raw/branch/main/cloudflare-iplist.sh -o /usr/local/bin/cloudflare-iplist && sudo chmod +x /usr/local/bin/cloudflare-iplist

Per-user (no sudo required; ensure ~/.local/bin is in your $PATH):

mkdir -p ~/.local/bin
curl -fsSL https://code.paulg.it/paulgit/cloudflare-iplist/raw/branch/main/cloudflare-iplist.sh -o ~/.local/bin/cloudflare-iplist && chmod +x ~/.local/bin/cloudflare-iplist

Manual install

  1. Copy the cloudflare-iplist script to a directory in your $PATH. For example, /usr/local/bin/ (system-wide) or ~/.local/bin/ (per-user):

    # System-wide
    sudo cp cloudflare-iplist.sh /usr/local/bin/cloudflare-iplist
    sudo chmod +x /usr/local/bin/cloudflare-iplist
    
    # Per-user (ensure ~/.local/bin is in your $PATH)
    mkdir -p ~/.local/bin
    cp cloudflare-iplist.sh ~/.local/bin/cloudflare-iplist
    chmod +x ~/.local/bin/cloudflare-iplist
    
  2. Ensure dependencies are installed:

    curl --version
    jq --version
    

    For DNS sources, also ensure dig is available:

    dig -v
    

    For URL caching, ensure sha256sum or shasum is available:

    sha256sum --version   # Linux
    shasum --version      # macOS
    

Configuration

The script reads a JSON configuration file. It searches for the config in this order:

  1. Path specified with -c / --config
  2. /etc/cloudflare-iplist/config.json
  3. ~/.config/cloudflare-iplist/config.json

Example configuration

{
  "default_ttl": 3600,
  "cache_dir": "/var/cache/cloudflare-iplist",
  "lists": [
    {
      "name": "cloudflare-ips",
      "account_id": "0123456789abcdef0123456789abcdef",
      "api_token": "your-api-token",
      "list_id": "0123456789abcdef0123456789abcdef",
      "sources": [
        { "uri": "https://www.cloudflare.com/ips-v4", "ttl": 86400 },
        { "uri": "https://www.cloudflare.com/ips-v6", "ttl": 86400 },
        { "uri": "198.41.0.0/24" },
        { "uri": "/etc/cloudflare-iplist/local-whitelist.txt" }
      ]
    },
    {
      "name": "internal-whitelist",
      "account_id": "another-account-id",
      "api_token": "another-api-token",
      "list_id": "another-list-id",
      "sources": [
        { "uri": "192.168.1.0/24" },
        { "uri": "10.0.0.1" },
        { "uri": "example.com", "type": "dns" },
        { "uri": "/etc/cloudflare-iplist/internal-ips.txt" }
      ]
    }
  ]
}

Configuration fields

Top-level fields

Field Required Default Description
lists yes Array of IP List definitions. Must contain at least one entry.
default_ttl no 3600 Cache TTL in seconds for URL and DNS sources that don't specify their own ttl. Must be between 1 and 31536000.
cache_dir no See Caching Private, user-owned directory for cached source data. Created automatically with mode 0700 if it doesn't exist.

List object (lists[])

Field Required Description
name yes Human-readable label for this list (used in log messages). Not sent to Cloudflare.
account_id yes Cloudflare account ID that owns the IP List.
api_token yes Cloudflare API token with Account > IP Lists > Edit permission.
list_id yes ID of the existing Cloudflare IP List to manage.
sources yes Array of source objects. IPs/CIDRs from all sources are merged and deduplicated.

Source object (lists[].sources[])

Field Required Description
uri yes The source to resolve. Can be a URL, local file path, DNS domain, bare IP, or CIDR.
type no Explicit source type: "url", "file", "dns", "cidr", or "ip". Auto-detected if omitted.
ttl no Cache TTL in seconds for this source. Must be between 1 and 31536000. Overrides default_ttl and is only meaningful for URL and DNS sources.

Source type auto-detection

When type is omitted, the uri is classified by pattern:

Pattern Detected type Example
Starts with https:// url https://www.cloudflare.com/ips-v4
Starts with / file /etc/cloudflare-iplist/local-whitelist.txt
Matches CIDR notation (x.x.x.x/y) cidr 198.41.0.0/24
Matches a bare IPv4 address ip 203.0.113.42
Matches IPv6 (with colons or ::) ip 2001:db8::1
Everything else dns example.com

Source type behaviour

Type Behaviour
url Downloads an HTTPS file, strictly validates one IP/CIDR per line (ignoring blanks and # comments), and caches the result for ttl seconds. Downloads are limited to 5 MiB and HTTPS redirects.
file Reads a local text file with the same one-per-line format. Not cached.
dns Resolves the domain's A and AAAA records via dig. Cached for ttl seconds.
cidr Passed through directly as a single CIDR entry. Not cached.
ip Passed through directly as a single IP entry. Not cached.

Creating the IP List

The script only updates existing lists. Create one first in the Cloudflare dashboard under Manage account > Configurations > Lists, or via the API.

API token permissions

Your API token needs at minimum:

  • Account > IP Lists > Edit

If you are also updating IP access rules via the same token, you additionally need one of the following:

  • Account > Account Firewall Access Rules > Edit (account-level rules)
  • Zone > Firewall Services > Edit (zone-level rules)

Security

The configuration file contains the Cloudflare API token. It must be owned by the effective user or root, must not be a symbolic link, and must not be accessible to group or other users. Set restrictive permissions before running the script:

chmod 600 ~/.config/cloudflare-iplist/config.json

The script refuses to use a configuration file that fails these checks. API tokens are supplied to curl through a protected temporary configuration file so they are not exposed in the process argument list.

Usage

cloudflare-iplist [options]

Options

Option Description
-h, --help Show usage information and exit.
-v, --version Show version information and exit.
-c, --config PATH Path to configuration file.
--cronmode Suppress all output unless an error occurs or a list changes.
--dry-run Show what would change without updating Cloudflare.
--reset Remove all existing entries before adding the new ones.

Normal mode

cloudflare-iplist

Outputs informational messages showing each step: config load, source resolution, comparison, and update.

Cron mode

cloudflare-iplist --cronmode

Silences all output except errors and change notifications. Suitable for running from cron or systemd timers. Exit code is 0 on success, non-zero on failure.

Dry run

cloudflare-iplist --dry-run

Performs all steps except the actual Cloudflare update. Shows the parsed IPs and the computed diff so you can preview changes before applying them.

Reset mode

cloudflare-iplist --reset

Removes all existing entries from the IP List before adding the resolved ones. This is useful when you want a clean slate or when the list may contain stale entries that the normal diff logic cannot reconcile. Combine with --dry-run to preview the effect.

Caching

URL and DNS source results are cached in cache_dir. If not set in config, the script searches for a writable directory in this order:

  1. ${XDG_CACHE_HOME}/cloudflare-iplist
  2. ~/.cache/cloudflare-iplist
  3. /var/cache/cloudflare-iplist (root only)
  4. ${TMPDIR:-/tmp}/cloudflare-iplist-cache.<UID>

The first safe directory that exists and is writable is used. Existing cache directories must be owned by the effective user, must not be symbolic links, and must not be writable by group or other users. If none exist, the script tries to create each in order until one succeeds. Created cache directories are set to 0700 and cache files to 0600.

Each cached file is stored under a SHA-256 hash of the source URI and is valid for the duration of its TTL (configured per-source or via default_ttl). Cache entries are written to private temporary files and atomically renamed into place without following an existing cache symlink.

If a fetch fails and a stale cache file exists, the stale data is used as a fallback to prevent a temporary network outage from clearing the list.

Source file format

Remote URL sources and local file sources must contain one IP address or CIDR range per line. Blank lines and lines beginning with # are ignored.

Example:

# Cloudflare IPv4 ranges
173.245.48.0/20
103.21.244.0/22

# Cloudflare IPv6 ranges
2400:cb00::/32
2606:4700::/32

How it works

  1. Reads the configuration file and discovers all list definitions.
  2. For each list, resolves every source (URL, file, DNS, IP, or CIDR).
  3. Merges and deduplicates all resolved entries.
  4. Fetches the current items from the Cloudflare IP List (with pagination).
  5. Compares the desired items to the current items.
  6. If there are differences, replaces the entire list atomically via the Cloudflare API. Each added or removed entry is logged with the source URI that contributed it, e.g. + 203.0.113.42 [https://example.com/list.txt].
  7. Every list item is tagged with the comment "Auto-managed by cloudflare-iplist".

Rate limiting and retries

API requests are retried up to 3 times with exponential backoff (1s, 2s, 4s) when the Cloudflare API returns HTTP 429 (rate limit) or 502/503/504 (transient server errors). Source and API requests have 10-second connection timeouts and 30-second total timeouts.

Exit codes

Code Meaning
0 Success (all lists are up to date or were updated).
1 Error (missing config, invalid input, source failure, API failure, etc.). If any configured list fails, the script continues with the remaining lists and exits 1 after processing them.

Testing

Run the security-focused regression suite with:

./test-security.sh

The suite checks strict address and TTL validation, log sanitization of remote data, cache directory and symlink protections, download size limits, configuration permissions, HTTPS enforcement, API-token argument secrecy, API failure propagation, and aggregate failure exit status.

Running from cron

Add to your crontab with --cronmode to avoid noise:

# Update Cloudflare IP lists every hour
0 * * * * /usr/local/bin/cloudflare-iplist --cronmode

Migration from v1

The v1 config format with source_url is no longer supported. To migrate, restructure your config to use the lists array:

// Old format (v1)
{
  "account_id": "...",
  "api_token": "...",
  "list_id": "...",
  "source_url": "https://www.cloudflare.com/ips-v4"
}

// New format (v2)
{
  "default_ttl": 3600,
  "lists": [
    {
      "name": "cloudflare-ips",
      "account_id": "...",
      "api_token": "...",
      "list_id": "...",
      "sources": [
        { "uri": "https://www.cloudflare.com/ips-v4", "ttl": 86400 }
      ]
    }
  ]
}

License

This project is licensed under the WTFPL — Do What The Fuck You Want To Public License.

No warranties are given for correct function.