refactor: make this a reusable deployment, not ours
Every environment value is a ${VAR} for the caller to substitute: hostname,
images, storage, ingress class, TLS issuer, Vault server and path, timezone,
retention. Nothing in k8s/ names a host, a realm, an agent or an estate.
Removed: the Flux plumbing under dev/, which described one deployment, and the
ExternalSecret pulling a specific agent's Keycloak client secret. Both belong to
whoever deploys this. The lab CA ConfigMap goes with them — the mount stays,
parameterised and marked optional, because 'trust a private CA' is a property of
an environment and not of n8n.
The README is now the template's: how to consume it, what every variable means,
and the three things it deliberately does not do.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,79 +1,95 @@
|
||||
# infra-n8n
|
||||
|
||||
n8n on the lab cluster: the runtime for the estate's autonomous agents,
|
||||
`Approvisionnement` and `Recouvrement`, whose workflows live in
|
||||
`classic-models-agent-flows`.
|
||||
A reusable n8n deployment: Postgres, the workload, TLS and an ingress, with
|
||||
every environment value left as a `${VAR}` for the caller to substitute.
|
||||
|
||||
Nothing business-specific is here. This repo deploys the engine; the workflows
|
||||
are imported into it.
|
||||
**Nothing here is specific to a deployment.** No hostname, no issuer, no Vault
|
||||
path, no image tag. A consumer supplies those through its own Flux
|
||||
`Kustomization` and its `postBuild.substitute` block — the same shape used for
|
||||
any templated estate. This repo is the template; it is not itself deployed.
|
||||
|
||||
## Layout
|
||||
## Using it
|
||||
|
||||
| | |
|
||||
Create, in the repo that owns the deployment:
|
||||
|
||||
```yaml
|
||||
apiVersion: source.toolkit.fluxcd.io/v1
|
||||
kind: GitRepository
|
||||
metadata: { name: infra-n8n, namespace: <ns> }
|
||||
spec:
|
||||
interval: 5m
|
||||
url: <this repo>
|
||||
ref: { branch: main }
|
||||
---
|
||||
apiVersion: kustomize.toolkit.fluxcd.io/v1
|
||||
kind: Kustomization
|
||||
metadata: { name: n8n, namespace: <ns> }
|
||||
spec:
|
||||
interval: 10m
|
||||
sourceRef: { kind: GitRepository, name: infra-n8n }
|
||||
path: ./k8s
|
||||
prune: true
|
||||
wait: true
|
||||
postBuild:
|
||||
substitute:
|
||||
NAMESPACE: <ns>
|
||||
# … every variable in the table below
|
||||
```
|
||||
|
||||
The namespace must exist first, and so must the AppRole Secret the store reads.
|
||||
|
||||
## Variables
|
||||
|
||||
| Variable | What it is |
|
||||
|---|---|
|
||||
| `k8s/postgres/` | the database, a StatefulSet with its own volume claim |
|
||||
| `k8s/n8n/` | the workload, its config and its data volume |
|
||||
| `k8s/ingress/` | a cert-manager `Certificate` and the nginx `Ingress` |
|
||||
| `k8s/secrets/` | the `ClusterSecretStore` for this estate and the `ExternalSecret` |
|
||||
| `dev/` | the Flux plumbing that applies `k8s/`, and manages itself |
|
||||
|
||||
Reachable at **https://n8n.sttlab.local** once `192.168.139.101` (ingress-nginx)
|
||||
resolves for that name.
|
||||
| `NAMESPACE` | where everything lands |
|
||||
| `N8N_HOSTNAME` | the public host. It goes into `WEBHOOK_URL` and the editor's links, so it must be what callers outside the cluster use — an in-cluster name here produces webhooks nothing can call |
|
||||
| `N8N_IMAGE`, `POSTGRES_IMAGE` | pinned images, tag included |
|
||||
| `N8N_STORAGE`, `POSTGRES_STORAGE` | claim sizes, e.g. `2Gi`, `5Gi` |
|
||||
| `TIMEZONE` | e.g. `Europe/Paris`; drives cron triggers |
|
||||
| `INGRESS_CLASS`, `INGRESS_BODY_SIZE` | e.g. `nginx`, `"50m"` |
|
||||
| `TLS_ISSUER_NAME`, `TLS_ISSUER_KIND` | a cert-manager issuer, e.g. `ClusterIssuer` |
|
||||
| `VAULT_SERVER`, `VAULT_MOUNT`, `VAULT_SECRET_PATH` | where the two secrets live |
|
||||
| `VAULT_STORE_NAME`, `VAULT_APPROLE_SECRET` | the `ClusterSecretStore` this creates, and the AppRole Secret in `external-secrets` it authenticates with |
|
||||
| `EXTRA_ENV_SECRET` | a Secret whose keys become environment variables for workflows. Optional in intent — supply an empty Secret if there is nothing |
|
||||
| `EXTRA_CA_CONFIGMAP` | a ConfigMap with `ca.crt`, mounted and set as `NODE_EXTRA_CA_CERTS`, so workflows can call internal TLS endpoints without verification being disabled. Marked `optional`, so an absent one is not an error |
|
||||
| `BLOCK_ENV_ACCESS` | `"true"` blocks `$env` in workflow expressions. n8n's default, and the right one wherever more than one team writes workflows: any workflow can otherwise read any variable in the pod. `"false"` is how a secret reaches a workflow without being committed in its JSON |
|
||||
| `EXECUTIONS_MAX_AGE_HOURS` | how long executions are kept. n8n keeps them for ever by default, which turns the database into a log |
|
||||
|
||||
## Two secrets, both of which must outlive any pod
|
||||
|
||||
They live in Vault at `secrets/n8n/instance`, read through the `vault-n8n`
|
||||
store — an AppRole whose policy can read `n8n/*` and nothing else, so a
|
||||
compromise here is not a way into the keycloak estate's secrets.
|
||||
Read from `VAULT_SECRET_PATH` as a whole, so the entry must hold exactly:
|
||||
|
||||
- `DB_PASSWORD`
|
||||
- `N8N_ENCRYPTION_KEY` — encrypts the credentials n8n stores in its database.
|
||||
**Never regenerate it.** A new key does not re-encrypt what is stored; it
|
||||
orphans it. The workflows survive, every credential in them does not.
|
||||
|
||||
## Bootstrap
|
||||
|
||||
Three manual applies, once. The Vault side is set up first — the commands are in
|
||||
The AppRole's policy should reach `VAULT_SECRET_PATH` and nothing else, so a
|
||||
workload here cannot read another deployment's secrets. The commands are in
|
||||
`k8s/secrets/clustersecretstore.yaml`.
|
||||
|
||||
```
|
||||
kubectl apply -f k8s/namespace.yaml
|
||||
kubectl apply -f dev/gitrepository.yaml
|
||||
kubectl apply -f dev/bootstrap-kustomization.yaml
|
||||
```
|
||||
|
||||
After that everything reconciles from git, `dev/` included. Never `kubectl
|
||||
apply` the objects under `k8s/` afterwards — Flux reverts them within its
|
||||
interval and the cluster silently stops matching what you edited.
|
||||
|
||||
## Choices worth knowing
|
||||
|
||||
**One replica, and `strategy: Recreate`.** n8n splits editor from workers only
|
||||
in queue mode, which needs Redis; without it a second replica is two schedulers
|
||||
racing over the same cron triggers. And the data volume is ReadWriteOnce, so a
|
||||
rolling update would deadlock — the new pod waiting for a volume the old one has
|
||||
not released.
|
||||
**One replica, and `strategy: Recreate`.** n8n separates editor from workers
|
||||
only in queue mode, which needs Redis; without it a second replica is two
|
||||
schedulers racing over the same cron triggers. And the data volume is
|
||||
ReadWriteOnce, so a rolling update deadlocks — the new pod waiting for a volume
|
||||
the old one has not released.
|
||||
|
||||
**The certificate is issued, not committed.** cert-manager writes `n8n-tls` from
|
||||
the `sttlab-local-ca` ClusterIssuer. The private key stays in the cluster.
|
||||
**Postgres is a StatefulSet with a `volumeClaimTemplate`.** The claim is not
|
||||
deleted with the workload, so losing the database to a `kubectl delete` takes
|
||||
more than one mistake.
|
||||
|
||||
**The CA certificate *is* committed**, in `k8s/ca-configmap.yaml`, and mounted
|
||||
as `NODE_EXTRA_CA_CERTS`. It is a certificate, never a key, and checking it in
|
||||
means a workflow can call `keycloak.sttlab.local` and the domain gateways
|
||||
without anyone disabling verification to make it work. Regenerate on rotation —
|
||||
it expires 2026-11-24.
|
||||
**The certificate is issued, never committed.** The private key is generated in
|
||||
the cluster and rotated by cert-manager.
|
||||
|
||||
**`N8N_HOST` and `WEBHOOK_URL` are the ingress host, not the Service.** They go
|
||||
into the webhook URLs n8n hands out; advertised as the in-cluster name, they
|
||||
would be URLs nothing outside can call.
|
||||
## What this repo deliberately does not do
|
||||
|
||||
**Executions are pruned after two weeks.** The default keeps them for ever,
|
||||
which turns the database into a log.
|
||||
**Authentication.** n8n's own user management is left at its defaults. A fresh
|
||||
instance is *unclaimed*: anyone who can reach it can create the owner account.
|
||||
Claim it immediately, and put it behind an identity-aware proxy or n8n's SSO
|
||||
before it holds anything that matters.
|
||||
|
||||
## What is deliberately not here
|
||||
|
||||
Authentication. n8n's own user management is left at its defaults, and the
|
||||
instance is reachable to anyone who can resolve the host on the lab network.
|
||||
Before it holds anything that matters, it needs either n8n's SSO against the
|
||||
`classic-models` realm or an `openid-connect` filter at the ingress — the same
|
||||
Keycloak the rest of the estate already uses.
|
||||
**Backups.** The Postgres claim survives a delete of the workload; it does not
|
||||
survive the cluster.
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
# Watches ./dev, so the GitRepository and the Kustomization below it reconcile
|
||||
# from git like any other manifest. It lives in ./dev itself, so it also manages
|
||||
# itself — the arrangement `flux bootstrap` sets up, which flux-operator does not
|
||||
# create, and the same one the rest of the estate uses.
|
||||
#
|
||||
# Three manual applies, once, to close the loop — nothing can watch ./dev until
|
||||
# the namespace and the source that serves it exist:
|
||||
#
|
||||
# kubectl apply -f k8s/namespace.yaml
|
||||
# kubectl apply -f dev/gitrepository.yaml
|
||||
# kubectl apply -f dev/bootstrap-kustomization.yaml
|
||||
#
|
||||
# After that every file in this repo, this one included, reconciles from git.
|
||||
#
|
||||
# Sharp edge: with prune enabled, deleting flux-kustomization.yaml from git
|
||||
# deletes the Kustomization it defines, and that Kustomization's own prune then
|
||||
# removes the deployment — including, on the PVCs, the workflows. The
|
||||
# volumeClaimTemplate on Postgres survives that; n8n-data does not.
|
||||
apiVersion: kustomize.toolkit.fluxcd.io/v1
|
||||
kind: Kustomization
|
||||
metadata:
|
||||
name: infra-n8n-bootstrap
|
||||
namespace: n8n
|
||||
spec:
|
||||
interval: 10m
|
||||
sourceRef:
|
||||
kind: GitRepository
|
||||
name: infra-n8n
|
||||
path: ./dev
|
||||
prune: true
|
||||
@@ -1,14 +0,0 @@
|
||||
apiVersion: kustomize.toolkit.fluxcd.io/v1
|
||||
kind: Kustomization
|
||||
metadata:
|
||||
name: infra-n8n
|
||||
namespace: n8n
|
||||
spec:
|
||||
interval: 10m
|
||||
sourceRef:
|
||||
kind: GitRepository
|
||||
name: infra-n8n
|
||||
path: ./k8s
|
||||
prune: true
|
||||
wait: true
|
||||
timeout: 5m
|
||||
@@ -1,14 +0,0 @@
|
||||
# HTTPS with no credentials: the repo is anonymously readable over HTTPS on
|
||||
# gitea, so a deploy key would guard nothing while adding a Vault entry and a
|
||||
# gitea key to create by hand before anything deploys. TLS still gives integrity
|
||||
# and authenticity of the source.
|
||||
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
|
||||
@@ -1,36 +0,0 @@
|
||||
# The lab CA, so n8n can verify keycloak.sttlab.local and the domain gateways
|
||||
# rather than disabling verification. A CA certificate is public by
|
||||
# construction — this is the certificate, never the key, and it is checked in so
|
||||
# the deployment has no hidden prerequisite.
|
||||
#
|
||||
# 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-----
|
||||
@@ -1,17 +1,16 @@
|
||||
# cert-manager issues this into the `n8n-tls` Secret, from the same
|
||||
# ClusterIssuer as the rest of the estate.
|
||||
# cert-manager issues this into the `n8n-tls` Secret.
|
||||
#
|
||||
# Deliberately not a copy of a certificate committed to git: the private key
|
||||
# stays in the cluster, is rotated by cert-manager, and never reaches a repo.
|
||||
# Deliberately not a certificate copied into the repo: the private key is
|
||||
# generated in the cluster, rotated by cert-manager, and never reaches git.
|
||||
apiVersion: cert-manager.io/v1
|
||||
kind: Certificate
|
||||
metadata:
|
||||
name: n8n-tls
|
||||
namespace: n8n
|
||||
namespace: ${NAMESPACE}
|
||||
spec:
|
||||
secretName: n8n-tls
|
||||
dnsNames:
|
||||
- n8n.sttlab.local
|
||||
- ${N8N_HOSTNAME}
|
||||
issuerRef:
|
||||
name: sttlab-local-ca
|
||||
kind: ClusterIssuer
|
||||
name: ${TLS_ISSUER_NAME}
|
||||
kind: ${TLS_ISSUER_KIND}
|
||||
|
||||
@@ -2,24 +2,24 @@ apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: n8n
|
||||
namespace: n8n
|
||||
namespace: ${NAMESPACE}
|
||||
annotations:
|
||||
# The editor pushes execution progress over a long-lived connection, and
|
||||
# imports can be large. Buffering it defeats the first; the default body
|
||||
# size defeats the second.
|
||||
nginx.ingress.kubernetes.io/proxy-body-size: "50m"
|
||||
nginx.ingress.kubernetes.io/proxy-body-size: ${INGRESS_BODY_SIZE}
|
||||
nginx.ingress.kubernetes.io/proxy-read-timeout: "3600"
|
||||
nginx.ingress.kubernetes.io/proxy-send-timeout: "3600"
|
||||
nginx.ingress.kubernetes.io/proxy-buffering: "off"
|
||||
nginx.ingress.kubernetes.io/proxy-http-version: "1.1"
|
||||
spec:
|
||||
ingressClassName: nginx
|
||||
ingressClassName: ${INGRESS_CLASS}
|
||||
tls:
|
||||
- hosts:
|
||||
- n8n.sttlab.local
|
||||
- ${N8N_HOSTNAME}
|
||||
secretName: n8n-tls
|
||||
rules:
|
||||
- host: n8n.sttlab.local
|
||||
- host: ${N8N_HOSTNAME}
|
||||
http:
|
||||
paths:
|
||||
- path: /
|
||||
|
||||
@@ -2,10 +2,8 @@ apiVersion: kustomize.config.k8s.io/v1beta1
|
||||
kind: Kustomization
|
||||
resources:
|
||||
- namespace.yaml
|
||||
- ca-configmap.yaml
|
||||
- secrets/clustersecretstore.yaml
|
||||
- secrets/externalsecret.yaml
|
||||
- secrets/agent-externalsecret.yaml
|
||||
- postgres/statefulset.yaml
|
||||
- postgres/service.yaml
|
||||
- n8n/pvc.yaml
|
||||
|
||||
+12
-16
@@ -4,21 +4,21 @@ apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: n8n-config
|
||||
namespace: n8n
|
||||
namespace: ${NAMESPACE}
|
||||
data:
|
||||
GENERIC_TIMEZONE: Europe/Paris
|
||||
TZ: Europe/Paris
|
||||
GENERIC_TIMEZONE: ${TIMEZONE}
|
||||
TZ: ${TIMEZONE}
|
||||
NODE_ENV: production
|
||||
|
||||
# What n8n believes it is reachable at. It goes into the webhook URLs it
|
||||
# generates and into the links it puts in its own UI, so it must be the
|
||||
# ingress host and not the Service — a webhook advertised as `n8n` inside the
|
||||
# cluster is a webhook nothing outside can call.
|
||||
N8N_HOST: n8n.sttlab.local
|
||||
N8N_HOST: ${N8N_HOSTNAME}
|
||||
N8N_PORT: "5678"
|
||||
N8N_PROTOCOL: https
|
||||
WEBHOOK_URL: https://n8n.sttlab.local/
|
||||
N8N_EDITOR_BASE_URL: https://n8n.sttlab.local/
|
||||
WEBHOOK_URL: https://${N8N_HOSTNAME}/
|
||||
N8N_EDITOR_BASE_URL: https://${N8N_HOSTNAME}/
|
||||
|
||||
# TLS terminates at the ingress, so n8n itself serves plain HTTP and must be
|
||||
# told it is behind a proxy — otherwise it reads the hop's address instead of
|
||||
@@ -28,18 +28,14 @@ data:
|
||||
# The default is to keep every execution for ever, which turns the database
|
||||
# into a log. Two weeks is long enough to debug a failed run.
|
||||
EXECUTIONS_DATA_PRUNE: "true"
|
||||
EXECUTIONS_DATA_MAX_AGE: "336"
|
||||
EXECUTIONS_DATA_MAX_AGE: ${EXECUTIONS_MAX_AGE_HOURS}
|
||||
|
||||
N8N_DIAGNOSTICS_ENABLED: "false"
|
||||
N8N_VERSION_NOTIFICATIONS_ENABLED: "false"
|
||||
N8N_RUNNERS_ENABLED: "true"
|
||||
|
||||
# Lets a workflow read `$env.<NAME>`. n8n blocks this by default, which is the
|
||||
# right default for an instance where anyone can write a workflow — here it is
|
||||
# how an agent's client secret reaches the workflow that acts as that agent,
|
||||
# without the secret being stored in the workflow JSON and committed.
|
||||
#
|
||||
# It does mean any workflow can read any variable in the pod. That is
|
||||
# acceptable while the instance runs this estate's own agents and nothing
|
||||
# else; it stops being acceptable the moment it is shared.
|
||||
N8N_BLOCK_ENV_ACCESS_IN_NODE: "false"
|
||||
# "false" lets a workflow read `$env.<NAME>`, which is how a secret reaches a
|
||||
# workflow without being stored in the workflow JSON. n8n blocks it by
|
||||
# default, and that default is right wherever more than one team can write a
|
||||
# workflow: any workflow can then read any variable in the pod.
|
||||
N8N_BLOCK_ENV_ACCESS_IN_NODE: ${BLOCK_ENV_ACCESS}
|
||||
|
||||
+19
-15
@@ -2,7 +2,7 @@ apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: n8n
|
||||
namespace: n8n
|
||||
namespace: ${NAMESPACE}
|
||||
spec:
|
||||
# One replica. n8n's queue mode splits the editor from workers and needs Redis
|
||||
# to do it; without that, a second replica is two schedulers racing over the
|
||||
@@ -26,7 +26,7 @@ spec:
|
||||
fsGroup: 1000
|
||||
initContainers:
|
||||
- name: wait-for-postgres
|
||||
image: postgres:16.10-alpine
|
||||
image: ${POSTGRES_IMAGE}
|
||||
command:
|
||||
- sh
|
||||
- -c
|
||||
@@ -36,7 +36,7 @@ spec:
|
||||
done
|
||||
containers:
|
||||
- name: n8n
|
||||
image: docker.n8n.io/n8nio/n8n:2.37.4
|
||||
image: ${N8N_IMAGE}
|
||||
ports:
|
||||
- name: http
|
||||
containerPort: 5678
|
||||
@@ -64,24 +64,27 @@ spec:
|
||||
secretKeyRef:
|
||||
name: n8n-secrets
|
||||
key: N8N_ENCRYPTION_KEY
|
||||
# The lab CA, so a workflow can call keycloak.sttlab.local and the
|
||||
# domain gateways without disabling verification. n8n is Node, hence
|
||||
# this variable rather than a trust store.
|
||||
# A private CA to trust, so a workflow can call an internal
|
||||
# endpoint over TLS without anyone disabling verification to make it
|
||||
# work. n8n is Node, hence this variable rather than a trust store.
|
||||
# Provided by the deployment; an empty ConfigMap disables it in
|
||||
# practice, since the file will not exist and Node falls back to its
|
||||
# own bundle.
|
||||
- name: NODE_EXTRA_CA_CERTS
|
||||
value: /etc/sttlab-ca/ca.crt
|
||||
value: /etc/extra-ca/ca.crt
|
||||
envFrom:
|
||||
- configMapRef:
|
||||
name: n8n-config
|
||||
# The agents' Keycloak client secrets, reached from a workflow as
|
||||
# `$env.APPROVISIONNEMENT_CLIENT_SECRET`. A workflow acts as its
|
||||
# agent, so it is the workflow that must be able to exchange tokens.
|
||||
# Whatever else this deployment's workflows need in their
|
||||
# environment. Provided by the deployment, not by this repo: an
|
||||
# empty Secret of that name is a valid answer.
|
||||
- secretRef:
|
||||
name: n8n-agent-secrets
|
||||
name: ${EXTRA_ENV_SECRET}
|
||||
volumeMounts:
|
||||
- name: data
|
||||
mountPath: /home/node/.n8n
|
||||
- name: sttlab-ca
|
||||
mountPath: /etc/sttlab-ca
|
||||
- name: extra-ca
|
||||
mountPath: /etc/extra-ca
|
||||
readOnly: true
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
@@ -110,6 +113,7 @@ spec:
|
||||
- name: data
|
||||
persistentVolumeClaim:
|
||||
claimName: n8n-data
|
||||
- name: sttlab-ca
|
||||
- name: extra-ca
|
||||
configMap:
|
||||
name: sttlab-ca
|
||||
name: ${EXTRA_CA_CONFIGMAP}
|
||||
optional: true
|
||||
|
||||
+2
-2
@@ -5,9 +5,9 @@ apiVersion: v1
|
||||
kind: PersistentVolumeClaim
|
||||
metadata:
|
||||
name: n8n-data
|
||||
namespace: n8n
|
||||
namespace: ${NAMESPACE}
|
||||
spec:
|
||||
accessModes: ["ReadWriteOnce"]
|
||||
resources:
|
||||
requests:
|
||||
storage: 2Gi
|
||||
storage: ${N8N_STORAGE}
|
||||
|
||||
@@ -2,7 +2,7 @@ apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: n8n
|
||||
namespace: n8n
|
||||
namespace: ${NAMESPACE}
|
||||
spec:
|
||||
selector:
|
||||
app: n8n
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
apiVersion: v1
|
||||
kind: Namespace
|
||||
metadata:
|
||||
name: n8n
|
||||
name: ${NAMESPACE}
|
||||
|
||||
@@ -2,7 +2,7 @@ apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: n8n-postgres
|
||||
namespace: n8n
|
||||
namespace: ${NAMESPACE}
|
||||
spec:
|
||||
selector:
|
||||
app: n8n-postgres
|
||||
|
||||
@@ -6,7 +6,7 @@ apiVersion: apps/v1
|
||||
kind: StatefulSet
|
||||
metadata:
|
||||
name: n8n-postgres
|
||||
namespace: n8n
|
||||
namespace: ${NAMESPACE}
|
||||
spec:
|
||||
serviceName: n8n-postgres
|
||||
replicas: 1
|
||||
@@ -22,7 +22,7 @@ spec:
|
||||
fsGroup: 999
|
||||
containers:
|
||||
- name: postgres
|
||||
image: postgres:16.10-alpine
|
||||
image: ${POSTGRES_IMAGE}
|
||||
ports:
|
||||
- name: postgres
|
||||
containerPort: 5432
|
||||
@@ -68,4 +68,4 @@ spec:
|
||||
accessModes: ["ReadWriteOnce"]
|
||||
resources:
|
||||
requests:
|
||||
storage: 5Gi
|
||||
storage: ${POSTGRES_STORAGE}
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
# The agents' Keycloak client secrets, for the workflows that act as those
|
||||
# agents. Read from the realm's own Vault entry through the keycloak store, not
|
||||
# copied into `n8n/*`: there is one source for a client secret, and rotating it
|
||||
# in Vault rotates it everywhere rather than leaving a stale twin behind.
|
||||
#
|
||||
# It is a narrow grant — this ExternalSecret names the properties it wants, so
|
||||
# the n8n 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
|
||||
@@ -1,37 +1,36 @@
|
||||
# One AppRole per estate, each with a policy that can read only its own path.
|
||||
# The n8n store cannot read `keycloak/*` and the keycloak store cannot read
|
||||
# `n8n/*` — so a compromised workload in one namespace does not become a way
|
||||
# into the other's secrets.
|
||||
# Reads this deployment's secrets from Vault. Scope the AppRole's policy to the
|
||||
# path this store reads and nothing more, so a workload here cannot reach
|
||||
# another estate's secrets.
|
||||
#
|
||||
# Its Vault side is created by hand, once:
|
||||
# The Vault side is created once, outside this repo:
|
||||
#
|
||||
# vault policy write n8n-readonly - <<'HCL'
|
||||
# path "secrets/data/n8n/*" { capabilities = ["read"] }
|
||||
# path "secrets/metadata/n8n/*" { capabilities = ["list", "read"] }
|
||||
# vault policy write <policy> - <<'HCL'
|
||||
# path "<mount>/data/<path>/*" { capabilities = ["read"] }
|
||||
# path "<mount>/metadata/<path>/*" { capabilities = ["list", "read"] }
|
||||
# HCL
|
||||
# vault write auth/approle/role/n8n-eso token_policies=n8n-readonly \
|
||||
# vault write auth/approle/role/<role> token_policies=<policy> \
|
||||
# token_ttl=1h token_max_ttl=4h
|
||||
# kubectl -n external-secrets create secret generic vault-approle-n8n \
|
||||
# --from-literal=roleId=$(vault read -field=role_id auth/approle/role/n8n-eso/role-id) \
|
||||
# --from-literal=secretId=$(vault write -f -field=secret_id auth/approle/role/n8n-eso/secret-id)
|
||||
# kubectl -n external-secrets create secret generic ${VAULT_APPROLE_SECRET} \
|
||||
# --from-literal=roleId=$(vault read -field=role_id auth/approle/role/<role>/role-id) \
|
||||
# --from-literal=secretId=$(vault write -f -field=secret_id auth/approle/role/<role>/secret-id)
|
||||
apiVersion: external-secrets.io/v1
|
||||
kind: ClusterSecretStore
|
||||
metadata:
|
||||
name: vault-n8n
|
||||
name: ${VAULT_STORE_NAME}
|
||||
spec:
|
||||
provider:
|
||||
vault:
|
||||
server: https://vault.sttlab.eu
|
||||
path: secrets
|
||||
server: ${VAULT_SERVER}
|
||||
path: ${VAULT_MOUNT}
|
||||
version: v2
|
||||
auth:
|
||||
appRole:
|
||||
path: approle
|
||||
roleRef:
|
||||
name: vault-approle-n8n
|
||||
name: ${VAULT_APPROLE_SECRET}
|
||||
namespace: external-secrets
|
||||
key: roleId
|
||||
secretRef:
|
||||
name: vault-approle-n8n
|
||||
name: ${VAULT_APPROLE_SECRET}
|
||||
namespace: external-secrets
|
||||
key: secretId
|
||||
|
||||
@@ -8,15 +8,15 @@ apiVersion: external-secrets.io/v1
|
||||
kind: ExternalSecret
|
||||
metadata:
|
||||
name: n8n-secrets
|
||||
namespace: n8n
|
||||
namespace: ${NAMESPACE}
|
||||
spec:
|
||||
refreshInterval: 1h
|
||||
secretStoreRef:
|
||||
kind: ClusterSecretStore
|
||||
name: vault-n8n
|
||||
name: ${VAULT_STORE_NAME}
|
||||
target:
|
||||
name: n8n-secrets
|
||||
creationPolicy: Owner
|
||||
dataFrom:
|
||||
- extract:
|
||||
key: n8n/instance
|
||||
key: ${VAULT_SECRET_PATH}
|
||||
|
||||
Reference in New Issue
Block a user