Files
jarl/docs/exposure.md
adamandClaude Opus 5.5 bad360107e docs: every rate-limit setting, with defaults and when to change them
A Settings section in docs/exposure.md: the counting model, a table of all
15 rate-limit settings from the BIND 9.20 ARM, and what was checked
against real named (TCP never limited, NXDOMAIN falls back to
responses-per-second and is counted per zone across names, all-per-second
only drops, slipped answers are TC or BADCOOKIE depending on DNS cookies).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 13:18:17 +01:00

13 KiB
Raw Permalink Blame History

Internet exposure

When a view (or the whole server) can be reached from the internet, a few things matter that don't on your own network: it shouldn't help attackers flood others with DNS answers (amplification), recurse for strangers, or tell scanners its exact version. Jarl works out which views are public, checks them, and stages the fixes for you to review.

In the web UI: Exposure (and its card on the dashboard). In the CLI: jarl exposure. In the API: GET /api/v1/exposure.

Which views are public

A view is public when its match-clients let in anyone: after ACLs and ranges are filled in, they end in a catch-all (any, 0.0.0.0/0, ::/0, or a prefix wider than /8 for IPv4 or /16 for IPv6), or they are empty, which named reads as "any". A typical external view (match-clients { any; } or { !internal; any; }) is public.

A view that lists the networks it serves is private, whatever kind of addresses those are. An internal view for your global IPv6 prefix (2a02:8013:7f3d:10::/64) is as private as one for 192.168.1.0/24: listing a network says "these are mine". Jarl doesn't look at whether addresses are private (RFC 1918, ULA) for this, because on IPv6 your own devices have global addresses.

A view whose match-destinations list only private addresses (it answers only on a private interface) is private too.

A server without views has no match list, so Jarl looks at allow-query and where named listens: it is public when anyone may query it (allow-query unset, or with a catch-all) and named listens on an address that isn't private (a public IPv4 address or a global IPv6 address of this host). This can't see your firewall, so it is a guess.

When Jarl gets it wrong, override it per view: public, private or auto. For example, behind a NAT port forward named only ever sees private addresses although it is reachable from the internet.

jarl view update external --exposure public
jarl settings set --exposure private          # a server without views

The Exposure page shows each view's classification and the reason ("match-clients ends in any", "match-clients lists only homelab, no catch-all"), with a selector for the override.

The checks

