Recursion firewall
Bounding the total work one request may cause, and caching resolution failures per RFC 9520.
A single query can be made to cost a resolver a great deal: a delegation chain that keeps referring, a zone whose nameservers all need resolving themselves, a signature set crafted to be expensive to verify. Timeouts bound how long that takes but not how much work it consumes.
The recursion firewall bounds the work itself, across the complete request tree, retries and nested resolver-generated queries included.
[recursion_firewall]
mode = "shadow"
Modes
off disables accounting. shadow records what would have been over budget and
changes no reply. enforce terminates over-budget recursion with SERVFAIL,
carrying EDE 0 with the text “Recursion work budget exceeded”, or, when the
budget crossed was a DNSSEC one, EDE 5 (DNSSEC Indeterminate) with “DNSSEC
validation work budget exceeded”. Fallback servers are not tried for such a
SERVFAIL.
shadow is the shipped default, and it is a genuine default rather than a
placeholder: the right limits depend on your traffic, and the way to find them
is to run shadow and read the histograms before you enforce anything.
The limits
max_outbound_queries = 128 # transport attempts in one request tree
max_internal_queries = 32 # resolver-generated child queries
An outbound attempt is one packet to one server; retries and UDP-to-TCP
fallbacks each consume another. An internal query is one the resolver generated
for itself: a cache-missed DS or DNSKEY, a nameserver address lookup, an alias
chase. 0 means “use the default” for both; to disable accounting use
mode = "off".
max_dnskey_candidates = 4 # same-tag keys tried per signature or DS
max_rrset_signature_checks = 8 # signatures tried per RRset
max_signature_checks = 32 # signature verifications per request tree
max_ds_digests = 32 # DS digest computations per request tree
max_nsec3_hashes = 32 # NSEC3 hashes per request tree
max_concurrent_crypto = 32 # crypto operations in flight, server-wide
The first two bound one verification; the next three bound the whole request
tree; the last bounds the server. Calibrate them from the
dnssec_work_per_request histogram, and pay particular attention to the NSEC3
p99, NSEC3 hashing is the operation an attacker can most cheaply make expensive.
Failure caching (RFC 9520)
failure_cache_size = 4096
failure_cache_min_ttl = "5s"
failure_cache_max_ttl = "5m"
This cache is active regardless of mode. mode governs work accounting;
caching failures is a protocol requirement, not optional hardening. The one way
to turn it off is the top-level rfc9520 = false, an emergency kill switch
that gives up RFC 9520 conformance and leaves the per-server retry ceiling and
the work limits in place (see
Resolution).
The first failed resolution is held for 5 seconds. Repeated failures back off exponentially to 5 minutes. RFC 9520 requires every active failure interval to fall between 1 second and 5 minutes, which is what the two bounds enforce.
Without this, a name that cannot resolve becomes a retry loop that hits the upstream once per client query.
Watching it
dns_recursion_firewall_exhaustions_total budget crossings, by reason (the limit crossed) and mode
dns_recursion_fanout_ratio histogram of outbound transport attempts per resolution tree
dns_resolution_shed_total lookups shed at an in-flight capacity ceiling, by scope
dns_resolution_shed_total is not a firewall outcome. It counts lookups
refused before any upstream work because too many were already in flight,
server-wide (global) or toward one zone (zone).
In shadow mode, dns_recursion_firewall_exhaustions_total{mode="shadow"} is
exactly the set of requests enforce would have failed. If it is nonzero for
ordinary traffic, the limit is too low for your workload; raise it before
enforcing, not after.