The consequential decisions behind protector — the why behind the code, not
just the what. New decisions get a new numbered file; superseded ones stay
(marked Superseded by NNNN).
Format: Michael Nygard's ADR style.
Copy 0000-template.md to start one.
| # | Decision | Status |
|---|---|---|
| 0001 | Async mitigation engine: propose / prove / respond, local-first | Accepted |
| 0002 | Change-driven incident loop: diff the cluster, prove the delta, manage the debt | Accepted |
| 0003 | Capability ports: depend on what a tool answers, not which tool it is | Accepted |
| 0004 | Graph representation: in-memory petgraph, rebuilt from observed state | Accepted |
| 0005 | Objectives are ATT&CK outcomes, not just secrets | Accepted |
| 0006 | Build the substrate; treat KubeHound/IceKube as catalogue and optional provider | Accepted |
| 0007 | Live network cuts are additive AdminNetworkPolicy Deny rules | Accepted |
| 0009 | Asymmetric action bar: live evidence acts, latent exposure proposes | Accepted (amended by 0011, 0013, 0016, 0017, 0022; corroboration made tool-agnostic + per-objective by 0014) |
| 0010 | Flannel actuator: quarantine the source with a default-deny NetworkPolicy | Accepted (amended by 0022) |
| 0011 | The model corroborates positively; operator access is out of scope, defended in depth | Superseded in part by 0013 |
| 0012 | Exposure is observed where possible, declared (annotation) where it can't be — tunnels | Accepted |
| 0013 | Proof winnows the search space; the model makes the exploitability call (positive gate + breach-relevance) | Accepted (amended by 0016) |
| 0014 | First-party behavioral telemetry via eBPF, behind a tool-agnostic port (potential vs actual) | Accepted (amended: per-objective corroboration landed; the Retire-Falco parity bar = measured decision-path coverage, retire the adapter not the port) |
| 0015 | Advisory evidence is mounted-snapshot-only (zero egress); structurally extracted + capped for injection safety | Accepted (advisory feed retired; Rekor egress carve-out amended by 0020) |
| 0016 | The breach model: prove chains, enrich them, the model decides and isolates until clear | Accepted (amended by 0017) |
| 0017 | Isolation persists on the breach condition: chain ∧ enrichment fingerprint (revert keys on entry_fingerprint) |
Accepted |
| 0018 | The breach notifier is the one sanctioned outbound path: operator-configured, off by default, redacted by default | Accepted |
| 0019 | Dashboard v3: server-rendered (maud), zero-egress, light-theme presentation — the view_model/component/page split + the honesty invariants | Accepted (amended: finding detail shows all proven paths; presentation mechanism superseded in part by 0025 — IA + honesty axes survive) |
| 0020 | Supply-chain trust is signature continuity: observe every image, learn a per-repo TOFU baseline, treat the signed→unsigned / identity-change regression as the signal — not prefix-gated single-identity (amended: baseline-relative downgrade; build-provenance as a second continuity axis) | Accepted |
| 0021 | Two-setting operating posture: mode (audit default / enforce) + one enforceScope arms all three enforcement surfaces (signature + mesh webhooks + engine live cut), fail-closed webhook selector and actuation RBAC derived from it — no per-surface toggle, no wildcard |
Accepted |
| 0022 | Quarantine the internet-facing entry is the default containment (entry-only, additive/reversible default-deny); the surgical edge-cut is the refinement used only when it suffices | Accepted |
| 0023 | Delta-aware adjudication: the full cluster state is the context, the change is the question | Accepted |
| 0024 | Corroboration shapes must be load-bearing when merged, not deferred-dead — a predicate whose result is already fixed by an existing arm lands when it bites, not ahead of it | Accepted |
| 0025 | Dashboard v4: a bundled Preact client reconciling from same-origin read-only JSON — supersedes 0019's maud server-render mechanism (its IA + honesty axes survive); view_model/props retained as the serde JSON contract, bundle built-from-source + gitignored, honesty stays server-derived | Accepted (its server-rendered strip/nav superseded in part by 0027 — the body is now root-only) |
| 0026 | Promote qwen3:1.7b as the adjudication judge (bakeoff: 12/12, the only clean sweep of all three evidence types + every refute; deployed qwen2.5:3b-instruct is 11/12, misses exposed-secret-in-field) — pending Pi latency/RAM validation, strict-JSON on-Pi, the delta-aware prompt path, and in-cluster zero-egress availability | Proposed |
| 0027 | Dashboard: the server emits a ROOT-ONLY shell (<head> + #dash-root); the status strip + tab nav move to the Preact client — supersedes 0025's server-rendered strip/nav. Honesty preserved (blank ≠ green; the all-clear/watching/judging-state tokens stay server-derived). Also fixes the reversed-args setInterval bug (dead poll + blank tab-swaps + CSP eval violation) with the CSP kept strict; SSR/hydration deferred |
Accepted |
| 0028 | Dashboard client: local state by default — App holds the 5 shared fields (+ the callback-decoupled poll) as plain useState, the hand-rolled store + reconcile tombstone are deleted, expansion/disclosure is local & ephemeral (native <details>; sessionStorage persistence dropped), keyed removal replaces the tombstone (a future cleared-cue is server-shipped), and the npm deps prune to build+test only (zero runtime). Extends 0025/0027 (both stand); the poll/CSP fix + strip persistence + server-derived honesty are retained |
Accepted |
| 0029 | The adjudicating model's verdict is authoritative: NO deterministic guards that override/second-guess its breach judgement, and NO capping/summarizing the evidence to steer it — the full enriched chain goes to the model and its call stands. Rare false-exploitable flips on huge borderline entries are a temp-0 tail event (diagnosed: not a config/model/prompt bug), accepted as a known cost under shadow-first; addressed only at the model layer (bakeoff-evaluated), never a verdict gate. Reaffirms 0013 (model decides) + 0016 (shadow-first). Does not remove the anti-fabrication guard_fabricated_cve (output grounding, not a judgement override) |
Accepted |
| 0030 | App-level, provider-agnostic OIDC verification supersedes edge-only trust: the dashboard + /api/*.json today carry only CSP (auth trusted entirely at the Cloudflare Access edge), so one kubectl port-forward reads the whole graph. Protector becomes an OAuth RESOURCE SERVER (verifies sig/iss/aud/exp/nbf, alg pinned to the issuer's asymmetric family — never from the token's own alg; extracts subject + a configurable tier) against a CONFIGURABLE issuer; it ships NO IdP (Dex/Keycloak out of scope; supersedes the in-cluster-Dex idea) and cannot actuate. ID-JAG is just a JWT the same verifier accepts. The JWKS/discovery GET is the ADR-0015 same-request-for-everyone lane (public keys in, no cluster datum out), not an egress breach. FAIL-CLOSED when configured (every error → 401/403/503, JWKS-unreachable is 503 not bypass); UNCONFIGURED behaves as today but logs loudly — the only bypass, and it announces itself. CF Access already issues a verifiable JWT, so pointing the verifier at the CF issuer closes the hole with no new interactive flow. Reaffirms 0016 (auth gates WHO MAY VIEW, not the verdict path) |
Proposed |
| 0031 | A read-only, tiered-redaction MCP server — the second sanctioned egress carve-out (pull-side sibling of 0018's push notifier). Four read-only tools (list_findings / explain_verdict / get_coverage / signing_inventory); NO actuation tool exists by construction (view-never-a-gate / shadow-first, 0016). Three tiers redacted/forensic/raw with the tier a server-enforced CEILING from a verified ADR-0030 claim (arg may only narrow) — redacted safe-by-construction, forensic/raw off-by-default, opt-in, journaled operator-owned egress; secret VALUES have no unlock tier. Redaction is server-side/in-cluster BEFORE egress, so protector IS the remote HTTP MCP server (reusing 0018's lifted scrubbers). Transport: RMCP behind our OIDC verifier (single-pathed, in-tree trust decisions; hand-rolled JSON-RPC fallback if it won't compose), ID-JAG protected-resource discovery for zero-touch enterprise auth. Depends on 0030 |
Proposed |
| 0032 | The model is the incident responder: over the whole internet-facing path it decides what is an attack and which cut to apply — choosing from a menu that determinism ENUMERATES — at minimum scope. Determinism proves/enriches/feeds/bounds; it does NOT decide the cut. The model now SEES per-node downstream evidence (was entry-only); the is_live_corroborated unconditional QuarantineWorkload auto-fire is deleted; internal-only actively-exploited pods become propose-only. Supersedes 0022's decision procedure (its vocabulary/shapes/ladder survive as the menu + proposal fallback); evolves 0009 (adjudicator veto → cut selector); realizes the north-star model-decides-the-cut shift. Judge tier deferred to 0033 (do NOT assume qwen3:1.7b; expected qwen3:4b). Realizes the VISION north star |
Proposed |
| 0034 | The cut-choice contract: refines 0032 §3 from a mechanism-menu to target-choice — the model outputs {assessment: attack/no_attack/uncertain, reason, contain:[node-key…]}, naming the compromised on-path nodes; determinism resolves each to its narrowest legal cut (the containment_for ladder / QuarantineWorkload), because per-target minimality is monotone/deterministic (no judgment to delegate) and a node-key is what a 1.7B can reliably copy from attended evidence. Collapses the 4-value verdict → 3-value assessment. Menu rendered as advisory input (mechanism + blast-radius); guards (menu-membership + per-node containment-grounding) downgrade to Uncertain, never Refuted; skeptic default is inert both ways (a model outage neither cuts nor lifts a standing cut); journal v2 stores the resolved cut_signature+fingerprint so replay can't repoint. Chosen for 1.7B-viability (re-scope: 1.7b-first); residual risk is grounded over-cut, measured by the T2b temp-0.8 bench. Supersedes 0032 §3 |
Proposed |
| 0036 | Disarm is a real, fast kill switch: the self-revert loop now reverts a standing cut when its OWN action class is no longer armed (not just on health/chain retirement, closing the enforce→audit orphaned-cut gap); a break-glass flag file (fixed mount path, presence-only, no content parsed) clamps actuation to dry-run and drives every standing cut to revert within one pass, with no image rebuild and no GitOps sync — narrows only, never arms wider than mode/enforceScope already permit. Chosen over a local admin endpoint: no new listener/auth surface, keeps working even if the dashboard/mesh/OIDC path is itself down |
Accepted |
| 0037 | Shadow-bake arm-readiness: the human-read exit criterion (bake duration, zero unexplained model-over-cut on a clean workload, 0033's bench threshold, a model-under-cut spot-check, continuous coverage) an operator reads from the model-vs-deterministic cut-divergence comparator before the single ADR-0021 enforce flip, narrowest ADR-0035 rung first — the comparator only informs; nothing here or in code auto-arms | Proposed |
| 0040 | Node-scoped containment is a deterministic mechanism escalation of a model-decided target: when typed evidence proves the adversary broke the pod boundary (host-cred read; root + escape edge; ptrace/module-load; or ≥2 model-confirmed actively-exploited pods co-resident), the resolver escalates the model-named workload's cut from a pod NetworkPolicy to ContainNode (cordon + co-resident default-deny) — the model keeps what, determinism owns how (rejects a model-selectable node menu line: highest blast on the weakest measured axis). Own node action class at ladder rung 3 (above quarantine), propose-first by construction (a node always has alive collateral), reversible + ownership-marked + one-node-cap + control-plane-excluded + worker-floor; posture-derived nodes RBAC; ships shadow-first (trigger/proposal, then actuator). Also records: adversary-reach is a presentation annotation, never judge context (context-not-evidence enforced by absence from the prompt), and off-path auto-containment is a won't-build (reaffirms 0032 §6, strengthened by 0038). Refines 0032/0034; reuses 0010/0017/0021/0035/0036 rails | Proposed |
| 0041 | Narrow the blanket notable-exec corroboration arm to shapes: a bare interactive-shell/pkg-mgr ProcessExec stops blanket-corroborating every objective (Behavior::ProcessExec → false in the flat arm — symmetric with PrivilegeChange/PtraceAttach/ModuleLoad, restoring the 0011 on-call-engineer FP guard), and exec-based corroboration re-homes in an entry-scoped reverse_shell_on_foothold shape (interactive-shell exec + internet NetworkConnection within a symmetric 60s window, foothold-gated) — unmasking the deferred reverse-shell shape per 0024. Corrects the framing: the model never sees corroborated (removed from the prompt by 0034); narrowing buys deterministic auto-apply-gate precision + honest latent/live presentation + 0024 unmasking, NOT model-input precision. Safe now because Falco's Alert arm still backstops (falco.enabled: true), pre-arming in audit. One real behavior change (inert under audit): a shell-only + decisive-attack incident becomes a proposal not an auto-cut in enforce (0011-correct). Package managers excluded (always egress); is_alarming_now/menu seeding untouched; a narrowing-delta counter feeds the retire-Falco parity bake (0037). Amends 0009's action-bar input | Proposed |
See also ../VISION.md for the longer-form narrative this ADR realizes.