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
@@ -228,6 +228,8 @@ func (x *expander) expand(req Request) ([]model.Op, error) {
|
||||
return []model.Op{x.versioned(req, o.Version, summary)}, nil
|
||||
case OpSettingsUpdate:
|
||||
return []model.Op{newOpRaw(OpSettingsUpdate, 0, 0, req.Data, "update settings")}, nil
|
||||
case OpExposureAck, OpExposureUnack:
|
||||
return x.exposureAck(req, req.Op == OpExposureAck)
|
||||
case OpBlocklistCreate:
|
||||
return x.blocklistCreate(req)
|
||||
case OpBlocklistUpdate, OpBlocklistDelete:
|
||||
@@ -1089,3 +1091,64 @@ func disabledZoneEffect(st *model.State, z *model.Zone) string {
|
||||
}
|
||||
return fmt.Sprintf("once zone %s is disabled, %s answers REFUSED for its names", dnsname.Display(z.Name), where)
|
||||
}
|
||||
|
||||
// exposureAck acknowledges an exposure finding of a view or the server,
|
||||
// or takes the acknowledgement back: the view's (or the settings')
|
||||
// acknowledgements are updated as a whole.
|
||||
func (x *expander) exposureAck(req Request, ack bool) ([]model.Op, error) {
|
||||
var in AckInput
|
||||
if err := decode(req.Data, &in); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
in.Code = strings.TrimSpace(in.Code)
|
||||
if !validate.Ackable(in.Code) {
|
||||
return nil, errf(KindBadRequest, "%q is not an exposure finding that can be acknowledged (exposure_... or security_open_...)", in.Code)
|
||||
}
|
||||
if ack && strings.TrimSpace(in.Reason) == "" {
|
||||
return nil, errf(KindBadRequest, "say why %s is accepted (a reason is required)", in.Code)
|
||||
}
|
||||
var v *model.View
|
||||
switch {
|
||||
case in.ViewID != 0:
|
||||
v = x.st.Views[in.ViewID]
|
||||
case in.View != "":
|
||||
v = x.st.ViewByName(in.View)
|
||||
}
|
||||
if (in.ViewID != 0 || in.View != "") && v == nil {
|
||||
return nil, errf(KindNotFound, "no view %s", firstNonEmptyStr(in.View, fmt.Sprint(in.ViewID)))
|
||||
}
|
||||
acks := x.st.Settings.Acknowledged
|
||||
where := "the server"
|
||||
if v != nil {
|
||||
acks, where = v.Acknowledged, "view "+v.Name
|
||||
}
|
||||
var out []model.Acknowledgement
|
||||
for _, a := range acks {
|
||||
if a.Code != in.Code {
|
||||
out = append(out, a)
|
||||
}
|
||||
}
|
||||
summary := "acknowledge " + in.Code + " for " + where + ": " + strings.TrimSpace(in.Reason)
|
||||
if ack {
|
||||
out = append(out, model.Acknowledgement{Code: in.Code, Reason: strings.TrimSpace(in.Reason), By: x.p.Name(), At: x.now})
|
||||
} else {
|
||||
if len(out) == len(acks) {
|
||||
return nil, errf(KindConflict, "%s isn't acknowledged for %s", in.Code, where)
|
||||
}
|
||||
summary = "take back the acknowledgement of " + in.Code + " for " + where
|
||||
}
|
||||
if out == nil {
|
||||
out = []model.Acknowledgement{}
|
||||
}
|
||||
if v != nil {
|
||||
return []model.Op{newOp(OpViewUpdate, v.ID, v.Version, ViewInput{Acknowledged: &out}, summary)}, nil
|
||||
}
|
||||
return []model.Op{newOp(OpSettingsUpdate, 0, 0, SettingsInput{Acknowledged: &out}, summary)}, nil
|
||||
}
|
||||
|
||||
func firstNonEmptyStr(a, b string) string {
|
||||
if a != "" {
|
||||
return a
|
||||
}
|
||||
return b
|
||||
}
|
||||
Reference in new issue
Block a user