Skip to content

ADR 0044 — OOBI Intrusion Detection + Blacklist

  • Status: Accepted (2026-05-14)
  • Date: 2026-05-14
  • Deciders: TLSStress.Art project (operator directive 2026-05-14 second message)
  • Builds on: ADR 0038 (DSCP), ADR 0043 (OOBI VLAN), pkg/oobi/auth/ (Bearer-token L2)
  • Targets: v4.0.0

Context

ADR 0043 moved OOBI from a VXLAN overlay to a traditional 802.1Q VLAN (ID 2777, 100.127.252.0/22). This brought clear simplification but also collapsed the previous "topology-independent" guarantee: now any L2-adjacent host on VLAN 2777 can send packets to any MÓDULO. The attack surface expands by exactly one threat actor: an operator who plugs a laptop into a Nexus port carrying VLAN 2777 and assigns themselves an IP from the canonical /22.

The operator captured the requirement verbatim:

Precisamos garantir que os modulos/kernels/runtimes só aceitem comunicacao interna lateral vinda de outro modulos/kernels/runtimes conhecidos pelo nosso software. Se algum operador inserir inadvertidamente na rede OOBI um Laptop dentro da VLAN 2777 e configurar manualmente o endereço IP do seu laptop como sendo algum endereco da rede IP 100.127.252.0/22 (range IP definido por diretiva), mesmo assim, todos elementos modulos/kernels/runtimes não devem aceitar conectividade com esse laptop invasor. Ao tentar receber esse tipo de conexao, o modulos/kernels/runtimes que observou essa tentativa deve emitir um alerta (do tipo push/Webhook, syslog, ou alguma outra maneira) de tentativa de invasao deve ser gerado no Dashboard, no grafana e no Prometheus. O Mac Address de IP Address do laptop invasor deve ser registrado e automaticamente cadastrado em uma black list da rede OOBI.

Decision

Introduce pkg/oobi/intrusion/ — a new middleware that runs BEFORE pkg/oobi/auth/ on every MÓDULO HTTP listener. The filter enforces three invariants on every incoming connection:

  1. Source IP inside oobi.IPv4SubnetParent (100.127.252.0/22). Out-of-subnet ⇒ out_of_subnet rejection.

  2. When the IP is in the slot /24 (100.127.252.0/24), the host octet must correspond to a slot that pkg/oobi/canon currently allocates. Unknown slot ⇒ unknown_slot rejection. (IPs in the parent /22 but outside the slot /24 are HPA pool / HA replicas / reserved — they pass this check and are policed at the K8s NAD level by PR-OOBI-VLAN-3.)

  3. Source IP not present in the shared blacklist. Blacklisted ⇒ blacklisted rejection.

Any failure produces an immediate HTTP 403 with no body (no information leak to the attacker), captures the attacker's MAC via /proc/net/arp, persists the entry in a local + shared blacklist, and dispatches alerts to four destinations:

  • Prometheus counter oobi_intrusion_attempts_total{reason, detected_by} (cardinality-bounded — IP and MAC NOT in labels)
  • Dashboard webhook POST /api/security/intrusion
  • Grafana panel via Prometheus scrape (lands in dashboards/oobi-intrusion.json)
  • Syslog LOG_ALERT + LOG_AUTH for SIEM / customer SOC

Alerters are fire-and-forget goroutines — alert delivery NEVER blocks the request path.

Why a separate package, not extension of pkg/oobi/auth/

Two responsibilities, two libraries:

Layer Question Implementation
pkg/oobi/intrusion/ Is this peer topologically plausible? Source IP filter, blacklist, MAC capture
pkg/oobi/auth/ Did this peer prove identity? Bearer-token timing-safe compare

Diverging the two:

  • Lets us count rejections separately (oobi_intrusion_attempts_total vs auth counters)
  • Returns distinct HTTP codes (403 vs 401) which Wireshark and Grafana panels can split on
  • Cheap L3 rejection runs first; expensive Bearer crypto only on plausible peers
  • Future enhancements (rate-limiting, geo-blocking, ML anomaly detection) belong in the intrusion package, not auth

Middleware order — locked

handler := intrusionM.Wrap(authM.Wrap(realHandler))

Reverse order would let the attacker exhaust auth crypto cycles before being rejected. Order is enforced by convention in every MÓDULO main.go (PR-OOBI-VLAN-3b wires it everywhere; subsequent PRs include a go vet check or static-analysis rule).

Blacklist propagation

