Jarl works out which views are reachable from the internet (match-clients
ending in a catch-all, not private-looking addresses: an internal view of a
global IPv6 prefix is still yours; a server without views: open allow-query
and a non-private listen address), with a per-view override (schema 9), and
checks public ones: rate limiting, open resolver, minimal-any, cache
snooping, open NOTIFY and transfers, version and host name, management
interfaces. Fixes are staged like any change, including rate limiting from
ISC's example in log-only mode with the secondaries exempt, then enforcing.
named logs what rate limiting limits (or would, in log-only mode, where its
counters don't move) as query-errors; Jarl routes those lines to its log
and counts them (rrl breakdown, ratelimited top lists, Prometheus
jarl_ratelimit_logged_total), shown on the new Exposure page.
A change that newly opens a public view up must be confirmed (accept_risk,
--accept-risk, a checkbox on the Changes page; 409 confirm_risk
otherwise). Findings can be acknowledged with a reason and stay visible as
info. GET /exposure, POST /exposure/fix|ack|unack, jarl exposure,
docs/exposure.md.
Also fixes query statistics skipping TSIG-signed queries ("IP#port/key
NAME" in named's log).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
8.8 KiB
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:
- 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 toexempt-clientsif they are). - When you're happy, Start enforcing (the fix of
exposure_rate_limit_log_only) removeslog-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).
You can tune the block like any option (jarl option set rate-limit '{ responses-per-second 20; window 15; exempt-clients { 192.0.2.53; }; }' --view external); the BIND ARM describes every setting.
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: truein the apply body, or?accept_risk=1on a change applied at once; without it the answer is 409confirm_risk. Previews (GET /changesets/{id}/preview) list them asrisks.
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).