qonto-assistant/workplans/QONTO-WP-0004-security-hardening-and-scale-to-zero.md
tegwick 21c98fc062 chore(consistency): write back QONTO-WP-0004 hub IDs
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-23 23:00:18 +02:00

7.9 KiB
Raw Blame History

id type title domain repo status owner topic_slug created updated state_hub_workstream_id
QONTO-WP-0004 workplan Security hardening and scale-to-zero facade for internet exposure infotech qonto-assistant active claude the-custodian 2026-07-23 2026-07-23 81a476ac-06d5-4e98-a2ed-1518b16863aa

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

id: QONTO-WP-0004-T01
status: done
priority: high
state_hub_task_id: "e93b2027-dd28-464b-8ea3-f7540f91caad"

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

id: QONTO-WP-0004-T02
status: done
priority: high
state_hub_task_id: "3b10dd59-b03f-4d2b-8164-aa2a1c8c06c4"

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

id: QONTO-WP-0004-T03
status: todo
priority: high
state_hub_task_id: "f02b1cc0-4afc-44e5-b71d-545a13a3e12b"

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

id: QONTO-WP-0004-T04
status: todo
priority: high
state_hub_task_id: "fce7bf0e-674a-461f-8f2f-42d7dde3a722"

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

id: QONTO-WP-0004-T05
status: todo
priority: high
state_hub_task_id: "e8a87292-488d-4871-be3e-59a5b6a38694"

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

id: QONTO-WP-0004-T06
status: todo
priority: medium
state_hub_task_id: "7cd52269-ca0a-4b09-ab51-f02ed71604f2"

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

id: QONTO-WP-0004-T07
status: todo
priority: low
state_hub_task_id: "2a2da206-d182-4a72-aea4-56e58ae74c76"

Close when T03T06 land or are explicitly deferred with a recorded reason. T01/T02 already shipped without waiting on external dependencies. Run statehub fix-consistency.