Decisions

ADR-026: Security defence-in-depth — layered controls, no single point of protection

The threats enumerated in [security-threat-model.md](../security-threat-model.md) span a wide surface: auth, authz, input validation, output encoding, transport, secret handling, audit, dependencies, container…

#026

ADR-026: Security defence-in-depth — layered controls, no single point of protection

Status: Accepted Date: 2026-04-21

Context

The threats enumerated in security-threat-model.md span a wide surface: auth, authz, input validation, output encoding, transport, secret handling, audit, dependencies, container hardening, infrastructure. No single control covers all of them; no single control covers even one category reliably under motivated attack. A compromise at any one layer should not produce a total compromise.

The redaction library's design (ADR-007) already models this: three layers (key, value, message) provide ~75% coverage; the compliance control is "don't log that data", the redactor is a safety net. We apply the same mental model fleet-wide: every class of threat gets at least two controls, and at least one of those controls is a detective control (security-tripwires.md).

The temptation at small scale is to rely on one big control (WAF, managed auth service, penetration test). Those are fine additions; as sole defences they are single points of failure. The industry postmortem archive is full of breaches enabled by "the WAF would have caught it (it didn't)" and "the auth service handles it (it had a bug)".

This ADR is the meta-commitment underneath every other security ADR.

Decision

Security posture is layered. Every class of threat in security-threat-model.md must have at least two controls: one preventive (stops the threat) and one detective (surfaces the threat if prevention fails).

Tiering (Tier-1 ships in this tranche; Tier-2 and Tier-3 are commitments for subsequent tranches):

Tier-1 — prevent + detect, shipped now

Tier-2 — hardening, next quarter (by 2026-07-21)

  • Container hardening: SHA-pinned base images, hardened seccomp profile, read-only root fs where feasible.
  • Image signing: cosign signatures on every built image; deploy-time verification.
  • Dependency vuln scan: govulncheck + npm audit in CI; trivy on Dockerfile; Renovate for updates (ADR-029).
  • Audit log: separate sink for auth events with 1-year retention, independent access control (standards §auditability).
  • Runtime file-integrity monitoring: watch /deploy/.secrets/, /etc/caddy/, /deploy/config/ for out-of-band modifications.
  • Secret manager: migrate from .env files to a secret manager with audit logs and automated rotation.

Tier-3 — advanced, not scheduled

  • Hardened agent sandboxing: agent tools run in a sandbox with no direct filesystem or network access to production resources.
  • Instruction-injection classifier: AI-adjacent defence against adversarial prompts in user content reaching agent context.
  • External SIEM: log forwarding to a write-only off-VPS store for tamper-evident retention.

No Tier-1 item ships without its detective control. No Tier-1 item ships as "prevent only" — that's the whole point.

Consequences

  • No single control failure causes a breach. An HMAC secret compromise leaks sessions; rotation catches it within a quarter. An authz bug goes out; tripwire 4 catches the probing. A redaction regression lands; tripwire 13 catches the resulting spike. None of these leads cleanly to "attacker has the database".
  • Observability is a security control. observability-architecture.md and this security tranche are inseparable. Tripwires read logs and metrics; logs and metrics are the detective layer. A compromise of the observability stack is a compromise of security. Mitigation: Grafana SSO, ADR-022's external-probe cross-check, and the threat-model row tagging Grafana-as-credential-harvest as a known risk.
  • Tier-1 is the minimum bar. Tier-2 is the one-quarter commitment. A new service ships at Tier-1 minimum; any gap gets filed as security debt with a fix date. Shipping at Tier-0 (no detective control) is not allowed.
  • Controls layer without perfect composability. An authz check that depends on the session cookie's HMAC signature depends on the HMAC key not being forged. If the HMAC key rotates while a user is mid-session, their session is invalidated — intentional, but a UX hit. We accept these edge cases; the alternative is single-layer controls, which have their own UX debt (long session TTLs that outlive employee tenure).
  • Detective controls have a duty cycle. A tripwire that never fires in six months isn't obviously working — might be right (no attacks), might be wrong (bad query). Monthly alert review (alerting-architecture §alert-review) is where we test the detective layer's health, including periodic synthetic red-team tests that intentionally trigger each tripwire.
  • We commit to three tranches. Tier-1 ships now; Tier-2 by July; Tier-3 when resourcing allows. The quarterly security audit (security-audit-2026-04.md is the first) scores against this tiering. Slipping a tranche requires a documented reason and a new ETA.

Alternatives considered

  • Single-layer "strong" controls (e.g., managed auth with no local logic). Rejected: single-layer controls fail loudly when they fail at all. A managed auth bug is the same outage as a self-rolled auth bug, and the managed service's opacity makes postmortem harder. Layering is cheaper than depending on a vendor's bug-free record.
  • WAF as the top-layer prevention for everything. Rejected: Cloudflare / AWS WAF is useful and not controversial to add later, but it is not a substitute for correct authz or correct input validation. The WAF catches what we forgot to handle; it cannot catch everything. We'd rather the primary code be correct and the WAF be a bonus layer (roadmapped as Tier-3).
  • Compliance framework adoption (SOC 2, ISO 27001) driving control choice. Rejected for now: we are pre-compliance-user, and the controls we need are not ceremony-shaped. When we pursue compliance for a customer deal, many of the Tier-1/Tier-2 items will already be in place; the gap will be documentation, not controls.
  • Continuous red-teaming as the main strategy. Rejected as primary: continuous red-team is a high-leverage add-on once the basics are in place, not a substitute for the basics. Ship the layers; add red-team after Tier-2.

Supersedes: none Superseded by: