- Shell 100%
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 |
||
|---|---|---|
| .gitignore | ||
| AGENTS.md | ||
| cloudflare-iplist.sh | ||
| config.example.json | ||
| LICENSE | ||
| README.md | ||
| REVIEW.md | ||
| test-security.sh | ||
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
-
Copy the
cloudflare-iplistscript 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 -
Ensure dependencies are installed:
curl --version jq --versionFor DNS sources, also ensure
digis available:dig -vFor URL caching, ensure
sha256sumorshasumis available:sha256sum --version # Linux shasum --version # macOS
Configuration
The script reads a JSON configuration file. It searches for the config in this order:
- Path specified with
-c/--config /etc/cloudflare-iplist/config.json~/.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:
${XDG_CACHE_HOME}/cloudflare-iplist~/.cache/cloudflare-iplist/var/cache/cloudflare-iplist(root only)${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
- Reads the configuration file and discovers all list definitions.
- For each list, resolves every source (URL, file, DNS, IP, or CIDR).
- Merges and deduplicates all resolved entries.
- Fetches the current items from the Cloudflare IP List (with pagination).
- Compares the desired items to the current items.
- 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]. - 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.