- Shell 100%
Security hardening pass: - Validate DNS-resolved entries through the shared filter_family_entries gate (fresh answers and cache reads), so dig +short garbage such as CNAME chain targets can never reach a restore file - Refuse plain-HTTP sources unless the source sets "allow_http": true; pin curl redirect protocols with --proto, cap redirects at 5 and downloads at 10 MiB (requires curl 8.4.0+, enforced at startup) - Check ipset restore's exit status (apply_restore_file) and skip the update on failure so a partially restored temp set is never swapped into the live firewall - Validate configured ipset names (charset and length) before use in restore files and sed expressions - Run with umask 077 and refuse a cache directory not owned by the invoking user Co-Authored-By: Claude Fable 5 |
||
|---|---|---|
| .gitignore | ||
| AGENTS.md | ||
| example.config.json | ||
| ipsets-warden.sh | ||
| LICENSE | ||
| README.md | ||
| REVIEW.md | ||
ipsets-warden
Manage iptables ipsets from a JSON configuration file, with
TTL-based caching and atomic (swap-in) updates. Designed to run
as a cron job.
Installation
sudo curl -fsSL https://code.paulg.it/paulgit/ipsets-warden/raw/branch/main/ipsets-warden.sh \
-o /usr/local/bin/ipsets-warden
sudo chmod +x /usr/local/bin/ipsets-warden
Requirements
ipset— kernel module and userspace tooljq— JSON parsingcurl— remote source fetchingdig(part ofbind-tools/bind9-utils) — domain resolutionflock(part ofutil-linux) — concurrency guardsha256sum(part ofcoreutils) — cache keying
On Debian/Ubuntu:
apt-get install ipset jq curl bind9-utils util-linux coreutils
For persistence across reboots on Debian/Ubuntu, also install:
apt-get install ipset-persistent iptables-persistent
On RHEL/Fedora:
dnf install ipset jq curl bind-utils util-linux coreutils
Configuration
Copy the example config to the system-wide location:
mkdir -p /etc/ipsets-warden
cp example.config.json /etc/ipsets-warden/config.json
The config file is JSON with three top-level keys:
| Key | Required | Default | Description |
|---|---|---|---|
ipsets |
yes | — | Array of ipset definitions |
default_ttl |
no | 3600 |
Cache TTL in seconds, applied to all sources unless overridden |
cache_dir |
no | /var/cache/ipsets-warden |
Directory for cached source data |
Each entry in ipsets has:
| Key | Required | Description |
|---|---|---|
name |
yes | Name of the ipset as it appears in ipset list |
type |
yes | "ipv4" or "ipv6" |
sources |
yes | Array of source objects |
Each source object has:
| Key | Required | Description |
|---|---|---|
uri |
yes | A bare IP, CIDR, domain name, local file path, or HTTP/HTTPS URL |
type |
no | Force the source type: ip, cidr, domain, or file. Auto-detected if omitted |
ttl |
no | Override the per-source TTL in seconds |
allow_http |
no | Permit a plain-http:// URL for this source. Defaults to false |
Remote URL sources are HTTPS-only by default: a plain-http://
URI is refused unless the source sets "allow_http": true, since
a network man-in-the-middle could otherwise control the entries
that end up in a firewall set. Redirects are pinned to the
permitted protocols and downloads are capped at 10 MiB.
This requires curl 8.4.0 or newer so the cap also applies when
the server does not provide a content length.
{
"cache_dir": "/var/cache/ipsets-warden",
"default_ttl": 3600,
"ipsets": [
{
"name": "cloudflare-ipv4",
"type": "ipv4",
"sources": [
{ "uri": "https://www.cloudflare.com/ips-v4", "ttl": 86400 },
{ "uri": "198.41.0.0/24" }
]
},
{
"name": "internal-whitelist",
"type": "ipv4",
"sources": [
{ "uri": "192.168.1.0/24" },
{ "uri": "10.0.0.1" },
{ "uri": "/etc/ipsets-warden/local-hosts.txt" }
]
}
]
}
Seeding ipsets
Run the script once as root to create and populate all ipsets defined in the config:
ipsets-warden
Verify the sets were created:
ipset list -n
Reference the ipset by name in your iptables rules. For
example, to allow all traffic from Cloudflare IP ranges:
iptables -A INPUT -m set --match-set cloudflare-ipv4 src -j ACCEPT
Rules referencing an ipset must be added after the ipset exists. The same ordering applies on boot: restore ipsets first, then restore iptables rules.
Useful inspection commands:
# List all managed ipsets
ipset list -n
# Show entries in a specific set
ipset list cloudflare-ipv4
# Count entries
ipset save cloudflare-ipv4 | grep -c "^add"
Scheduled updates
Add a cron entry to refresh ipsets periodically. Use
--cron-mode to suppress informational output — only changes
and errors are printed, which keeps cron mail quiet on unchanged
runs:
0 * * * * root /usr/local/bin/ipsets-warden --cron-mode
If you use ipset-persistent, save the updated live ipsets after
each successful run so the latest state survives reboot:
0 * * * * root /usr/local/bin/ipsets-warden --cron-mode && ipset save > /etc/iptables/ipsets
Persistence across reboots
ipsets and iptables rules live in kernel memory and are lost on reboot.
On Debian/Ubuntu, use ipset-persistent with
iptables-persistent:
apt-get install ipset-persistent iptables-persistent
Then seed the ipsets, add any iptables or ip6tables rules
that reference them, and save the current netfilter state:
ipsets-warden
netfilter-persistent save
This saves state to the standard Debian/Ubuntu locations:
/etc/iptables/ipsets
/etc/iptables/rules.v4
/etc/iptables/rules.v6
On boot, netfilter-persistent restores the saved ipsets before
restoring the saved iptables rules.
ipset-persistent restores the most recently saved ipset state.
If you want fresh data from live sources on every boot, add an
optional post-boot refresh service:
[Unit]
Description=Refresh ipsets from ipsets-warden config
After=network-online.target netfilter-persistent.service
Wants=network-online.target
[Service]
Type=oneshot
ExecStart=/usr/local/bin/ipsets-warden
ExecStartPost=/sbin/ipset save -file /etc/iptables/ipsets
[Install]
WantedBy=multi-user.target
Save it as /etc/systemd/system/ipsets-warden.service, then run:
systemctl daemon-reload
systemctl enable ipsets-warden.service
On systems without ipset-persistent, use your distribution's
native firewall persistence mechanism, or restore ipsets manually
before restoring iptables rules:
ipset restore < /path/to/ipsets.dump
iptables-restore < /etc/iptables/rules.v4
ip6tables-restore < /etc/iptables/rules.v6
Usage
Usage: ipsets-warden [options]
Options:
-h, --help Show this help message and exit.
--version Show version and exit.
-c, --config PATH Path to configuration file (default:
/etc/ipsets-warden/config.json or
~/.config/ipsets-warden/config.json)
--cron-mode Suppress output unless an error occurs
or an ipset changes.
--no-cache Bypass cache; re-download all sources.
--dry-run Resolve sources and print resolved entry
counts without applying any ipset updates.
License
WTFPL — see LICENSE.