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:
Stéphane Tailland
2026-08-29 13:10:05 +02:00
parent c4747a6149
commit d68d20c6f4
18 changed files with 141 additions and 247 deletions
+71 -55
View File
@@ -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.
-30
View File
@@ -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
-14
View File
@@ -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
-14
View File
@@ -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
-36
View File
@@ -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-----
+7 -8
View File
@@ -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}
+5 -5
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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}
+1 -1
View File
@@ -2,7 +2,7 @@ apiVersion: v1
kind: Service
metadata:
name: n8n
namespace: n8n
namespace: ${NAMESPACE}
spec:
selector:
app: n8n
+1 -1
View File
@@ -1,4 +1,4 @@
apiVersion: v1
kind: Namespace
metadata:
name: n8n
name: ${NAMESPACE}
+1 -1
View File
@@ -2,7 +2,7 @@ apiVersion: v1
kind: Service
metadata:
name: n8n-postgres
namespace: n8n
namespace: ${NAMESPACE}
spec:
selector:
app: n8n-postgres
+3 -3
View File
@@ -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}
-24
View File
@@ -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
+16 -17
View File
@@ -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
+3 -3
View File
@@ -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}