The iron-fisted keeper of your iptables sets. Creates, updates, syncs and enforces — no rule passes without its say.
Find a file
Paul Git 8b71c41063
feat: harden source validation, fetching, and restore handling
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
2026-07-15 21:04:53 +08:00
.gitignore feat: harden source validation, fetching, and restore handling 2026-07-15 21:04:53 +08:00
AGENTS.md Update project conventions and add REVIEW.md guide 2026-06-12 06:09:04 +07:00
example.config.json Add ipsets-warden script, config, and README 2026-05-23 22:49:39 +08:00
ipsets-warden.sh feat: harden source validation, fetching, and restore handling 2026-07-15 21:04:53 +08:00
LICENSE Initial commit 2026-05-23 19:44:30 +08:00
README.md feat: harden source validation, fetching, and restore handling 2026-07-15 21:04:53 +08:00
REVIEW.md feat: harden source validation, fetching, and restore handling 2026-07-15 21:04:53 +08:00

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 tool
  • jq — JSON parsing
  • curl — remote source fetching
  • dig (part of bind-tools / bind9-utils) — domain resolution
  • flock (part of util-linux) — concurrency guard
  • sha256sum (part of coreutils) — 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.