qonto-assistant/workplans/QONTO-WP-0004-security-hardening-and-scale-to-zero.md
tegwick 00191ee841 QONTO-WP-0004: mark T06 done
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-24 00:28:09 +02:00

9.4 KiB
Raw Blame History

id type title domain repo status owner topic_slug created updated state_hub_workstream_id
QONTO-WP-0004 workplan Security hardening and scale-to-zero facade for internet exposure infotech qonto-assistant active claude the-custodian 2026-07-23 2026-07-23 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

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

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

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

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: Facade / scale-to-zero activator — design and reference implementation

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

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.

Task: Closure review

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.