nsupdate — a DNS blocklist (RBL)

Maintains an RBL-style DNS blocklist by using nsupdate(1) to add and remove TXT records under a BIND zone, authenticated with a TSIG key — the equivalent of the fail2ban nsupdate action. Nothing is blocked on any host; consumers of the RBL (MTAs, milters, other tooling querying the zone) do their own refusing.

Banning 1.2.3.4 creates:

4.3.2.1.rbl.example.com. 60 IN TXT "banned"

— the classic reversed-octet RBL layout.

[kur.rbl]
backend = "nsupdate"

[kur.rbl.options]
domain  = "rbl.example.com"
keyfile = "/usr/local/etc/namedb/rbl.key"

BIND-side setup — required before use

Generate a TSIG key and let it update the zone:

tsig-keygen -a hmac-sha256 kur-rbl > /usr/local/etc/namedb/rbl.key
chmod 400 /usr/local/etc/namedb/rbl.key
include "/usr/local/etc/namedb/rbl.key";

zone "rbl.example.com" {
    type master;
    file "dynamic/rbl.example.com.zone";
    update-policy { grant kur-rbl subdomain rbl.example.com. TXT; };
};

The update-policy grant above is scoped to TXT records under the zone — all this backend ever writes. A plain allow-update { key kur-rbl; }; also works but grants more. The zone file must be writable by named (dynamic zones usually live in a directory of their own).

Requirements

Settings

Options

| option | default | what | |------------|--------------|------------------------------------------------------------------| | domain | (required) | domain the records live under; /^[a-zA-Z0-9.\-]+$/ | | keyfile | (required) | full path to the TSIG key file; no whitespace or single quotes | | ttl | 60 | TTL in seconds for created TXT records | | rdata | banned | TXT record data; /^[a-zA-Z0-9 .,:_\-]+$/ | | nsupdate | nsupdate | the nsupdate command to run |

rdata is what RBL consumers see when they query; some tooling keys off the text, so it is settable. Keep ttl low — it bounds how long a lifted ban keeps resolving from caches.

What each operation runs

Everything is printf '<statements>' | nsupdate -k '<keyfile>', with the record name built by reversing the IP's octets onto the domain:

| operation | statements fed to nsupdate | |------------|------------------------------------------------------------------------------------| | init | none — just verifies the keyfile exists as a regular file | | ban | prereq nxrrset <rev>.<domain> TXTupdate add <rev>.<domain> <ttl> IN TXT "<rdata>"send | | unban | update delete <rev>.<domain> TXTsend | | list | no command — the kur's own ban book | | check | nothing — always reports healthy | | flush | the delete statements per banned IP | | re_init | teardown (best effort), init, re-add every banned IP | | teardown | the delete statements per banned IP (ban book kept for re_init) |

The prereq nxrrset on ban makes the add conditional on the record not already existing — a replayed ban (tablet reload after restart) fails the prereq rather than stacking records. DNS deletes are idempotent, so unbanning an already-gone record succeeds quietly.

self_heal and remote drift

check has nothing it can probe cheaply and always reports healthy, so self_heal is a no-op for this backend. A record deleted on the server by hand stays gone until re_init; a hand-added record is invisible to the kur. If the master is unreachable, bans fail at ban time (nsupdate exits non-zero after its own retry/timeout behavior — the kur imposes no timeout of its own).

Gotchas