Layer Persistence
In-process intrusion.Blacklist struct, atomic snapshot
Per-host /var/run/oobi/blacklist.json, atomic rewrite on every Add
Cross-MÓDULO Dashboard receives webhook → writes Postgres oobi_intrusion_blacklist table → updates ConfigMap oobi-blacklist → MÓDULOs reload via Refresh() every 30 s

The 30 s reload window is the maximum time between "MÓDULO A detected attacker" and "MÓDULO B begins blocking the same attacker". For most threat models this is acceptable; the Dashboard can publish a SSE stream for sub-second propagation in PR-OOBI-VLAN-6 if needed.

Operator override path

The Dashboard exposes /admin/security/oobi-intrusion (lands in PR-OOBI-VLAN-3c). The operator can:

  • View the blacklist sorted by last_seen desc
  • Remove an entry (creates audit log row in audit_actions with kind=intrusion_unblock, who=<operator>, why=<free-text>)
  • Export to CSV/JSON for offline SIEM ingestion
  • Click through to the Grafana panel filtered by detected_by

Consequences

Positive

  • Defense in depth: ZTP-prem layer-3 (network segmentation) is no longer the only barrier — an L2-pivoting attacker still has to guess a valid slot AND a valid Bearer token.
  • Audit trail: every attempt is captured with IP + MAC + reason
  • timestamp + which MÓDULO detected it. Forensics gain hard evidence even when the attack fails.
  • SIEM integration: syslog ALERT priority is the standard hook; customer SOC tools (Splunk, QRadar, ELK) consume it out of the box.
  • Bounded cardinality: Prometheus labels are reason × detected_by only. A brute-force probe can generate at most 3 × N_modules ≈ 111 unique label combinations.

Negative

  • 30 s blacklist propagation window: an attacker hitting MÓDULO A then immediately MÓDULO B sees up to 30 s of "open door" on B before the ConfigMap refresh. Acceptable for v1; SSE / push propagation lands in PR-OOBI-VLAN-6 if needed.
  • MAC capture is Linux-only: macOS / Windows fall back to empty MAC. Dashboard handles missing MAC gracefully (renders "—") but forensic value is reduced on non-Linux hosts.
  • False positives on operator-managed peers: any host that legitimately speaks OOBI but is NOT in the slot allocation will be blocked. Examples: ad-hoc operator scripts, ansible runners. Those must use the same Bearer token + an allocated slot (operator signs in via Dashboard which proxies through a designated slot).
  • No active L2 isolation: the filter rejects at L3; the laptop is still on the VLAN. Active port-shutdown via Nexus SNMP/NETCONF is a separate controller, out of scope for the application layer. PR-OOBI-VLAN-6+ may add an optional controller hook.

Implementation plan (3 PRs)

PR Scope Hours Status
PR-OOBI-VLAN-3a pkg/oobi/intrusion/ library + 30+ unit tests + ADR + README ~6 #735 merged 2026-05-14
PR-OOBI-VLAN-3b Wire intrusionM.Wrap in every MÓDULO main.go (8 binaries) ~3 #736 merged 2026-05-14
PR-OOBI-VLAN-3c Dashboard /admin/security/oobi-intrusion page + POST /api/security/intrusion endpoint + Postgres table + Grafana dashboard JSON + Prometheus alert rules + runbook ~8 #896 merged 2026-05-18

Operator surface (post-PR-3c)

Artifact Path
Dashboard page /admin/security/oobi-intrusion
Webhook endpoint POST /api/security/intrusion (OOBI Bearer auth)
Admin list / remove GET / DELETE /api/admin/security/oobi-intrusion
SIEM export GET /api/admin/security/oobi-intrusion/export?format=json\|csv
Postgres table oobi_intrusion_events (migration 0029)
Grafana dashboard observability/grafana/dashboards/oobi-intrusion.json
Prometheus alerts observability/prometheus/alerts/oobi-intrusion.yml (Burst / UnknownSlot / BlacklistGrowing / SilentMÓDULO)
Runbook pkg/octopus/docs/05-operations/runbooks/oobi-intrusion.md (3 langs)

References

  • Memo: project_oobi_intrusion_detection_2026_05_14.md
  • Memo: project_oobi_no_vxlan_directive_2026_05_14.md
  • ADR 0038: Universal DSCP Marking Policy
  • ADR 0043: OOBI VLAN (no VXLAN) Refactor
  • pkg/oobi/intrusion/ source
  • pkg/oobi/auth/ source (complementary L2 layer)