qonto-assistant/workplans/QONTO-WP-0004-security-hardening-and-scale-to-zero.md

287 lines
13 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 Knative runtime path for internet exposure"
domain: infotech
repo: qonto-assistant
status: finished
owner: claude
topic_slug: the-custodian
created: "2026-07-23"
updated: "2026-07-27"
state_hub_workstream_id: "81a476ac-06d5-4e98-a2ed-1518b16863aa"
---
# Security hardening and Knative runtime path for internet exposure
Implements `docs/SecurityPractice.md`, written ahead of deploying
`qonto-assistant` to the Railiance runtime fleet. 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.
The intended runtime direction is now explicit: use `reef-railiance`,
establish a new `rail-knative`, and dynamically run a future `rapp-qonto`
package on that rail. This repo remains the current ownership home for the
Qonto domain code and the place where the security contract is first made
concrete, but the remaining deployment-oriented work in this plan should
feed that repo-family path rather than extend a permanent direct
`qonto-assistant` deployment target.
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.
As of Sunday, July 26, 2026, the cross-repo framework direction for that path
is tracked in
`railiance-master/workplans/RMASTER-WP-0019-knative-qonto-runtime-on-reef-railiance.md`.
Tasks already marked `done` below remain valid as ownership-repo hardening and
migration input; the remaining open tasks are now about handing the runtime
path into `reef-railiance`, `rail-knative`, and `rapp-qonto`.
## 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: done
priority: high
state_hub_task_id: "f02b1cc0-4afc-44e5-b71d-545a13a3e12b"
```
**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).
## Task: Register and enforce `finance.qonto.read` in flex-auth
```task
id: QONTO-WP-0004-T04
status: done
priority: high
state_hub_task_id: "fce7bf0e-674a-461f-8f2f-42d7dde3a722"
```
**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.
## Task: Define the `rail-knative` internet-facing runtime contract
```task
id: QONTO-WP-0004-T05
status: done
priority: high
state_hub_task_id: "e8a87292-488d-4871-be3e-59a5b6a38694"
```
Per `docs/SecurityPractice.md` §2/§6, the internet-facing wake path must
authenticate and authorize *before* waking the real service, and the backend
must still scale back to zero when idle. Under the new framework direction,
this repo should no longer assume a custom in-repo facade or a permanent direct
`Deployment` target. The default plan is now a new `rail-knative` hosted by
`reef-railiance`, with this repo describing the app-side contract that rail
must satisfy.
This task should define, from `qonto-assistant`'s point of view:
- the pre-wake authn/authz behavior the rail must guarantee
- the cold-start and idle-scale semantics the package path depends on
- which behavior belongs in `rail-knative` versus the future `rapp-qonto`
package versus this ownership repo
- which parts of `docs/SecurityPractice.md` need rewriting now that the target
runtime path is explicit
**Depends on:** `RMASTER-WP-0019` defining the cross-repo architecture
boundary, plus the future `rail-knative` bootstrap work that will follow from
it.
Done when: the required runtime contract is written clearly enough that future
`rail-knative` and `rapp-qonto` implementation work can start without
reopening the security model, and no new direct-deployment assumption is left
as the default target.
**2026-07-26:** Added `docs/knative-runtime-and-rapp-handoff.md`. It defines
pre-wake controls, cold-start/idempotency/failure behavior, raw-port exposure,
identity and credential boundaries, and the evidence required before
production approval.
## Task: Isolation placement request to Railiance
```task
id: QONTO-WP-0004-T06
status: done
priority: medium
state_hub_task_id: "7cd52269-ca0a-4b09-ab51-f02ed71604f2"
```
**2026-07-24:** Made the request concrete and reviewable (this task's own
stated scope — not to implement scheduling itself). Delivered:
- `deploy/k8s/qonto-assistant/` — dedicated namespace (not shared with
`activity-core`), `Deployment` at `replicas: 0` (meant to be scaled 0↔1 by
the facade, T05), `ClusterIP`-only `Service`, default-deny `NetworkPolicy`.
Modeled on `llm-connect`'s real deployment, tightened per
`docs/SecurityPractice.md` §7. Verified: `kubectl kustomize` renders all
six resources cleanly.
- **CCR-2026-0009** (in `railiance-platform`, `status: proposed`) — a
workload-scoped Kubernetes-auth access lane into the existing
`tenants/binky/qonto-api` credential, since CCR-2026-0008 is human/OIDC
admin access only and unusable by a running pod. Mirrors CCR-2026-0003's
llm-connect pattern. Paired draft `ClusterSecretStore` also added. Both
validated against `schemas/credential-change-request.schema.yaml`.
- `railiance/app.toml` — staged-promotion contract, `criticality = "critical"`
(mandatory human approval before Stage 2 exposure and Stage 3 promotion,
per the contract's own rule for production-critical workloads). Validated
against `railiance-cluster/schemas/railiance-app.schema.json`.
**Still pending, explicitly not this task's to resolve:** CCR-2026-0009
needs platform-operator + binky-tenant-owner approval; the actual isolation
tier (I1 vs I2) is Railiance's scheduling call once the manifests are
reviewed; whether the facade (T05) co-locates in this namespace is assumed
in `networkpolicy.yaml` but not yet decided.
**2026-07-26 direction update:** this direct Kubernetes placement package now
serves as migration input and isolation evidence rather than the intended final
runtime topology. Future implementation should treat these manifests, the CCR,
and the staged-promotion contract as baseline material to be re-homed into the
`reef-railiance` + `rail-knative` + `rapp-qonto` path, not as the long-term
deployment target of this repo.
## Task: Plan the `rapp-qonto` extraction and runtime handoff
```task
id: QONTO-WP-0004-T08
status: done
priority: high
state_hub_task_id: "e265cce1-df09-48ca-a0fd-f38897c44b4c"
```
Define how the current `qonto-assistant` deployment-oriented material hands off
into a future `rapp-qonto` package repo while preserving this repo as the
ownership home for the Qonto domain logic.
At minimum, this task must name:
- what stays in `qonto-assistant`
- what becomes package/runtime content in `rapp-qonto`
- how `KEY-WP-0004`'s workload-identity lane and the OpenBao secret lane bind
to the future runtime principal
- which current files are migration input only and must stop growing in place
Done when: the extraction and handoff boundary is written; the dependency on
`RMASTER-WP-0019` is explicit; and no new direct
`deploy/k8s/qonto-assistant` implementation starts without an explicit
exception note.
**2026-07-26:** The handoff document now assigns domain behavior and application
policy to this repo, package/runtime assets to `rapp-qonto`, and generic
activation/revision behavior to `rail-knative`. Existing direct Kubernetes
assets are explicitly migration input.
## Task: Closure review
```task
id: QONTO-WP-0004-T07
status: done
priority: low
state_hub_task_id: "2a2da206-d182-4a72-aea4-56e58ae74c76"
```
Close when T03T06 and T08 land or are explicitly deferred with a recorded
reason. T01/T02 already shipped without waiting on external dependencies, and
T06 now counts as migration input rather than the final runtime shape. Run
`statehub fix-consistency`.
**2026-07-27:** Closure review passed. T03T06 and T08 are complete, the
ownership/runtime split is implemented by `rapp-qonto` and `rail-knative`,
the OpenBao-backed secret lane is live, and restricted Qonto egress is
enforced by a controlled proxy plus a fail-closed startup gate. Remaining
deployment, rollback, and reef risk evidence belongs to
`REEF-RAILIANCE-WP-0003`, not this source-repo hardening plan.