RPZ (DNS Firewall)

License: pro tier and above. See Licensing.

RPZ intercepts a query name before normal resolution and applies a rule: NXDOMAIN a known-bad domain, explicitly allow (PASSTHRU) an otherwise-blocked name, or CNAME it to a sinkhole. Rules are managed via the API/WebUI/gslbctl — there is no YAML config for this feature, matching the existing TXT-record precedent (a table of rows, not a config block; an empty rule set is simply a no-op).

Rule Shape

Field Meaning
triggerType exact (matches only this FQDN) or domain (matches this FQDN and all its subdomains).
triggerValue The domain, lowercased, no trailing dot.
action nxdomain (block), passthru (explicitly allow), or cname (sinkhole).
actionTarget CNAME target — only used when action is cname.
source Free-text label for where the rule came from (local by default).
hitCount Times this rule has matched a query, for visibility into what's actually firing.

Specificity: an exact rule always wins over a shorter domain rule that would otherwise cover it. Among domain rules, the most specific (deepest) enclosing one wins. This lets you block evil.com and everything under it, while carving out an explicit passthru exception for one particular subdomain.

How Matching Works

Rules are loaded from the DB into an in-memory index (two maps — one for exact triggers, one for domain triggers) rather than scanned linearly per query. A query name is checked against the exact-match map first, then walked label-by-label up toward the TLD checking the domain-match map at each level — O(number of labels in the query name), not O(number of rules). This is the actual point of the feature: RPZ only has value if it can run on every query without adding meaningful latency.

Each node keeps its own copy, refreshed from the DB every 5 seconds (a rule created via the API on the node that served the request takes effect on that node immediately; the 5s interval is the convergence bound for every other cluster node, which pick the change up via the DB — rqlite/Raft already replicated the row, this is purely how often each node's local index catches up to it).

Hit counts are tracked in memory per node and flushed to the DB on the same 5-second cadence — a query hitting a rule right before a node restart can lose a few increments. Acceptable for a visibility counter, not something anything else depends on.

RPZ intercepts happen before DNSSEC signing and are deliberately not signed — a rewritten NXDOMAIN or CNAME can't be validly signed under the real zone's keys without synthetic denial-of-existence records. Real-world RPZ deployments generally treat DNSSEC+RPZ interaction as a separate, opt-in concern; out of scope here.

Deferred, Not Built

  • Feed subscription (periodic auto-import from an external threat-intel URL) needs a poller and a format for parsing third-party feeds. Local API-managed rules are the requested "DNS firewall" capability and stand on their own; a feed importer is additive, not blocking.
  • RPZ-over-AXFR (letting third-party security tooling pull this rule set as a standard RPZ zone via zone transfer) needs an AXFR encoder for the RPZ wire format specifically. The rules themselves are already fully available and manageable via the REST API; the AXFR compatibility layer is a distinct, deferrable integration surface, not the core feature.

Both are real DNS firewall use cases and worth revisiting, but each is its own scoped follow-up rather than part of this vertical slice.

Usage

WebUI

/admin/rpz — add/remove rules, see hit counts per rule.

CLI

gslbctl rpz list
gslbctl rpz add evil-tracker.example --trigger-type domain --action nxdomain
gslbctl rpz add ads.example.com --trigger-type exact --action cname --target sinkhole.internal.example
gslbctl rpz remove <rule-id>

API

curl https://<api-host>:<port>/api/v1/rpz/rules -H "Authorization: Bearer $TOKEN"

curl -X POST https://<api-host>:<port>/api/v1/rpz/rules \
  -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
  -d '{"triggerType":"domain","triggerValue":"evil-tracker.example","action":"nxdomain"}'

Terraform

resource "nexus_rpz_rule" "block_tracker" {
  trigger_type  = "domain"
  trigger_value = "evil-tracker.example"
  action        = "nxdomain"
}

Rules are fire-and-forget, like Routing Schedules and Rollouts — there is no update API, so every attribute forces replacement.


Was this article helpful?
© 2026