Put every trigger endpoint behind an authentication subrequest
n8n's webhook, form and mcp endpoints carry no authentication of their own, and for an automation platform those endpoints are the platform: whatever a workflow reads or changes answers an unauthenticated request. The editor's login protects the editor and nothing else. N8N_WEBHOOK_AUTH_URL is required rather than optional — there is no way to deploy this without naming an authenticator. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -48,6 +48,7 @@ The namespace must exist first, and so must the AppRole Secret the store reads.
|
|||||||
| `N8N_STORAGE`, `POSTGRES_STORAGE` | claim sizes, e.g. `2Gi`, `5Gi` |
|
| `N8N_STORAGE`, `POSTGRES_STORAGE` | claim sizes, e.g. `2Gi`, `5Gi` |
|
||||||
| `TIMEZONE` | e.g. `Europe/Paris`; drives cron triggers |
|
| `TIMEZONE` | e.g. `Europe/Paris`; drives cron triggers |
|
||||||
| `INGRESS_CLASS`, `INGRESS_BODY_SIZE` | e.g. `nginx`, `"50m"` |
|
| `INGRESS_CLASS`, `INGRESS_BODY_SIZE` | e.g. `nginx`, `"50m"` |
|
||||||
|
| `N8N_WEBHOOK_AUTH_URL` | an endpoint that answers 2xx for an authorised caller and 401/403 otherwise. **Required** — n8n's trigger endpoints carry no authentication of their own, so without this anyone who can reach the host can run any workflow. An OIDC provider's `userinfo` works as-is |
|
||||||
| `TLS_ISSUER_NAME`, `TLS_ISSUER_KIND` | a cert-manager issuer, e.g. `ClusterIssuer` |
|
| `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_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 |
|
| `VAULT_STORE_NAME`, `VAULT_APPROLE_SECRET` | the `ClusterSecretStore` this creates, and the AppRole Secret in `external-secrets` it authenticates with |
|
||||||
@@ -93,3 +94,24 @@ before it holds anything that matters.
|
|||||||
|
|
||||||
**Backups.** The Postgres claim survives a delete of the workload; it does not
|
**Backups.** The Postgres claim survives a delete of the workload; it does not
|
||||||
survive the cluster.
|
survive the cluster.
|
||||||
|
|
||||||
|
## Webhooks are not open
|
||||||
|
|
||||||
|
n8n serves a workflow's trigger endpoints — `/webhook`, `/form`, `/mcp` and
|
||||||
|
their test and waiting variants — on the same host as the editor, and **they
|
||||||
|
carry no authentication of their own**. For an automation platform those
|
||||||
|
endpoints *are* the platform: whatever a workflow reads or changes is reachable
|
||||||
|
by an unauthenticated request unless something in front says otherwise. The
|
||||||
|
editor's login protects the editor, nothing else.
|
||||||
|
|
||||||
|
So they are served by a second Ingress that puts an authentication subrequest in
|
||||||
|
front of every one of them, via `${N8N_WEBHOOK_AUTH_URL}`. There is deliberately
|
||||||
|
no way to deploy this without naming one.
|
||||||
|
|
||||||
|
Callers **inside** the cluster reach the Service directly and never traverse the
|
||||||
|
Ingress, so a facade or a gateway in front of a workflow keeps working while the
|
||||||
|
outside door is shut.
|
||||||
|
|
||||||
|
If you rename an endpoint with `N8N_ENDPOINT_WEBHOOK` and friends, rename it in
|
||||||
|
`k8s/ingress/ingress-webhooks.yaml` too. A prefix left out of that list is the
|
||||||
|
hole all over again.
|
||||||
|
|||||||
@@ -0,0 +1,73 @@
|
|||||||
|
# Webhook endpoints, authenticated.
|
||||||
|
#
|
||||||
|
# n8n serves a workflow's trigger endpoints on the same host as the editor, and
|
||||||
|
# they carry **no authentication of their own** — a workflow behind one answers
|
||||||
|
# anybody who can reach the host. That is not a detail: for an automation
|
||||||
|
# platform, those endpoints are the platform. Anything a workflow can read or
|
||||||
|
# change is reachable by an unauthenticated request unless something in front
|
||||||
|
# says otherwise.
|
||||||
|
#
|
||||||
|
# So this Ingress puts every trigger path behind an authentication subrequest.
|
||||||
|
# `${N8N_WEBHOOK_AUTH_URL}` must answer 2xx for an authorised request and
|
||||||
|
# 401/403 otherwise; an OIDC provider's userinfo endpoint has exactly that
|
||||||
|
# behaviour and needs nothing built. nginx forwards the caller's own headers to
|
||||||
|
# it, so a bearer on the original request is what gets checked.
|
||||||
|
#
|
||||||
|
# The editor keeps its own path (`/`) on the main Ingress and its own session
|
||||||
|
# login. Callers inside the cluster reach the Service directly and never pass
|
||||||
|
# through here — which is how an A2A facade in front of a workflow keeps working
|
||||||
|
# while the outside door is closed.
|
||||||
|
#
|
||||||
|
# Every trigger prefix n8n can serve is listed. One left out is the whole hole
|
||||||
|
# again, and the list is configurable per deployment
|
||||||
|
# (N8N_ENDPOINT_WEBHOOK and friends) — if you rename one, rename it here too.
|
||||||
|
apiVersion: networking.k8s.io/v1
|
||||||
|
kind: Ingress
|
||||||
|
metadata:
|
||||||
|
name: n8n-webhooks
|
||||||
|
namespace: ${NAMESPACE}
|
||||||
|
annotations:
|
||||||
|
nginx.ingress.kubernetes.io/auth-url: ${N8N_WEBHOOK_AUTH_URL}
|
||||||
|
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: ${INGRESS_CLASS}
|
||||||
|
tls:
|
||||||
|
- hosts:
|
||||||
|
- ${N8N_HOSTNAME}
|
||||||
|
secretName: n8n-tls
|
||||||
|
rules:
|
||||||
|
- host: ${N8N_HOSTNAME}
|
||||||
|
http:
|
||||||
|
paths:
|
||||||
|
- path: /webhook
|
||||||
|
pathType: Prefix
|
||||||
|
backend: &n8n
|
||||||
|
service:
|
||||||
|
name: n8n
|
||||||
|
port:
|
||||||
|
number: 80
|
||||||
|
- path: /webhook-test
|
||||||
|
pathType: Prefix
|
||||||
|
backend: *n8n
|
||||||
|
- path: /webhook-waiting
|
||||||
|
pathType: Prefix
|
||||||
|
backend: *n8n
|
||||||
|
- path: /form
|
||||||
|
pathType: Prefix
|
||||||
|
backend: *n8n
|
||||||
|
- path: /form-test
|
||||||
|
pathType: Prefix
|
||||||
|
backend: *n8n
|
||||||
|
- path: /form-waiting
|
||||||
|
pathType: Prefix
|
||||||
|
backend: *n8n
|
||||||
|
- path: /mcp
|
||||||
|
pathType: Prefix
|
||||||
|
backend: *n8n
|
||||||
|
- path: /mcp-test
|
||||||
|
pathType: Prefix
|
||||||
|
backend: *n8n
|
||||||
@@ -12,3 +12,4 @@ resources:
|
|||||||
- n8n/service.yaml
|
- n8n/service.yaml
|
||||||
- ingress/certificate.yaml
|
- ingress/certificate.yaml
|
||||||
- ingress/ingress.yaml
|
- ingress/ingress.yaml
|
||||||
|
- ingress/ingress-webhooks.yaml
|
||||||
|
|||||||
Reference in New Issue
Block a user