--- 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 T03–T06 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. T03–T06 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.