QONTO-WP-0004-T06: draft isolation placement manifests + railiance/app.toml

Concrete, reviewable placement request per docs/SecurityPractice.md #7
(I1 Reinforced minimum: dedicated node pool/namespace, given internet
reachability via the facade and sole custody of the bank credential).

deploy/k8s/qonto-assistant/: modeled on llm-connect's real deployment
(deploy/k8s/activity-core-llm-connect/) but tightened -- dedicated
namespace (not shared), Deployment starts at replicas:0 (meant to be
scaled 0<->1 by the facade, QONTO-WP-0004-T05), ClusterIP-only Service,
NetworkPolicy default-deny with ingress limited to the facade and
egress limited to DNS+443 (the FQDN-egress limitation is called out
explicitly, not silently widened). Verified: `kubectl kustomize`
renders all six resources cleanly.

externalsecret.yaml depends on CCR-2026-0009 (new), proposed in
railiance-platform: 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/ExternalSecretsOperator pattern. Paired
draft ClusterSecretStore also added there. Both validated against
schemas/credential-change-request.schema.yaml. Status: proposed, not
approved -- explicitly not something this repo can complete alone.

railiance/app.toml: staged-promotion contract (criticality=critical,
mandatory human approval before Stage 2 traffic exposure and Stage 3
promotion, per railiance-cluster/docs/app-toml-contract.md's own rule
for production-critical workloads). Validated against
railiance-cluster/schemas/railiance-app.schema.json.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
tegwick 2026-07-24 00:27:37 +02:00
parent 60d4b9f8da
commit 9fbcef7b05
9 changed files with 429 additions and 0 deletions

View file