They run on public views only, and are warnings (they don't stop you applying) unless acknowledged.

Code What Fix Jarl can stage
exposure_rate_limit No response rate limiting: the view can be used to flood others with its answers (DNS amplification) rate-limit { responses-per-second 10; log-only yes; exempt-clients { your secondaries }; }
exposure_rate_limit_log_only (info) Rate limiting only logs The same block without log-only: start enforcing
security_open_resolver Recursion for anyone: an open resolver recursion no in that view
exposure_minimal_any ANY queries get full answers, a favourite for amplification minimal-any yes
exposure_cache_snooping allow-query-cache lets anyone see what your clients look up allow-query-cache { none; }
security_open_transfer Zone transfers for anyone none: allow your secondaries or a TSIG key
exposure_open_notify allow-notify accepts NOTIFY from anyone none: allow your primaries
exposure_version dig CH TXT version.bind shows the exact BIND version version none
exposure_hostname dig CH TXT hostname.bind shows the host name hostname none
exposure_management controls or statistics-channels on an address that isn't loopback, open to anyone none: use 127.0.0.1 or an allow list

The last three are server-wide: they are checked when any view is public.

A fix goes into your pending changes like any edit (or is applied at once in live mode), so you see the named.conf diff before it reaches named:

jarl exposure                                         # classification, findings, fixes
jarl exposure fix external exposure_rate_limit
jarl exposure fix server exposure_version

Response rate limiting

Response rate limiting (RRL, BIND's rate-limit) counts identical responses sent to the same client network. Past the limit, named drops some and "slips" others: it sends a short reply asking the client to retry over TCP (or with a DNS cookie). A real client does; the spoofed victim of an amplification attack never sees the answers it didn't ask for. ISC recommends it for authoritative servers only, so Jarl only suggests it for public views, never for your internal resolver.

Jarl starts from ISC's example, 10 identical responses per second per client network, and exempts the servers your zones notify and allow transfers to (also-notify and allow-transfer addresses, through ACLs too): limiting your own secondaries is the classic RRL mistake.

Rollout in two steps:

  1. The fix turns rate limiting on with log-only yes. named answers everyone as before, but logs every response it would have limited. With query statistics on, Jarl reads those lines: the Exposure page shows how many responses would have been limited, and which client networks and names. Check that your secondaries, monitoring, or a big shared resolver aren't among them (add them to exempt-clients if they are).
  2. When you're happy, Start enforcing (the fix of exposure_rate_limit_log_only) removes log-only.

In log-only mode named's own counters don't move: what would be limited is only in its log (category query-errors). That is why the activity needs query statistics; without them, look at named's log for would rate limit. When rate limiting is configured and statistics are on, Jarl adds query-errors to its log channel (keeping named's syslog destination for it).

The statistics also count it: the rrl breakdown (drop, slip, would-drop, would-slip), the ratelimited and ratelimited_name top lists, and in Prometheus jarl_ratelimit_logged_total{action} next to named's own jarl_ratelimit_responses_total (metrics.md).

Settings

You can tune the block like any option; option set replaces the whole block, so copy the current value from jarl option list --view external and edit it:

jarl option set rate-limit '{ responses-per-second 20; window 15; slip 2; exempt-clients { 192.0.2.53; }; log-only yes; }' --view external

named counts identical responses to the same client network (a /24 for IPv4, a /56 for IPv6) over a window, and limits each kind of response separately. These are the settings (BIND 9.20; the ARM has the full text):

Setting Default What it does When to change it
responses-per-second 0 (off) Answers with data for the same name and type, per client network. Wildcard answers count as the zone's apex. This is the main limit; Jarl's fix sets 10 Raise it if a legitimate network shows up in log-only mode, e.g. a large shared resolver (mobile carriers, public resolvers) asking for your popular names
nxdomains-per-second same as responses-per-second NXDOMAIN answers (names that don't exist), counted together for a domain whatever the name asked 0 turns NXDOMAIN limiting off, for zones that legitimately get many lookups of missing names
nodata-per-second same as responses-per-second Empty answers (the name exists, the type doesn't), counted together whatever the type Rarely
referrals-per-second same as responses-per-second Referrals to child zones you delegate Only if you delegate subdomains to other servers
errors-per-second same as responses-per-second Other errors (REFUSED, SERVFAIL, FORMERR), counted together whatever the name Rarely. REFUSED and SERVFAIL can't be truncated, so slipped ones still go out at the slip rate
all-per-second 0 (off) All UDP answers to a client network, of any kind, like a firewall rate limit; over it, answers are only dropped (slip doesn't apply) Usually leave off; if you use it, ISC says at least 4 times the other limits
window 15 seconds (1–3600) How far back named counts. A client over the limit stays limited until its rate over the window falls again Longer windows remember attackers longer and also punish short legitimate bursts longer
slip 2 (0–10) Of the limited answers, every n-th "slips": instead of being dropped it gets a short reply asking the client to retry: truncated (TC) so it retries over TCP, or, when the query carried a DNS cookie, BADCOOKIE with a server cookie that lets it through on its retry. 0 drops all, 1 slips all 1 if you'd rather every real client gets a retry hint; 0 drops all (spoofed victims see nothing, but real clients just time out)
ipv4-prefix-length 24 The size of the client network answers are counted for (IPv4) 32 counts each address on its own: fairer to neighbours, more memory
ipv6-prefix-length 56 The same for IPv6 64 if one /56 holds many independent clients; 48 to treat a whole site as one
exempt-clients none Address match list (addresses, prefixes, ranges, ACLs) that is never limited. Jarl's fix puts your secondaries here Add monitoring, other servers of yours, and anything legitimate that log-only mode shows
log-only no Count and log what would be limited, answer everyone normally Jarl starts with yes; the "start enforcing" fix removes it
qps-scale off When the total query rate is above this, all limits shrink in proportion (at a rate of 1000 with qps-scale 250, a limit of 20 becomes 5) Only on busy servers that see attacks spread over many networks
max-table-size, min-table-size min 500 entries The table of client networks named tracks (40–80 bytes per entry) Only on busy servers

What Jarl checked against real named: TCP answers are never limited; nxdomains-per-second falls back to responses-per-second and 0 turns it off; NXDOMAIN answers for different missing names in a zone share one limit; all-per-second only drops; with slip 1, clients without DNS cookies get truncated answers, and clients with cookies (dig sends them by default) get through on their retry with the server's cookie; exempt-clients is never limited; in log-only mode nothing is limited and only the log shows what would be.

Rate limiting is per view: put it in the external view (--view external). Set in the server options, it applies to every view that has none of its own, your internal resolver included, which RRL isn't meant for.

Confirming a change that opens a view up

A change that introduces an open resolver, open zone transfers, cache snooping, open NOTIFY or an open management interface on a public view must be confirmed:

  • Web UI: the Changes page shows the finding with a checkbox (I understand: …); Apply stays disabled until it is ticked.
  • CLI: the command stops (exit 5) and says why; run it again with --accept-risk.
  • API: accept_risk: true in the apply body, or ?accept_risk=1 on a change applied at once; without it the answer is 409 confirm_risk. Previews (GET /changesets/{id}/preview) list them as risks.

Only changes that newly open something up ask: an unrelated change to a view that is already open, or taking back an acknowledgement, doesn't.

Acknowledging a finding

Some findings are deliberate: a public resolver for a club's network, a visible version string. Acknowledge them with a reason:

jarl exposure ack external security_open_resolver --reason "public resolver for the club, monitored"
jarl exposure ack server exposure_version --reason "version shown on purpose"
jarl exposure unack external security_open_resolver

The finding doesn't disappear: it stays visible as info, with who accepted it, when and why (open resolver … (acknowledged by adam on 12 Oct 2026: public resolver for the club, monitored)), on the Exposure page, the dashboard and in jarl check. An acknowledgement covers that one finding on that one view (or the server); the same finding elsewhere warns as usual. Acknowledging is a configuration change like any other, so it is in the history and audit log, and can be reverted.

Jarl isn't designed to run an open resolver, and an acknowledgement makes one deliberate and visible, not safe: a public resolver also needs abuse monitoring, response size limits and usually a machine of its own.

API

Method Path Body
GET /exposure scopes (per view, or the server: public, override, reason, rate_limit off/log-only/enforcing, findings with fix where Jarl has one), server (server-wide findings), rate_limiting (last 24h, with statistics: counts, networks, names)
POST /exposure/fix {"view": "external", "code": "exposure_rate_limit"} (view_id works too; no view: the server). Staged with ?changeset= like any change
POST /exposure/ack {"view": "external", "code": "security_open_resolver", "reason": "..."}
POST /exposure/unack {"view": "external", "code": "security_open_resolver"}

The override is a view's exposure field (PATCH /views/{id}), or the settings' for a server without views (PATCH /settings).