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

194 lines
7.9 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

---
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"
state_hub_workstream_id: "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
```task
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
```task
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
```task
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
```task
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
```task
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
```task
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
```task
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`.