feat(shadow): global shadow mode — log/record mitigations, act on nothing #187

Merged
vxfemboy merged 6 commits from sp-shadow-mode into main 2026-07-11 19:44:43 +00:00
vxfemboy commented 2026-07-11 19:38:35 +00:00 (Migrated from github.com)

What

Deployment backlog #9 (issue #178, milestone M0): an opt-in shadow mode that makes the mitigation plane log + record + meter every RTBH/FlowSpec/XDP mitigation it would apply, without executing it — the interlock that makes a detection-only deployment safe to run live on the security boundary.

How it works

Shadow decorates the execution boundary only — detection, selection, and controller logic (caps, hold-down, eligibility) all run identically; only the final side effect is suppressed and recorded.

  • RTBH + FlowSpec: a ShadowBgpExecutor (impl BgpExecutor, holds no BGP handle — structurally can't announce) + NoOpJournal. The real iBGP session is not even spawned in shadow.
  • XDP: a ShadowXdpExecutor gate (records intended block/rate-limit, skips the eBPF map write) + NoOpXdpJournal; rehydrate skipped.
  • Config: a bare shadow directive → Policy.shadow (default false, non-breaking).
  • Recording: INFO log + blackwall_shadow_would_mitigate_total{plane,action} metric + structured audit_log rows (via new Store::record_audit), surfaced by the existing /v1/audit endpoint. Startup WARN: SHADOW MODE banner.

Safety

The whole point is that no real mitigation happens in shadow, and the persistent mirror stays empty so a later shadow→live restart can't rehydrate never-vetted entries into real writes — verified exhaustively in the final review across all three planes, both seams each (executor + journal), auto and manual paths. Non-breaking: absent shadow ⇒ live wiring byte-equivalent.

Quality

Subagent-driven: 4 tasks each spec+quality reviewed + a final opus whole-branch review. The final review's one Critical (XDP journal not no-op'd → shadow→live promotion hole) was fixed before merge, plus two polish items (structured audit JSON, hardened live-BGP wiring). clippy --workspace --all-targets --deny warnings clean; fmt clean; coverage 95.33%. No as casts, #[expect] not #[allow].

## What Deployment backlog **#9** (issue #178, milestone **M0**): an opt-in `shadow` mode that makes the mitigation plane **log + record + meter every RTBH/FlowSpec/XDP mitigation it would apply, without executing it** — the interlock that makes a detection-only deployment safe to run live on the security boundary. ## How it works Shadow decorates the *execution boundary only* — detection, selection, and controller logic (caps, hold-down, eligibility) all run identically; only the final side effect is suppressed and recorded. - **RTBH + FlowSpec**: a `ShadowBgpExecutor` (impl `BgpExecutor`, holds no BGP handle — structurally can't announce) + `NoOpJournal`. The real iBGP session is not even spawned in shadow. - **XDP**: a `ShadowXdpExecutor` gate (records intended block/rate-limit, skips the eBPF map write) + `NoOpXdpJournal`; rehydrate skipped. - **Config**: a bare `shadow` directive → `Policy.shadow` (default false, non-breaking). - **Recording**: INFO log + `blackwall_shadow_would_mitigate_total{plane,action}` metric + structured `audit_log` rows (via new `Store::record_audit`), surfaced by the existing `/v1/audit` endpoint. Startup `WARN: SHADOW MODE` banner. ## Safety The whole point is that **no real mitigation happens in shadow**, and the **persistent mirror stays empty** so a later shadow→live restart can't rehydrate never-vetted entries into real writes — verified exhaustively in the final review across all three planes, both seams each (executor + journal), auto and manual paths. Non-breaking: absent `shadow` ⇒ live wiring byte-equivalent. ## Quality Subagent-driven: 4 tasks each spec+quality reviewed + a final opus whole-branch review. The final review's one Critical (XDP journal not no-op'd → shadow→live promotion hole) was fixed before merge, plus two polish items (structured audit JSON, hardened live-BGP wiring). clippy `--workspace --all-targets --deny warnings` clean; fmt clean; coverage 95.33%. No `as` casts, `#[expect]` not `#[allow]`.
Sign in to join this conversation.
No description provided.