Add SecurityPractice.md, Security Genome record, and deny-escalation lockout

Design doc for hardening qonto-assistant before deployment to
railiance01: this is the first fleet service that must be
internet-reachable (external harness clients, not just in-cluster
jobs) while holding a real bank credential. Covers identity (key-cape
in place of the interim bearer token), authorization (finance.qonto.read
in flex-auth + tenant-engine capability roles instead of the
hardcoded default_tenant_id), network exposure (facade-only internet
address), isolation profile, and a Kings Guard mapping (the existing
audit stream is already Immune-Observation-shaped; nothing to rebuild
later).

Ships one concrete, dependency-free piece of that design now:
DenyEscalationTracker locks out an actor who repeatedly triggers
arg_constraint/credential_exfil denies within a short window, closing
the gap where a probing client could retry indefinitely at whatever
rate the existing rate limiter otherwise allows. Wired through
CapabilityService, on by default, configurable via
QONTO_DENY_ESCALATION_* env vars. Ordinary denies (authz_denied,
tenant_scope) never count toward it.

Also adds specs/security-genome.yaml (kings-guard's genome-record
shape, populated now so no rework is needed once a consumer exists).

Verified: pytest -> 39 passed (8 new); REST and MCP smoke scripts both
pass against fixtures; compileall clean.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
tegwick 2026-07-23 22:59:06 +02:00
parent 78eb2a819c
commit 3faf1fed71
9 changed files with 655 additions and 1 deletions

View file

@ -206,3 +206,16 @@ That consumer-side write remains outside this repo.
- The service supports a `bearer` auth mode for future upstream evolution, but
the current dogfood path remains `legacy_api_key` because that is the proven
BINKY-WP-0005 header mode.
## Deny-escalation lockout
On by default (`QONTO_DENY_ESCALATION_ENABLED=true`). An actor who triggers
`arg_constraint` or `credential_exfil` policy denials `QONTO_DENY_ESCALATION_THRESHOLD`
times (default 3) within `QONTO_DENY_ESCALATION_WINDOW_SECONDS` (default 60s)
is locked out for `QONTO_DENY_ESCALATION_LOCKOUT_SECONDS` (default 300s) —
every request from that actor is rejected with `actor_locked_out` before the
policy kernel is even consulted, regardless of which capability they call
next. Ordinary denies (`authz_denied`, `tenant_scope`, `unknown_capability`)
never count toward this — only the two reason classes that indicate
probing/exfiltration rather than a client mistake. See
`docs/SecurityPractice.md` §9.3 and `src/qonto_assistant/security_watch.py`.