Description
n8n Secure Webhook
Defense-in-Depth for Public APIs
Production-grade webhook trigger for n8n with HMAC/JWT auth, replay protection, IP policies, mTLS header checks, per-IP rate limiting, Redis HA support, and external audit export โ developed by prokodo.
๐บ๐ธ Need help implementing secure n8n webhooks & workflows (HMAC, JWT/JWKS, mTLS, Redis, rate limits)?
prokodo โ n8n Security & Automation โ click here๐ฉ๐ช Sie brauchen Unterstรผtzung bei sicheren n8n Webhooks & Workflows (HMAC, JWT/JWKS, mTLS, Redis, Rate Limits)?
prokodo โ n8n Security & Automation โ hier klicken
โจ Features
- ๐ Authentication profiles
- HMAC: raw-body or extended signature (method + path + query + timestamp + nonce + bodyhash)
- JWT (JWKS): verify
Authorization: Bearer <jwt>against your IdP - API Key: lightweight static header (best for internal calls)
- Combo: HMAC + IP allow/deny (CIDR)
- ๐ก๏ธ Replay protection (timestamp + nonce, one-time use)
- ๐งฑ Rate limiting per IP (in-memory token bucket or Redis Lua fixed-window)
- ๐ IP policies (allow/deny by CIDR) for partner/VPN hardening
- ๐ mTLS proof via proxy headers (e.g.,
x-ssl-client-verify: SUCCESS) - ๐งพ Audit export (best-effort HTTPS POST of accept/reject metadata)
- ๐งฐ HA/Cluster-ready with optional Redis for rate+replay state
- โ๏ธ Strict input hygiene: Content-Type allowlist & body size cap
- ๐ Dual-key rotation window for HMAC secret rollovers
- ๐งฉ UX-friendly node options with comprehensive inline help
Internal node name: prokodoSecureWebhook
In the node picker: โSecure Webhookโ
๐ Why not the default n8n Webhook?
| Capability | n8n Default Webhook | Secure Webhook (this node) |
|---|---|---|
| Auth | Token in URL or basic header patterns | HMAC / JWT (JWKS) / API Key / Combo |
| Replay protection | โ | Timestamp + Nonce |
| Rate limiting | โ | Per-IP (memory or Redis) |
| IP policies | โ | Allow/Deny (CIDR) |
| mTLS awareness | โ | Proxy header checks |
| Audit trail | Minimal | External HTTPS audit events |
| HA state | N/A | Redis for nonce & rate data |
| Signature scope | N/A | Raw body or Extended (method+path+query+โฆ) |
Bottom line: If your webhook is public-facing or exposed to the internet, this hardened trigger offers defense-in-depth that the default webhook does not.
โ Requirements
- Node.js 18+ / 22 LTS
- n8n โฅ 1.88 (tested on 1.116.2+)
- (Optional) Redis for HA/cluster setups
- For HMAC: a shared secret in n8n credentials (
hmacSharedSecretApi) - For JWT: a reachable JWKS URL (
jwtJwksApi) and optionaliss/aud - For API Key: header name + value (
apiKeyHeaderApi) - For mTLS headers: a reverse proxy (Nginx/Envoy/Cloudflare) that terminates TLS and forwards verification headers
Using an older n8n (e.g. 1.88)? It may still work if you align n8n-core / n8n-workflow versions. For best results, upgrade n8n.
๐ฆ Install
Option A โ Custom extensions folder (recommended)
Local n8n (not Docker):
# choose your custom folder (default ~/.n8n)
export N8N_CUSTOM_EXTENSIONS=~/.n8n
# install your built package into that folder
npm install --prefix "$N8N_CUSTOM_EXTENSIONS" @prokodo/n8n-nodes-secure-webhook
# start n8n
n8n start
Docker (example Dockerfile):
FROM n8nio/n8n:latest
ENV N8N_CUSTOM_EXTENSIONS=/home/node/.n8n
ENV NODE_PATH=/home/node/.n8n/node_modules
USER node
RUN npm install --prefix /home/node/.n8n @prokodo/n8n-nodes-secure-webhook@latest
After starting n8n, search in the node picker for โSecure Webhookโ
Internal name: prokodoSecureWebhook
๐ Dev install (build + link locally)
# in this repo
npm ci
npm run build
# make your package linkable
npm link
# link into your n8n custom extensions folder
npm link @prokodo/n8n-nodes-secure-webhook --prefix ~/.n8n
# start n8n with your custom folder
export N8N_CUSTOM_EXTENSIONS=~/.n8n
n8n start
Publish-ready tip: This package publishes compiled JS from dist/ to npm.
You donโt need to commit dist/ to Git. To support installs straight from GitHub, add:
"scripts": {
"prepare": "npm run build"
}
โฆand commit src/ (not dist/).
๐ง Webhook Endpoint
Path: /secure, method: POST, response mode: onReceived.
Final URL (default n8n): https://<your-host>/webhook/secure
๐ Credentials
- HMAC โ Create credentials
hmacSharedSecretApiand set your shared secret. - JWT (JWKS) โ Create
jwtJwksApiwith your JWKS URL (e.g., Auth0/Okta). Optionally setjwtIss/jwtAudin the node. - API Key โ Create
apiKeyHeaderApiwithheaderName(e.g.,x-api-key) andvalue.
Pick the Security Profile in the node to match the credential type you configured.
๐งฉ Node Options (Field Guide)
Core
Security Profile
- HMAC โ safest default for public internet
- jwks (JWT) โ for IdPs like Auth0/Okta/OIDC
- apikey (Static header) โ simple, for internal clients
- combo โ HMAC + IP allow/deny
Shared safety
- Trusted Proxy Hops โ how many
X-Forwarded-Forhops you trust (e.g., Cloudflare + Nginx = 2) - Max Body (bytes) โ reject large payloads early (default 1 MB)
- Allowed Content-Types โ comma-separated allowlist (default:
application/json,application/x-www-form-urlencoded) - Rate Limit / Minute (per IP) โ default 120. With Redis itโs cluster-wide; otherwise per process
Replay Protection
- Timestamp Header (
x-timestamp) โ UNIX seconds, 10 digits - Nonce Header (
x-nonce) โ unique per request (UUID v4 or 16โ32B hex) - Max Skew (sec) โ default 300; tune based on client/server time sync
HMAC (recommended/combo)
- HMAC Signature Header (
x-signature) โ value likesha256=<hex> - HMAC Algorithm โ
sha256(default) orsha512
Signature Mode
- body โ sign raw body with HMAC (compatibility)
- extended โ method + path + canonical query + timestamp + nonce + bodyhash (strongest)
Dual-Key Rotation
- Previous Secret โ optional, for rolling changes
- Dual-Key Window (min) โ accept previous secret briefly during rotation
IP Policies (combo)
- IP Allow (CIDR) โ only allow these ranges
- IP Deny (CIDR) โ block these ranges first (deny takes precedence)
JWT (JWKS)
- JWT Issuer (iss) โ optional but recommended
- JWT Audience (aud) โ optional but recommended
- JWKS Cooldown (ms) โ min interval between JWKS fetches (default 60s)
- JWKS Fetch Timeout (ms) โ network timeout (default 3s)
mTLS via Proxy
- Require mTLS Header โ expect proof from your proxy (e.g.,
x-ssl-client-verify: SUCCESS) - Verify Header โ default
x-ssl-client-verify - Subject Header โ default
x-ssl-client-dn - Subject Allow Regex โ optional (
^CN=partner\.), fail-closed on bad regex
Audit Export
- Audit Export โ
noneorhttps - Audit Endpoint URL โ HTTPS receiver of compact JSON events
- Audit Bearer Token โ optional
Authorization: Bearer <token>
๐ Client Integration Scenarios
1) HMAC (Recommended)
Server:
- Create credentials
hmacSharedSecretApiwith your shared secret. - Profile: Empfohlen (HMAC).
- Keep default timestamp/nonce headers unless you need custom names.
Client (Node.js, extended mode):
import crypto from 'node:crypto';
const url = 'https://your-n8n.example/secure?foo=bar';
const body = JSON.stringify({ hello: 'world' });
const ts = Math.floor(Date.now()/1000).toString();
const nonce = crypto.randomUUID();
const algo = 'sha256';
// body hash
const bodyHash = crypto.createHash(algo).update(Buffer.from(body)).digest('hex');
// canonicalize query
const { URL } = await import('node:url');
const u = new URL(url);
const query = [...u.searchParams.entries()]
.sort(([a],[b]) => a.localeCompare(b))
.map(([k,v]) => `${encodeURIComponent(k)}=${encodeURIComponent(v)}`)
.join('&');
// base string
const base = ['POST', u.pathname, query, ts, nonce, bodyHash].join('\n');
// hmac
const signatureHex = crypto.createHmac(algo, process.env.WEBHOOK_SECRET).update(base).digest('hex');
const resp = await fetch(url, {
method: 'POST',
headers: {
'content-type': 'application/json',
'x-timestamp': ts,
'x-nonce': nonce,
'x-signature': `${algo}=${signatureHex}`,
},
body,
});
console.log(resp.status, await resp.text());
curl (body mode):
TS=$(date +%s)
NONCE=$(uuidgen)
BODY='{"hello":"world"}'
SIG_HEX=$(printf %s "$BODY" | openssl dgst -sha256 -hmac "$WEBHOOK_SECRET" -binary | xxd -p -c 256)
curl -X POST "https://your-n8n.example/secure" \
-H "content-type: application/json" \
-H "x-timestamp: $TS" \
-H "x-nonce: $NONCE" \
-H "x-signature: sha256=$SIG_HEX" \
--data "$BODY"
2) JWT (JWKS)
Server:
- Create credentials jwtJwksApi with your JWKS URL.
- Profile: JWT (JWKS). Optionally set jwtIss/jwtAud.
Client:
curl -X POST "https://your-n8n.example/secure" \
-H "content-type: application/json" \
-H "authorization: Bearer $JWT" \
--data '{"ok":true}'
3) API Key (Internal)
Server:
- Create credentials
apiKeyHeaderApiwith:headerName: e.g.,x-api-keyvalue: your static secret
- Profile: Einfacher API Key.
Client:
curl -X POST "https://your-n8n.example/secure" \
-H "content-type: application/json" \
-H "x-api-key: YOUR_SECRET" \
--data '{"ok":true}'
4) Combo (HMAC + IP Allow/Deny)
Server:
- Profile: Kombiniert.
- Configure HMAC and IP Allow/Deny CIDRs.
Client: Same as HMAC, but requests must originate from allowed networks.
๐ HMAC Secret Rotation (Playbook)
- Set Previous Secret = current secret; deploy the node with a Dual-Key Window (e.g., 60 min).
- Distribute the new secret to clients and switch their signing key.
- After the window elapses and traffic is fully migrated, clear Previous Secret.
๐งญ Reverse Proxy (mTLS Header) Example
Nginx:
ssl_verify_client on; # request client cert
ssl_client_certificate /etc/nginx/ca_chain.pem;
location /secure {
proxy_pass http://n8n:5678/secure;
proxy_set_header x-ssl-client-verify $ssl_client_verify; # "SUCCESS" on pass
proxy_set_header x-ssl-client-dn $ssl_client_s_dn;
}
Node settings
- Enable Require mTLS Header
- Keep defaults or adjust header names
- Optionally Subject Allow Regex like
^CN=partner\.
๐งฎ Rate Limiting Details
- In-memory (default): token bucket (burst friendly), per process
- Redis (optional): fixed window via atomic Lua (
EVALSHA), cluster-safe
Fields
- Rate Limit / Minute (per IP): max hits per 60s
- Redis aktivieren / URL / Namespace: shared state across nodes
- Returns 429 with
Retry-Afterwhen exceeded
๐ Audit Export (Best-Effort)
If enabled, the node POSTs a compact JSON event to your endpoint:
{
"ts": "2025-01-01T10:00:00.000Z",
"rid": "9f2aโฆ",
"ip": "203.0.113.42",
"profile": "recommended",
"outcome": "accept",
"reason": "bad_signature"
}
- No request payload is sent (metadata only).
- Failures are swallowed to avoid breaking the main request path.
๐ Security Model (At a Glance)
- Input hygiene: strict Content-Type allowlist, body size cap
- Auth: HMAC/JWT/API Key; extended HMAC defends against path/query tampering
- Replay: timestamp + nonce (short TTL); requires client time sync
- Abuse: per-IP rate limiting; optional IP allow/deny (CIDR)
- Transport: terminate TLS at edge; optional mTLS forwarding
- State: Redis for HA (nonces & rate); memory fallback for simple installs
- Audit: external events for SIEM/forensics (no payload leakage)
๐งช Testing Checklist
- โ Valid Content-Type and within Max Body
- โ Include x-timestamp (10-digit seconds) within Max Skew
- โ Include x-nonce (unique per request)
- โ HMAC: correct signature (body/extended mode)
- โ
JWT: valid token & reachable JWKS;
iss/audmatch if configured - โ
Rate limiting: send >
rateMax/min to observe 429 + Retry-After - โ IP policy behavior (allow/deny)
- โ (Optional) mTLS headers forwarding via proxy
- โ Observe x-request-id and audit events
๐งฏ Troubleshooting
- 415
unsupported_content_typeโ Add clientโs type to Allowed Content-Types - 413
payload_too_largeโ Increase Max Body or shrink payload - 401
bad_or_missing_timestampโ Ensurex-timestampwithin Max Skew - 401
replay_detectedโ Use a freshx-noncefor every request - 401
bad_signature/bad_signature_algoโ Check algorithm/header and base string mode - 401
invalid_token/missing_bearerโ SendAuthorization: Bearer <jwt>; verify IdP/JWKS - 401
mtls_*โ Confirm proxy sets verification headers - 403
ip_denied/ip_not_allowedโ Adjust IP Deny/Allow CIDRs - 429
rate_limitedโ Decrease traffic, raise threshold, or enable Redis
๐ง Best Practices
- Prefer HMAC extended mode for public endpoints.
- Keep Max Skew low (120โ300s) and ensure NTP on all systems.
- Rotate HMAC secrets regularly; use Dual-Key Window during rollouts.
- For HA/Cluster, enable Redis (protect with TLS/network policy).
- Terminate TLS at the edge; add mTLS for partner integrations.
- Use precise CIDRs in IP allow lists (avoid wildcards).
- Log/trace with x-request-id end-to-end.
๐ Contributing
PRs welcome!
npm ci
npm run build
Open a PR with what changed and how to test it.
๐ License
This library is published under MIT.
ยฉ 2025 prokodo.
Visit us at prokodo.com.
