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.