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:
parent
60d4b9f8da
commit
9fbcef7b05
9 changed files with 429 additions and 0 deletions
50
deploy/k8s/qonto-assistant/README.md
Normal file
50
deploy/k8s/qonto-assistant/README.md
Normal 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`.
|
||||
29
deploy/k8s/qonto-assistant/configmap.yaml
Normal file
29
deploy/k8s/qonto-assistant/configmap.yaml
Normal 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
|
||||
69
deploy/k8s/qonto-assistant/deployment.yaml
Normal file
69
deploy/k8s/qonto-assistant/deployment.yaml
Normal 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
|
||||
29
deploy/k8s/qonto-assistant/externalsecret.yaml
Normal file
29
deploy/k8s/qonto-assistant/externalsecret.yaml
Normal 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
|
||||
9
deploy/k8s/qonto-assistant/kustomization.yaml
Normal file
9
deploy/k8s/qonto-assistant/kustomization.yaml
Normal 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
|
||||
13
deploy/k8s/qonto-assistant/namespace.yaml
Normal file
13
deploy/k8s/qonto-assistant/namespace.yaml
Normal 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
|
||||
53
deploy/k8s/qonto-assistant/networkpolicy.yaml
Normal file
53
deploy/k8s/qonto-assistant/networkpolicy.yaml
Normal 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
|
||||
20
deploy/k8s/qonto-assistant/service.yaml
Normal file
20
deploy/k8s/qonto-assistant/service.yaml
Normal 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
157
railiance/app.toml
Normal 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."
|
||||
Loading…
Add table
Add a link
Reference in a new issue