Decision records

ADR-0013: Deterministic security engine (first stage)

  • Status: Accepted (2026-10-05)

Context

Handoff §11: deterministic protocol/application rules first, modular so a mature WAF can be evaluated later; rate limits with tenant/app/route/source dimensions without unbounded in-memory key growth; slowloris and exhaustion protection.

Decision

  • Path rules: deny_paths from the application’s profile plus configuration, matched case-insensitively on the normalised (decoded, dot-segment-free) path. A pattern without * matches the path or anything below it (segment-aligned); * matches any sequence of characters, including /. Denied requests get 404 (no confirmation that the resource exists) and are counted.
  • Per-source limits (server.security.per_ip): concurrent connections (checked at accept, connection closed immediately) and a request token bucket (429). IPv4 addresses are tracked individually, IPv6 per /64. State lives in 64 shards with a fixed total capacity; when a shard is full the least recently seen idle entry is evicted, and if every entry is in use the new source is admitted untracked (fail open, counted) — the global connection limit still applies. Memory is therefore bounded regardless of the number of attacking addresses.
  • Slow readers: a write timeout on the connection (limits.write_timeout) closes a connection whose writes make no progress.
  • Strict SNI (listeners[].strict_sni): a request whose Host differs from the TLS SNI is answered 421 Misdirected Request (clients then open a new connection), preventing domain fronting across tenants.
  • Rules are evaluated before routing to handlers and before the cache.

Consequences

  • No trusted-proxy handling yet: behind a load balancer, per-IP limits see the balancer’s address. Configuring trusted proxies is follow-up work.
  • A rule engine for request content (OWASP CRS-class) is out of scope for this stage.