2026-07-23 22:59:51 +02:00
|
|
|
|
---
|
|
|
|
|
|
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"
|
2026-07-23 23:00:18 +02:00
|
|
|
|
state_hub_workstream_id: "81a476ac-06d5-4e98-a2ed-1518b16863aa"
|
2026-07-23 22:59:51 +02:00
|
|
|
|
---
|
|
|
|
|
|
|
|
|
|
|
|
# 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 23:00:18 +02:00
|
|
|
|
state_hub_task_id: "e93b2027-dd28-464b-8ea3-f7540f91caad"
|
2026-07-23 22:59:51 +02:00
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
**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 23:00:18 +02:00
|
|
|
|
state_hub_task_id: "3b10dd59-b03f-4d2b-8164-aa2a1c8c06c4"
|
2026-07-23 22:59:51 +02:00
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
**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
|
2026-07-24 00:21:04 +02:00
|
|
|
|
status: done
|
2026-07-23 22:59:51 +02:00
|
|
|
|
priority: high
|
2026-07-23 23:00:18 +02:00
|
|
|
|
state_hub_task_id: "f02b1cc0-4afc-44e5-b71d-545a13a3e12b"
|
2026-07-23 22:59:51 +02:00
|
|
|
|
```
|
|
|
|
|
|
|
2026-07-24 00:21:04 +02:00
|
|
|
|
**2026-07-24:** No `key-cape`-side coordination was actually needed — its
|
|
|
|
|
|
`/jwks` is a standard RS256 endpoint. `KeyCapeTokenVerifier`
|
|
|
|
|
|
(`src/qonto_assistant/key_cape_auth.py`) fetches/caches signing keys over
|
|
|
|
|
|
httpx, validates `iss`/`aud`/`exp` and the IAM Profile v0.3 required claims,
|
|
|
|
|
|
and derives `ActorClaims` from the token. Wired into
|
|
|
|
|
|
`auth.actor_claims_from_headers` (the single seam both REST and MCP use); a
|
|
|
|
|
|
verified bearer token now takes precedence over self-asserted `X-Actor-*`
|
|
|
|
|
|
headers, and can be made mandatory via `QONTO_KEY_CAPE_REQUIRED` once real
|
|
|
|
|
|
tokens are issued to callers. Off by default (no
|
|
|
|
|
|
`QONTO_KEY_CAPE_JWKS_URL` set). `QONTO_ASSISTANT_MCP_TOKEN` remains a
|
|
|
|
|
|
documented local-dev/legacy fallback per `docs/mcp-integration.md`.
|
|
|
|
|
|
|
|
|
|
|
|
Verified: 13 new tests using a real generated RSA keypair + JWKS served
|
|
|
|
|
|
over `httpx.MockTransport` (valid/expired/wrong-audience/wrong-issuer/
|
|
|
|
|
|
missing-claim/unknown-key/rotated-key tokens, plus the auth.py precedence
|
|
|
|
|
|
and required-vs-optional paths).
|
2026-07-23 22:59:51 +02:00
|
|
|
|
|
|
|
|
|
|
## Task: Register and enforce `finance.qonto.read` in flex-auth
|
|
|
|
|
|
|
|
|
|
|
|
```task
|
|
|
|
|
|
id: QONTO-WP-0004-T04
|
2026-07-24 00:21:04 +02:00
|
|
|
|
status: done
|
2026-07-23 22:59:51 +02:00
|
|
|
|
priority: high
|
2026-07-23 23:00:18 +02:00
|
|
|
|
state_hub_task_id: "fce7bf0e-674a-461f-8f2f-42d7dde3a722"
|
2026-07-23 22:59:51 +02:00
|
|
|
|
```
|
|
|
|
|
|
|
2026-07-24 00:21:04 +02:00
|
|
|
|
**2026-07-24:** Registered `qonto-assistant` in `flex-auth`
|
|
|
|
|
|
(`flex-auth/examples/qonto-assistant/`, modeled directly on
|
|
|
|
|
|
`examples/tenant-engine/`): one resource type (`finance-snapshot`), one
|
|
|
|
|
|
action (`finance.qonto.read`), a Rego policy gating on
|
|
|
|
|
|
`resource.system` + `action` + `subject.type` + tenant match. Tenant
|
|
|
|
|
|
capability-role/plan liveness (`VEN`/`CUS`) is deliberately *not* encoded in
|
|
|
|
|
|
that policy — kept as a separate `tenant-engine` live-lookup check, per
|
|
|
|
|
|
both repos' policy-scope notes.
|
|
|
|
|
|
|
|
|
|
|
|
`LiveAuthorizationGate` (`src/qonto_assistant/live_authorization.py`)
|
|
|
|
|
|
combines `FlexAuthCheckClient.is_allowed()` (live `POST /v1/check`) and
|
|
|
|
|
|
`TenantEngineClient.active_roles()` (live `GET
|
|
|
|
|
|
/tenants/{id}/roles/live`, gated on `QONTO_TENANT_ENGINE_REQUIRED_ROLES`,
|
|
|
|
|
|
default `VEN,CUS`) and is wired into `CapabilityService._execute` ahead of
|
|
|
|
|
|
the internal policy kernel. Both clients fail closed by construction.
|
|
|
|
|
|
Replaces `QONTO_ASSISTANT_ENFORCE_SCOPE`'s cached-claim check as the live
|
|
|
|
|
|
authorization control point; off by default (no `QONTO_FLEX_AUTH_URL` set).
|
|
|
|
|
|
|
|
|
|
|
|
Verified beyond mocked unit tests: ran a real `flex-auth serve` loaded with
|
|
|
|
|
|
the registered policy (`flex-auth test-policy`/`load-registry`/`check` all
|
|
|
|
|
|
pass) and a real `tenant-engine` instance seeded with a `VEN` grant for
|
|
|
|
|
|
`tenant:friendly:binky`, and exercised this repo's actual clients against
|
|
|
|
|
|
both live processes over real HTTP — `allow` for the correct tenant,
|
|
|
|
|
|
`live_authz_denied` for a mismatched one. 28 new unit tests.
|
2026-07-23 22:59:51 +02:00
|
|
|
|
|
|
|
|
|
|
## Task: Facade / scale-to-zero activator — design and reference implementation
|
|
|
|
|
|
|
|
|
|
|
|
```task
|
|
|
|
|
|
id: QONTO-WP-0004-T05
|
|
|
|
|
|
status: todo
|
|
|
|
|
|
priority: high
|
2026-07-23 23:00:18 +02:00
|
|
|
|
state_hub_task_id: "e8a87292-488d-4871-be3e-59a5b6a38694"
|
2026-07-23 22:59:51 +02:00
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
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
|
2026-07-23 23:00:18 +02:00
|
|
|
|
state_hub_task_id: "7cd52269-ca0a-4b09-ab51-f02ed71604f2"
|
2026-07-23 22:59:51 +02:00
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
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
|
2026-07-23 23:00:18 +02:00
|
|
|
|
state_hub_task_id: "2a2da206-d182-4a72-aea4-56e58ae74c76"
|
2026-07-23 22:59:51 +02:00
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
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`.
|