287 lines
13 KiB
Markdown
287 lines
13 KiB
Markdown
---
|
||
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.
|