diff --git a/workplans/QONTO-WP-0004-security-hardening-and-scale-to-zero.md b/workplans/QONTO-WP-0004-security-hardening-and-scale-to-zero.md new file mode 100644 index 0000000..af5437e --- /dev/null +++ b/workplans/QONTO-WP-0004-security-hardening-and-scale-to-zero.md @@ -0,0 +1,186 @@ +--- +id: QONTO-WP-0004 +type: workplan +title: "Security hardening and scale-to-zero facade for internet exposure" +domain: infotech +repo: qonto-assistant +status: active +owner: claude +topic_slug: the-custodian +created: "2026-07-23" +updated: "2026-07-23" +--- + +# Security hardening and scale-to-zero facade for internet exposure + +Implements `docs/SecurityPractice.md`, written ahead of deploying +`qonto-assistant` to `railiance01`. This is the first fleet service that +must be reachable by clients outside the cluster (laptop-based agent +harness sessions) while holding a real company bank credential — a risk +class above every other agent-facing service shipped so far. + +Some of this workplan's scope depends on systems this repo does not own +(`key-cape`, `flex-auth`, `tenant-engine`, Railiance placement). Those +dependencies are tracked explicitly per task rather than assumed away; the +`kings-guard`-owned portion is tracked as a separate intake against +`KG-WP-0002`, not duplicated here. + +## Task: Security Genome record and audit-stream review + +```task +id: QONTO-WP-0004-T01 +status: done +priority: high +``` + +**2026-07-23:** `specs/security-genome.yaml` written per +`kings-guard/specs/NetKingdomImmuneArchitecture.md` §9.1's schema — +capabilities provided/consumed, expected egress (OpenBao + Qonto API only), +data classification, recovery expectations, and explicit tolerances for +today's known-temporary gaps (shared-secret bearer auth, self-asserted actor +claims). Confirmed `AuditLogger`'s existing event shape (actor, capability, +decision, deny_reason, latency, upstream status, policy version) already +matches the Immune Observation contract closely enough that no schema +rework is anticipated once a real consumer exists. + +Done when: genome record exists and is reviewed against the blueprint +schema; no code changes required for this task. + +## Task: Deny-escalation lockout + +```task +id: QONTO-WP-0004-T02 +status: done +priority: high +``` + +**2026-07-23:** `src/qonto_assistant/security_watch.py::DenyEscalationTracker` +locks out an actor who triggers `arg_constraint`/`credential_exfil` policy +denials `QONTO_DENY_ESCALATION_THRESHOLD` times (default 3) within +`QONTO_DENY_ESCALATION_WINDOW_SECONDS` (default 60s), for +`QONTO_DENY_ESCALATION_LOCKOUT_SECONDS` (default 300s). Wired into +`CapabilityService._execute` ahead of the policy kernel call; on by default +via `QONTO_DENY_ESCALATION_ENABLED`. Ordinary denies (`authz_denied`, +`tenant_scope`, `unknown_capability`) never count. This needed no external +dependency — it closes a real gap using only what already exists (the audit +stream and rate limiter). + +Verified: `tests/test_deny_escalation.py` (8 new tests: tracker unit tests ++ service integration for both the escalating and non-escalating paths); +full suite `pytest` → 39 passed; REST and MCP smoke scripts both pass +against fixtures; `compileall` clean. + +Done when: threshold/window/lockout are configurable, on by default, and +proven not to affect ordinary policy-deny traffic. + +## Task: Replace shared-secret bearer token with key-cape identity + +```task +id: QONTO-WP-0004-T03 +status: todo +priority: high +``` + +Verify `key-cape`-issued IAM Profile tokens at the request boundary (facade +or assistant, depending on where T05 lands the auth check) in place of +`QONTO_ASSISTANT_MCP_TOKEN`. Derive `X-Actor-*`-equivalent claims from the +verified token instead of trusting self-asserted headers — closing the gap +`docs/mcp-integration.md`'s auth-model section calls out explicitly. + +**Depends on:** `key-cape` exposing a client-verifiable token/JWKS surface +this service can validate against — needs coordination with `key-cape`, not +something this repo can complete alone. + +Done when: a request bearing a valid key-cape token is accepted with +claims derived from the token; the shared-secret bearer path is +demoted to fixture/local-dev-only, matching what `docs/mcp-integration.md` +already documents as the intended boundary. + +## Task: Register and enforce `finance.qonto.read` in flex-auth + +```task +id: QONTO-WP-0004-T04 +status: todo +priority: high +``` + +Register `finance.qonto.read` as a `flex-auth` resource. Call `flex-auth` +for a live decision on this resource rather than relying on +`QONTO_ASSISTANT_ENFORCE_SCOPE`'s current cached-claim check — mirroring +`tenant-engine`'s own reasoning that a stale grant is not an acceptable risk +for money-adjacent actions, applied here to read access. Replace the +hardcoded `default_tenant_id="binky"` assumption in `config.py` with a live +`tenant-engine` capability-role + plan lookup (`VEN`/`CUS`) so a lapsed +tenant loses read access on the next request, not whenever a cache expires. + +**Depends on:** `flex-auth` resource registration and a `tenant-engine` +lookup/cache API this repo can call — both external, not something this +repo can complete alone. + +Done when: an unauthorized tenant/role is denied by a live `flex-auth` +decision, not a locally cached scope check; policy tests cover both the +allow and the newly-live-checked deny path. + +## Task: Facade / scale-to-zero activator — design and reference implementation + +```task +id: QONTO-WP-0004-T05 +status: todo +priority: high +``` + +Per `docs/SecurityPractice.md` §2/§6: a thin, always-on facade is the only +internet-facing component. It authenticates (key-cape) and authorizes +(flex-auth pre-check) *before* waking the real service, then scales the +backend `Deployment` 0→1, waits on `/v1/health`, proxies through, and scales +back to 0 after an idle timeout. `qonto-assistant`'s raw REST/MCP port must +never be bound to a publicly-reachable address in any deployment. + +Check whether the target `railiance01` cluster already runs Knative Serving +before building a custom activator — if it does, this task becomes +integration (Knative Service manifest + autoscaling annotations) rather +than new code. If not, scope a minimal reference implementation (one +Service, one Deployment, one wake/idle controller) sized to this repo's +narrow surface, not a general-purpose PaaS. + +**Depends on:** a decision on where this component lives (this repo, +Railiance, or a new dedicated repo) and Railiance cluster capabilities +(Knative or not) — flag both as open questions rather than assuming. + +Done when: an unauthenticated request never triggers a backend wake; an +authenticated+authorized request gets proxied through after a bounded +cold-start; the backend scales back to 0 after the configured idle window. + +## Task: Isolation placement request to Railiance + +```task +id: QONTO-WP-0004-T06 +status: todo +priority: medium +``` + +Per `docs/SecurityPractice.md` §7: request I1 "Reinforced" (dedicated node +pool / sandboxed runtime) at minimum for the `qonto-assistant` deployment on +`railiance01`, given tenant-confidential financial-data classification, +internet reachability via the facade, and sole custody of the bank +credential. I2 "Dedicated" (own namespace/keys) is worth considering; the +placement decision belongs to whoever owns Railiance scheduling for this +workload, not this repo. + +**Depends on:** a Railiance-side placement decision — this task's job is to +make the request concrete and reviewable, not to implement scheduling. + +Done when: a placement decision is recorded (even if the decision is "I0 for +now, revisit before going live") and referenced back into this workplan. + +## Task: Closure review + +```task +id: QONTO-WP-0004-T07 +status: todo +priority: low +``` + +Close when T03–T06 land or are explicitly deferred with a recorded reason. +T01/T02 already shipped without waiting on external dependencies. Run +`statehub fix-consistency`.