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
@@ -2,7 +2,7 @@ import { h, mount, time, fmtTime, relTime, problemsList, badge, plural } from ".
|
||||
import { get, state } from "../api.js";
|
||||
|
||||
export async function render(el) {
|
||||
const [st, mine] = await Promise.all([get("/status"), get("/changesets", { status: "open", mine: 1, limit: 50 }).catch(() => [])]);
|
||||
const [st, mine, exposure] = await Promise.all([get("/status"), get("/changesets", { status: "open", mine: 1, limit: 50 }).catch(() => []), get("/exposure").catch(() => null)]);
|
||||
const s = st.engine || {};
|
||||
// The user's own changesets with something in them: what the Changes page shows.
|
||||
const pending = mine.filter((c) => (c.ops || []).length).length;
|
||||
@@ -16,6 +16,7 @@ export async function render(el) {
|
||||
card("Access", plural((s.counts || {}).acls || 0, "ACL"), plural((s.counts || {}).tsig_keys || 0, "TSIG key")),
|
||||
card("Options", (s.counts || {}).options ?? "–", "server, view, zone and top-level"),
|
||||
st.backup ? backupCard(st.backup) : null,
|
||||
exposure ? exposureCard(exposure) : null,
|
||||
);
|
||||
const last = s.last_applied;
|
||||
const parts = [h("div", { class: "page-head" },
|
||||
@@ -106,6 +107,16 @@ function backupCard(b) {
|
||||
return card("Backup", value, sub);
|
||||
}
|
||||
|
||||
// exposureCard sums up the internet exposure page.
|
||||
function exposureCard(r) {
|
||||
const pub = r.scopes.filter((s) => s.public);
|
||||
let warn = r.server.filter((f) => f.severity === "warning").length;
|
||||
for (const s of r.scopes) warn += s.findings.filter((f) => f.severity === "warning").length;
|
||||
const value = !pub.length ? badge("nothing public", "ok") : warn ? badge(plural(warn, "warning"), "warn") : badge("hardened", "ok");
|
||||
const sub = pub.length ? "public: " + pub.map((s) => s.view || "the server").join(", ") : "no view is reachable from the internet";
|
||||
return h("a", { class: "card link", href: "#/exposure" }, h("div", { class: "label" }, "Exposure"), h("div", { class: "value" }, value), h("div", { class: "sub" }, sub));
|
||||
}
|
||||
|
||||
function card(label, value, sub) {
|
||||
return h("div", { class: "card" }, h("div", { class: "label" }, label), h("div", { class: "value" }, value), sub ? h("div", { class: "sub" }, sub) : null);
|
||||
}
|
||||
Reference in new issue
Block a user