From f9511fd15d0f62438038aae48e0f9ed13fa36949 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Tailland?= Date: Sat, 29 Aug 2026 13:10:10 +0200 Subject: [PATCH] feat: this estate's n8n deployment MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The values that infra-n8n deliberately does not carry: our hostname, issuer, Vault path and images, plus the two things that are ours alone — the lab CA the workflows need to verify internal TLS, and the agents' Keycloak client secrets, read from the realm's own Vault entry so there is one source per secret. Self-managing after three applies, like the rest of the estate. Co-Authored-By: Claude Opus 5 --- deploy/agent-secrets.yaml | 24 +++++++++++++++ deploy/bootstrap-kustomization.yaml | 20 +++++++++++++ deploy/ca-configmap.yaml | 36 +++++++++++++++++++++++ deploy/flux-kustomization.yaml | 45 +++++++++++++++++++++++++++++ deploy/gitrepository-self.yaml | 12 ++++++++ deploy/gitrepository.yaml | 13 +++++++++ deploy/namespace.yaml | 4 +++ 7 files changed, 154 insertions(+) create mode 100644 deploy/agent-secrets.yaml create mode 100644 deploy/bootstrap-kustomization.yaml create mode 100644 deploy/ca-configmap.yaml create mode 100644 deploy/flux-kustomization.yaml create mode 100644 deploy/gitrepository-self.yaml create mode 100644 deploy/gitrepository.yaml create mode 100644 deploy/namespace.yaml diff --git a/deploy/agent-secrets.yaml b/deploy/agent-secrets.yaml new file mode 100644 index 0000000..7d1b846 --- /dev/null +++ b/deploy/agent-secrets.yaml @@ -0,0 +1,24 @@ +# The agents' Keycloak client secrets, for the workflows that act as those +# agents. Read from the realm's own Vault entry rather than copied into n8n's +# path: there is one source for a client secret, and rotating it in Vault +# rotates it everywhere instead of leaving a stale twin behind. +# +# A narrow grant — this names the properties it wants, so the namespace receives +# those and nothing else of the realm. +apiVersion: external-secrets.io/v1 +kind: ExternalSecret +metadata: + name: n8n-agent-secrets + namespace: n8n +spec: + refreshInterval: 1h + secretStoreRef: + kind: ClusterSecretStore + name: vault-keycloak + target: + name: n8n-agent-secrets + data: + - secretKey: APPROVISIONNEMENT_CLIENT_SECRET + remoteRef: + key: keycloak/classic-models-realm + property: KC_SECRET_CLASSIC_MODELS_AGENT_APPROVISIONNEMENT diff --git a/deploy/bootstrap-kustomization.yaml b/deploy/bootstrap-kustomization.yaml new file mode 100644 index 0000000..e1c57e4 --- /dev/null +++ b/deploy/bootstrap-kustomization.yaml @@ -0,0 +1,20 @@ +# Watches ./deploy, so everything here — including itself — reconciles from git. +# +# Three manual applies, once. Nothing can watch ./deploy until the namespace and +# the source that serves it exist: +# +# kubectl apply -f deploy/namespace.yaml +# kubectl apply -f deploy/gitrepository-self.yaml +# kubectl apply -f deploy/bootstrap-kustomization.yaml +apiVersion: kustomize.toolkit.fluxcd.io/v1 +kind: Kustomization +metadata: + name: n8n-deploy-bootstrap + namespace: n8n +spec: + interval: 10m + sourceRef: + kind: GitRepository + name: agent-flows + path: ./deploy + prune: true diff --git a/deploy/ca-configmap.yaml b/deploy/ca-configmap.yaml new file mode 100644 index 0000000..84ffd22 --- /dev/null +++ b/deploy/ca-configmap.yaml @@ -0,0 +1,36 @@ +# The lab CA, mounted into n8n as NODE_EXTRA_CA_CERTS so a workflow can call +# keycloak.sttlab.local and the domain gateways with TLS verified rather than +# disabled. A CA certificate is public by construction — this is the +# certificate, never the key. +# +# Issued by the sttlab-local-ca ClusterIssuer, valid until 2026-11-24. +# Regenerate on rotation: +# kubectl -n cert-manager get secret sttlab-local-ca \ +# -o jsonpath='{.data.tls\.crt}' | base64 -d +apiVersion: v1 +kind: ConfigMap +metadata: + name: sttlab-ca + namespace: n8n +data: + ca.crt: | + -----BEGIN CERTIFICATE----- + MIIDPDCCAiSgAwIBAgIUd/utDf+f0yQR3v3qTjZqx+wvTZMwDQYJKoZIhvcNAQEL + BQAwGTEXMBUGA1UEAwwOKi5zdHRsYWIubG9jYWwwHhcNMjUxMTI0MTUzODQ1WhcN + MjYxMTI0MTUzODQ1WjAZMRcwFQYDVQQDDA4qLnN0dGxhYi5sb2NhbDCCASIwDQYJ + KoZIhvcNAQEBBQADggEPADCCAQoCggEBALj0sryq8qXtkShT/B+/EjVJNN/TLHIn + QJYc6xuUH/f7q3dYdLm4iH0XnFgrSQLUo4iqd/Mczef5iWCYwT5FFa7P17/TcLVf + gso0j41qNLBjGxDQGQ6Z1VCxi6oan8ow98rumjVj0E0Qgs5CX/rN+gcEmZ/TZFWU + qudVoztpeiVqJ0dxCIq+KMutzd1BU7BXx4VRO9Y+t1idUc7oY7BPmS2qPAN7TtXP + j1KhH2BgLsmF5GRhYHNS578eCltcmS2sbQ3c7GwOV96lpgF9TixEvFXeN4xk43E6 + 1COUD7CRiWAZ0XI8FMlpZGeNZeV4bbV/MqVig1ybWNOz9MDKYj3eoXUCAwEAAaN8 + MHowHQYDVR0OBBYEFDbzYWOPr4vxa+VONs7xHbcNx70YMB8GA1UdIwQYMBaAFDbz + YWOPr4vxa+VONs7xHbcNx70YMA8GA1UdEwEB/wQFMAMBAf8wJwYDVR0RBCAwHoIO + Ki5zdHRsYWIubG9jYWyCDHN0dGxhYi5sb2NhbDANBgkqhkiG9w0BAQsFAAOCAQEA + tU4p5/ONXm7x4YDzSeL8NdThVLguiFur0jcTxvvPUS4NFNsnQJk4OXxBgjy1YwpL + 3i56/4mCEd9OCQF/Hl0eFwGZBzNKlMoTA08AC/lBDvPrnVjuUu0qe/b/2KOkhESa + x8rQNp0o8pcj0f29nu0JfRqYGkzy0Lh2qgXhPu2iC+t2PfdleRAky1tt68wogdFL + TvHDsJbVo/ihsPpONlSyHfxG6bb/0Mkz64PH46TTt9GvJflEKMqDwivNFUA1P5S/ + U4Ic8Qapnxc7h2+WbRjmYrnuygC2wMBE6rL9OF3gwCr1fkgGGRr5lVRUq2X2HZV5 + 5jqViv6eurq8O61N3vWGCQ== + -----END CERTIFICATE----- diff --git a/deploy/flux-kustomization.yaml b/deploy/flux-kustomization.yaml new file mode 100644 index 0000000..38a47d1 --- /dev/null +++ b/deploy/flux-kustomization.yaml @@ -0,0 +1,45 @@ +# This estate's n8n: the template from infra-n8n, with our values. +# +# Everything specific lives here rather than in that repo, so the same template +# deploys somebody else's n8n without carrying ours. +apiVersion: kustomize.toolkit.fluxcd.io/v1 +kind: Kustomization +metadata: + name: n8n + namespace: n8n +spec: + interval: 10m + sourceRef: + kind: GitRepository + name: infra-n8n + path: ./k8s + prune: true + wait: true + timeout: 5m + postBuild: + substitute: + NAMESPACE: n8n + N8N_HOSTNAME: n8n.sttlab.local + N8N_IMAGE: docker.n8n.io/n8nio/n8n:2.37.4 + POSTGRES_IMAGE: postgres:16.10-alpine + N8N_STORAGE: 2Gi + POSTGRES_STORAGE: 5Gi + TIMEZONE: Europe/Paris + INGRESS_CLASS: nginx + INGRESS_BODY_SIZE: "50m" + TLS_ISSUER_NAME: sttlab-local-ca + TLS_ISSUER_KIND: ClusterIssuer + VAULT_SERVER: https://vault.sttlab.eu + VAULT_MOUNT: secrets + VAULT_SECRET_PATH: n8n/instance + VAULT_STORE_NAME: vault-n8n + VAULT_APPROLE_SECRET: vault-approle-n8n + # The agents' Keycloak client secrets, so a workflow can exchange tokens + # as the agent it represents. Created by agent-secrets.yaml beside this. + EXTRA_ENV_SECRET: n8n-agent-secrets + EXTRA_CA_CONFIGMAP: sttlab-ca + # "false" so a workflow can read $env._CLIENT_SECRET. It means any + # workflow on this instance can read any variable in the pod — acceptable + # while it runs only this estate's agents, and not a moment longer. + BLOCK_ENV_ACCESS: "false" + EXECUTIONS_MAX_AGE_HOURS: "336" diff --git a/deploy/gitrepository-self.yaml b/deploy/gitrepository-self.yaml new file mode 100644 index 0000000..1fedfda --- /dev/null +++ b/deploy/gitrepository-self.yaml @@ -0,0 +1,12 @@ +# This repo, so ./deploy reconciles from git rather than from whoever last ran +# kubectl. +apiVersion: source.toolkit.fluxcd.io/v1 +kind: GitRepository +metadata: + name: agent-flows + namespace: n8n +spec: + interval: 5m + url: https://gitea.sttlab.eu/stt/classic-models-agent-flows.git + ref: + branch: main diff --git a/deploy/gitrepository.yaml b/deploy/gitrepository.yaml new file mode 100644 index 0000000..53136a0 --- /dev/null +++ b/deploy/gitrepository.yaml @@ -0,0 +1,13 @@ +# The generic n8n deployment template. It carries no value specific to this +# estate — every hostname, issuer and Vault path comes from the substitutions +# in flux-kustomization.yaml beside this file. +apiVersion: source.toolkit.fluxcd.io/v1 +kind: GitRepository +metadata: + name: infra-n8n + namespace: n8n +spec: + interval: 5m + url: https://gitea.sttlab.eu/stt/infra-n8n.git + ref: + branch: main diff --git a/deploy/namespace.yaml b/deploy/namespace.yaml new file mode 100644 index 0000000..8389695 --- /dev/null +++ b/deploy/namespace.yaml @@ -0,0 +1,4 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: n8n