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:
1 parent
cf550bd512
commit
bdd7acfd3b
50 files changed
+2019
-52
No files matched your search
@@ -563,10 +563,15 @@ func (c *checker) checkSecurity() {
|
||||
for _, v := range views {
|
||||
ctxs = append(ctxs, ctx{v, "view " + v.Name})
|
||||
}
|
||||
exposure := map[int64]Exposure{}
|
||||
for _, x := range c.exposures() {
|
||||
exposure[x.ViewID] = x
|
||||
}
|
||||
for _, x := range ctxs {
|
||||
// A view only reachable from some clients (e.g. "internal") can
|
||||
// recurse for everyone inside it without being an open resolver.
|
||||
if x.v != nil && !c.listMatchesEveryone(x.v.MatchClients, true) {
|
||||
// Without views, only a server marked private is left out.
|
||||
if x := exposure[viewID(x.v)]; !x.Public && (x.ViewID != 0 || x.Override == model.ExposurePrivate) {
|
||||
continue
|
||||
}
|
||||
if c.effectiveBool(x.v, "recursion", true) {
|
||||
@@ -608,6 +613,9 @@ func (c *checker) checkSecurity() {
|
||||
p := c.errf("dynamic_update_unsupported", "zone", z.ID, "allow-update makes the zone dynamic, but Jarl rewrites zone files and would discard dynamic updates; remove allow-update (or set it to { none; }) and manage records through Jarl")
|
||||
p.Zone, p.View = z.Name, c.s.ViewName(z)
|
||||
}
|
||||
if x, ok := exposure[z.ViewID]; ok && !x.Public && (x.ViewID != 0 || x.Override == model.ExposurePrivate) {
|
||||
continue // only reachable by your own clients
|
||||
}
|
||||
if o := c.s.FindOption(model.ScopeZone, z.ID, "allow-transfer"); o != nil && namedconf.ContainsAny(c.amlOf(o)) {
|
||||
p := c.warnf("security_open_transfer", "zone", z.ID, "allows zone transfers (AXFR) to anyone; restrict allow-transfer to your secondaries or a TSIG key")
|
||||
p.Zone, p.View = z.Name, c.s.ViewName(z)
|
||||
|
||||
Reference in new issue
Block a user