# 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. ```sh 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: ```sh 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](stats.md) 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](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: 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: ```sh 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`).