Internet exposure checks and response rate limiting

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>
This commit is contained in:
adamandClaude Opus 5.5 committed 2026-10-12 10:38:56 +01:00
1 parent cf550bd512
commit bdd7acfd3b
50 files changed
+2019 -52

No files matched your search

+39
View File
@@ -366,6 +366,45 @@ expect_dig "unsigned from here: the internal view" "127.0.0.1" ns1.adbraun.test
expect_dig "signed with the external view's key" "203.0.113.1" -k $E/external.key ns1.adbraun.test A
run "view key shows a dig command" 0 $J view key external
contains "dig -y hmac-sha256:jarl-view-external:"
# Internet exposure (docs/exposure.md): external lets in anyone, internal
# only homelab.
run "exposure" 0 $J exposure
contains "^internal *private"
contains "^external *public *off"
contains "exposure_rate_limit "
contains "fix: jarl exposure fix external exposure_rate_limit"
run "fix: rate limiting, log-only first" 0 $J exposure fix external exposure_rate_limit
run "named.conf has it" 0 grep -A2 'view "external"' $E/etc-jarl/named.conf
contains "rate-limit { responses-per-second 10; log-only yes; };"
run "exposure: log-only now" 0 $J exposure
contains "^external *public *log-only"
contains "exposure_rate_limit_log_only"
run "fix: start enforcing" 0 $J exposure fix external exposure_rate_limit_log_only
contains "responses-per-second 10; }"
# A burst of identical queries in the external view (signed with its key):
# some are dropped. The internal view has no rate limit.
DROPS=0
for i in $(seq 60); do
$DIG +tries=1 +time=1 +ignore -k $E/external.key -p $DNS @127.0.0.1 ns1.adbraun.test A | grep -q "status:" || DROPS=$((DROPS+1))
done
if [ $DROPS -gt 0 ]; then ok "rate limiting dropped $DROPS of 60"; else bad "rate limiting dropped nothing"; fi
INTERNAL=$(for i in $(seq 30); do $DIG +tries=1 +time=1 +short -p $DNS @127.0.0.1 ns1.adbraun.test A; done | grep -c 127.0.0.1)
if [ "$INTERNAL" = 30 ]; then ok "the internal view isn't limited"; else bad "the internal view answered $INTERNAL of 30"; fi
run "remove the rate limit again" 0 $J option unset rate-limit --view external
run "an open resolver on a public view needs confirming (exit 5)" 5 $J option set allow-recursion any --view external
contains "accept-risk"
run "with --accept-risk" 0 $J option set allow-recursion any --view external --accept-risk
run "acknowledge it" 0 $J exposure ack external security_open_resolver --reason "e2e: on purpose"
run "acknowledged: info, with the reason" 0 $J exposure
contains "info *external *security_open_resolver .*acknowledged by .*e2e: on purpose"
run "take it back" 0 $J exposure unack external security_open_resolver
run "close the resolver again" 0 $J option unset allow-recursion --view external
run "fix: hide the version" 0 $J exposure fix server exposure_version
expect_dig "version.bind is hidden" "" -c CH version.bind TXT
run "mark external private" 0 $J view update external --exposure private
run "exposure: private by hand" 0 $J exposure
contains "^external *private (set)"
run "back to auto" 0 $J view update external --exposure auto
# named-checkconf's own warnings stay on the dashboard after the apply.
run "option set recursion no (external)" 0 $J option set recursion no --view external
run "allow-recursion without its braces" 0 $J option set allow-recursion 192.0.2.1 --view external