@ -0,0 +1,50 @@
# qonto-assistant Kubernetes manifests (draft)
QONTO-WP-0004-T06. Modeled on `llm-connect/deploy/k8s/activity-core-llm-connect/`,
tightened per `docs/SecurityPractice.md` §7's isolation-profile request
(I1 "Reinforced" minimum, given internet reachability via the facade and
sole custody of the bank credential).
## Files
| File | Purpose |
| --- | --- |
| `namespace.yaml` | Dedicated `qonto-assistant` namespace — not shared with `activity-core` or any other workload |
| `configmap.yaml` | Non-secret runtime config. Security-integration URLs left unset until key-cape/flex-auth/tenant-engine have confirmed in-cluster addresses |
| `deployment.yaml` | `replicas: 0` by default — meant to be scaled 0↔1 by the facade (T05, not yet built), never run continuously |
| `service.yaml` | `ClusterIP` only — never bind the raw port to a public address |
| `networkpolicy.yaml` | Default-deny; ingress only from the (assumed co-located) facade pod; egress limited to DNS + 443 |
| `externalsecret.yaml` | Syncs `API_KEY`/`API_USER` from `tenants/binky/qonto-api`**depends on CCR-2026-0009 (proposed, not approved)** |
## What is real vs. draft
- **Real, already live:** the underlying secret at `tenants/binky/qonto-api`
(CCR-2026-0008, human/OIDC admin access only).
- **Draft, needs approval before applying:** `externalsecret.yaml` and the
paired `ClusterSecretStore` in
`railiance-platform/argocd/platform-addons/openbao-secretstore/openbao-qonto-assistant.clustersecretstore.yaml`
both depend on **CCR-2026-0009** (workload-scoped Kubernetes-auth access
lane), which is `status: proposed` — not yet approved by
platform-operator/binky-tenant-owner.
- **Open question, not decided here:** whether the facade
(QONTO-WP-0004-T05) lands in this same namespace (assumed by
`networkpolicy.yaml`'s ingress rule) or a separate one. Update the
`podSelector`/`namespaceSelector` in `networkpolicy.yaml` once that's
decided.
- **Known egress-policy limitation:** native Kubernetes `NetworkPolicy` has
no FQDN-aware egress rule, so the `0.0.0.0/0:443` egress rule is broader
than the intent (only `thirdparty.qonto.com` and `bao.coulomb.social`).
Called out in `networkpolicy.yaml`'s own comment rather than silently
widened further.
## Applying (once CCR-2026-0009 is approved)
```bash
kubectl kustomize deploy/k8s/qonto-assistant # render and review first
kubectl apply -k deploy/k8s/qonto-assistant
kubectl -n qonto-assistant scale deployment/qonto-assistant --replicas=1 # manual wake, until T05 exists
```
See `railiance/app.toml` for the staged-promotion contract (Stage 1 local →
Stage 2 canary → Stage 3 production) this deployment should go through, per
`railiance-cluster/docs/app-toml-contract.md`.

View file

@ -0,0 +1,29 @@
# Non-secret runtime configuration. Security-integration URLs
# (QONTO_KEY_CAPE_JWKS_URL / QONTO_FLEX_AUTH_URL / QONTO_TENANT_ENGINE_URL)
# are deliberately left unset here: none of key-cape, flex-auth, or
# tenant-engine has a confirmed in-cluster reachable address yet. Each
# integration fails safe when unset (see docs/SecurityPractice.md #10 and
# #4) -- unset is the correct value until those addresses are confirmed,
# not a placeholder to fill in blindly.
apiVersion: v1
kind: ConfigMap
metadata:
name: qonto-assistant-config
namespace: qonto-assistant
labels:
app.kubernetes.io/name: qonto-assistant
app.kubernetes.io/part-of: qonto-assistant
data:
QONTO_ASSISTANT_HOST: "0.0.0.0"
QONTO_ASSISTANT_PORT: "8080"
QONTO_ASSISTANT_DEFAULT_TENANT: "binky"
QONTO_BASE_URL: "https://thirdparty.qonto.com"
QONTO_AUTH_MODE: "legacy_api_key"
QONTO_CREDENTIAL_SOURCE: "env"
QONTO_RATE_LIMIT_REQUESTS: "20"
QONTO_RATE_LIMIT_WINDOW_SECONDS: "60"
QONTO_MAX_CONCURRENCY: "4"
QONTO_DENY_ESCALATION_ENABLED: "true"
# QONTO_KEY_CAPE_JWKS_URL: TODO once key-cape has a confirmed in-cluster address
# QONTO_FLEX_AUTH_URL: TODO once flex-auth has a confirmed in-cluster address
# QONTO_TENANT_ENGINE_URL: TODO once tenant-engine is deployed anywhere reachable

View file

@ -0,0 +1,69 @@
# Starts at replicas: 0 -- this Deployment is meant to be scaled 0<->1 by
# the facade/activator (QONTO-WP-0004-T05, not yet built), never run
# continuously. Applying this manifest alone leaves the service correctly
# idle; something else (the facade, or manual `kubectl scale` for interim
# testing) must patch replicas to 1 to actually run it.
apiVersion: apps/v1
kind: Deployment
metadata:
name: qonto-assistant
namespace: qonto-assistant
labels:
app.kubernetes.io/name: qonto-assistant
app.kubernetes.io/part-of: qonto-assistant
spec:
replicas: 0
selector:
matchLabels:
app.kubernetes.io/name: qonto-assistant
template:
metadata:
labels:
app.kubernetes.io/name: qonto-assistant
app.kubernetes.io/part-of: qonto-assistant
spec:
containers:
- name: qonto-assistant
image: docker.io/library/qonto-assistant:latest
imagePullPolicy: Never
envFrom:
- configMapRef:
name: qonto-assistant-config
- secretRef:
name: qonto-assistant-qonto-api
optional: false
ports:
- name: http
containerPort: 8080
readinessProbe:
httpGet:
path: /v1/health
port: http
periodSeconds: 10
timeoutSeconds: 3
failureThreshold: 3
livenessProbe:
httpGet:
path: /v1/health
port: http
periodSeconds: 30
timeoutSeconds: 3
failureThreshold: 3
resources:
requests:
cpu: 50m
memory: 128Mi
limits:
cpu: 500m
memory: 512Mi
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop:
- ALL
readOnlyRootFilesystem: true
runAsNonRoot: true
runAsUser: 10001
runAsGroup: 10001
securityContext:
fsGroup: 10001

View file

@ -0,0 +1,29 @@
# DRAFT -- depends on CCR-2026-0009 (status: proposed) and the paired draft
# ClusterSecretStore railiance-platform/argocd/platform-addons/openbao-secretstore/
# openbao-qonto-assistant.clustersecretstore.yaml. Do not apply until that
# CCR is approved and the store exists.
apiVersion: external-secrets.io/v1
kind: ExternalSecret
metadata:
name: qonto-assistant-qonto-api
namespace: qonto-assistant
labels:
app.kubernetes.io/name: qonto-assistant
app.kubernetes.io/part-of: railiance-gitops
spec:
refreshInterval: 15m
secretStoreRef:
kind: ClusterSecretStore
name: openbao-qonto-assistant
target:
name: qonto-assistant-qonto-api
creationPolicy: Owner
data:
- secretKey: API_KEY
remoteRef:
key: tenants/binky/qonto-api
property: API_KEY
- secretKey: API_USER
remoteRef:
key: tenants/binky/qonto-api
property: API_USER

View file

@ -0,0 +1,9 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- namespace.yaml
- configmap.yaml
- deployment.yaml
- service.yaml
- networkpolicy.yaml
- externalsecret.yaml

View file

@ -0,0 +1,13 @@
# Dedicated namespace, not shared with activity-core or any other workload.
# Per docs/SecurityPractice.md §7: qonto-assistant is the sole holder of a
# real bank credential and must be internet-reachable (via the facade,
# QONTO-WP-0004-T05) -- Kings Guard isolation profile I1 "Reinforced"
# minimum, I2 "Dedicated" worth considering. A dedicated namespace is the
# baseline either way.
apiVersion: v1
kind: Namespace
metadata:
name: qonto-assistant
labels:
app.kubernetes.io/part-of: qonto-assistant
railiance-platform/isolation-profile: reinforced

View file

@ -0,0 +1,53 @@
# Default-deny with a narrow allow list, tighter than llm-connect's sibling
# policy (deploy in llm-connect/deploy/k8s/activity-core-llm-connect/) --
# that one allows any pod in its shared namespace on ingress; this one only
# allows the facade (assumed co-located in this namespace until
# QONTO-WP-0004-T05 decides otherwise -- update podSelector below if the
# facade lands in a different namespace).
#
# Egress limitation, stated plainly: native Kubernetes NetworkPolicy has no
# FQDN-aware egress rule. The `0.0.0.0/0:443` rule below is broader than the
# intent ("only thirdparty.qonto.com and bao.coulomb.social") -- it is the
# same coarseness llm-connect's own NetworkPolicy accepts for its egress.
# If Cilium (or another CNI with FQDN-aware NetworkPolicy/CiliumNetworkPolicy)
# is available on railiance01, replace this rule with an FQDN allow-list
# instead of widening it further.
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: qonto-assistant-facade-only
namespace: qonto-assistant
labels:
app.kubernetes.io/name: qonto-assistant
app.kubernetes.io/part-of: qonto-assistant
spec:
podSelector:
matchLabels:
app.kubernetes.io/name: qonto-assistant
policyTypes:
- Ingress
- Egress
ingress:
- from:
- podSelector:
matchLabels:
app.kubernetes.io/name: qonto-assistant-facade
ports:
- protocol: TCP
port: 8080
egress:
- to:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: kube-system
ports:
- protocol: UDP
port: 53
- protocol: TCP
port: 53
- to:
- ipBlock:
cidr: 0.0.0.0/0
ports:
- protocol: TCP
port: 443

View file

@ -0,0 +1,20 @@
# ClusterIP only. Per docs/SecurityPractice.md §5: qonto-assistant's raw
# port must never be bound to a publicly-reachable address in any
# deployment -- the facade (QONTO-WP-0004-T05) is the sole internet-facing
# component and reaches this Service from inside the cluster.
apiVersion: v1
kind: Service
metadata:
name: qonto-assistant
namespace: qonto-assistant
labels:
app.kubernetes.io/name: qonto-assistant
app.kubernetes.io/part-of: qonto-assistant
spec:
type: ClusterIP
selector:
app.kubernetes.io/name: qonto-assistant
ports:
- name: http
port: 8080
targetPort: http

157
railiance/app.toml Normal file
View file

@ -0,0 +1,157 @@
schema_version = "railiance.app.v1"
[app]
id = "qonto-assistant"
name = "Qonto Governed Assistant"
repo = "qonto-assistant"
owner = "binky-tenant-owner"
criticality = "critical"
description = "Sole holder of the Binky Qonto bank API credential; governed read-only REST+MCP finance surface for agent harnesses, reachable via a facade (QONTO-WP-0004-T05, not yet built)."
[source]
revision = "git:main"
artifact = "image"
digest_policy = "required"
[rollback]
strategy = "manual-runbook"
command = "kubectl -n qonto-assistant scale deployment/qonto-assistant --replicas=0"
verification = "Facade reports the backend unreachable; no traffic proxied until re-woken against a known-good image digest."
[[platform.dependencies]]
name = "openbao"
kind = "other"
required = true
stage = "stage2"
evidence = "ExternalSecret qonto-assistant-qonto-api syncs API_KEY/API_USER without error (depends on CCR-2026-0009 approval)."
[[secrets.references]]
name = "qonto-api-credential"
route = "openbao-workload-kv-read"
target = "ExternalSecret/qonto-assistant-qonto-api"
stage = "stage2"
required = true
[[observability.health_endpoints]]
name = "local-health"
url = "http://127.0.0.1:8080/v1/health"
stage = "stage1"
expected_status = 200
[[observability.health_endpoints]]
name = "cluster-health"
url = "http://qonto-assistant.qonto-assistant.svc.cluster.local:8080/v1/health"
stage = "stage2"
expected_status = 200
[stages.stage1]
enabled = true
namespace = "local"
release = "qonto-assistant-local"
commands = ["make test", "kubectl kustomize deploy/k8s/qonto-assistant"]
checks = ["unit-tests", "kustomize-render", "local-health"]
evidence = ["pytest output", "kustomize render success", "local health 200"]
requires_approval = false
[stages.stage2]
enabled = true
namespace = "qonto-assistant"
release = "qonto-assistant-canary"
commands = ["kubectl apply -k deploy/k8s/qonto-assistant --dry-run=server", "kubectl -n qonto-assistant scale deployment/qonto-assistant --replicas=1"]
checks = ["server-dry-run", "canary-ready", "cluster-health", "operator-approval"]
evidence = ["dry-run diff", "pod readiness", "health 200", "State Hub progress id"]
requires_approval = true
canary_mode = "isolated"
observation_minutes = 60
[stages.stage3]
enabled = true
namespace = "qonto-assistant"
release = "qonto-assistant"
commands = ["kubectl apply -k deploy/k8s/qonto-assistant", "kubectl -n qonto-assistant scale deployment/qonto-assistant --replicas=0"]
checks = ["stage2-accepted", "rollback-target", "cluster-health", "operator-approval"]
evidence = ["promotion commit", "new stable digest", "post-promotion smoke, then scale back to 0 -- the facade (T05) owns wake/idle from here on"]
requires_approval = true
promotion_mode = "release-replace"
previous_stable = "kubectl:qonto-assistant:previous-digest"
[[checks]]
id = "unit-tests"
type = "command"
stage = "stage1"
description = "Run repository unit tests."
required = true
run = "make test"
timeout_seconds = 600
[[checks]]
id = "kustomize-render"
type = "command"
stage = "stage1"
description = "Render the kustomize manifest set locally, no cluster contact."
required = true
run = "kubectl kustomize deploy/k8s/qonto-assistant"
[[checks]]
id = "local-health"
type = "http"
stage = "stage1"
description = "Confirm local service health."
required = true
url = "http://127.0.0.1:8080/v1/health"
expected_status = 200
timeout_seconds = 10
[[checks]]
id = "server-dry-run"
type = "kubernetes"
stage = "stage2"
description = "Server-side dry run of the full manifest set before scaling the canary up."
required = true
namespace = "qonto-assistant"
resource = "kustomization/deploy/k8s/qonto-assistant"
condition = "DryRunAccepted"
[[checks]]
id = "canary-ready"
type = "kubernetes"
stage = "stage2"
description = "Canary deployment reaches Available after being scaled to 1."
required = true
namespace = "qonto-assistant"
resource = "deploy/qonto-assistant"
condition = "Available"
[[checks]]
id = "cluster-health"
type = "http"
stage = "stage2"
description = "Cluster health endpoint returns 200."
required = true
url = "http://qonto-assistant.qonto-assistant.svc.cluster.local:8080/v1/health"
expected_status = 200
timeout_seconds = 10
[[checks]]
id = "operator-approval"
type = "manual"
stage = "stage2"
description = "Human approval is recorded before this internet-facing, credential-holding workload takes traffic -- criticality=critical per app.toml."
required = true
evidence_required = "State Hub approval note id, candidate digest, rollback target, confirmation that CCR-2026-0009 is approved."
[[checks]]
id = "stage2-accepted"
type = "manual"
stage = "stage3"
description = "Stage 2 gates passed for the same candidate artifact."
required = true
evidence_required = "State Hub Stage 2 acceptance progress id."
[[checks]]
id = "rollback-target"
type = "manual"
stage = "stage3"
description = "Previous stable release is recorded before promotion."
required = true
evidence_required = "Previous image digest or kustomize revision."