Hardened webhook trigger for n8n with HMAC/JWT (JWKS), replay protection, IP policies, mTLS proxy checks, per-IP rate limiting, Redis HA state, and audit export.
npm install @prokodo/n8n-nodes-secure-webhookProduction-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


---
- ๐ Authentication profiles
- HMAC: raw-body or extended signature (method + path + query + timestamp + nonce + bodyhash)
- JWT (JWKS): verify Authorization: Bearer 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โ
| 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.
)
- For JWT: a reachable JWKS URL (jwtJwksApi) and optional iss/aud
- For API Key: header name + value (apiKeyHeaderApi)
- For mTLS headers: a reverse proxy (Nginx/Envoy/Cloudflare) that terminates TLS and forwards verification headersUsing 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
$3
Local n8n (not Docker):
`bash
choose your custom folder (default ~/.n8n)
export N8N_CUSTOM_EXTENSIONS=~/.n8ninstall your built package into that folder
npm install --prefix "$N8N_CUSTOM_EXTENSIONS" @prokodo/n8n-nodes-secure-webhookstart n8n
n8n start
`#### Docker (example Dockerfile):
`bash
FROM n8nio/n8n:latestENV 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)
`bash
in this repo
npm ci
npm run buildmake your package linkable
npm linklink into your n8n custom extensions folder
npm link @prokodo/n8n-nodes-secure-webhook --prefix ~/.n8nstart 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:
`tsx
"scripts": {
"prepare": "npm run build"
}
`โฆand commit src/ (not dist/).
๐ง Webhook Endpoint
Path: /secure, method: POST, response mode: onReceived.
Final URL (default n8n): https://๐ Credentials
- HMAC โ Create credentials hmacSharedSecretApi and set your shared secret.
- JWT (JWKS) โ Create jwtJwksApi with your JWKS URL (e.g., Auth0/Okta). Optionally set jwtIss/jwtAud in the node.
- API Key โ Create apiKeyHeaderApi with headerName (e.g., x-api-key) and value.Pick the Security Profile in the node to match the credential type you configured.
๐งฉ Node Options (Field Guide)
$3
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
---
$3
- Trusted Proxy Hops โ how many
X-Forwarded-For hops 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---
$3
- 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---
$3
- HMAC Signature Header (
x-signature) โ value like sha256=
- HMAC Algorithm โ sha256 (default) or sha512Signature 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
---
$3
- IP Allow (CIDR) โ only allow these ranges
- IP Deny (CIDR) โ block these ranges first (deny takes precedence)
---
$3
- 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)
---
$3
- 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---
$3
- Audit Export โ
none or https
- Audit Endpoint URL โ HTTPS receiver of compact JSON events
- Audit Bearer Token โ optional Authorization: Bearer ๐ Client Integration Scenarios
$3
Server:
- Create credentials
hmacSharedSecretApi with your shared secret.
- Profile: Empfohlen (HMAC).
- Keep default timestamp/nonce headers unless you need custom names.Client (Node.js, extended mode):
`js
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):
`bash
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"
`---
$3
#### Server:
- Create credentials jwtJwksApi with your JWKS URL.
- Profile: JWT (JWKS). Optionally set jwtIss/jwtAud.
#### Client:
`bash
curl -X POST "https://your-n8n.example/secure" \
-H "content-type: application/json" \
-H "authorization: Bearer $JWT" \
--data '{"ok":true}'
`---
$3
Server:
- Create credentials
apiKeyHeaderApi with:
- headerName: e.g., x-api-key
- value: your static secret
- Profile: Einfacher API Key.#### Client:
`bash
curl -X POST "https://your-n8n.example/secure" \
-H "content-type: application/json" \
-H "x-api-key: YOUR_SECRET" \
--data '{"ok":true}'
`---
$3
Server:
- Profile: Kombiniert.
- Configure HMAC and IP Allow/Deny CIDRs.
Client: Same as HMAC, but requests must originate from allowed networks.
---
$3
1. Set Previous Secret = current secret; deploy the node with a Dual-Key Window (e.g., 60 min).
2. Distribute the new secret to clients and switch their signing key.
3. After the window elapses and traffic is fully migrated, clear Previous Secret.
๐งญ Reverse Proxy (mTLS Header) Example
Nginx:
`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;
}
`$3
- Enable Require mTLS Header
- Keep defaults or adjust header names
- Optionally Subject Allow Regex like ^CN=partner\.---
$3
- In-memory (default): token bucket (burst friendly), per process
- Redis (optional): fixed window via atomic Lua (EVALSHA), cluster-safeFields
- Rate Limit / Minute (per IP): max hits per 60s
- Redis aktivieren / URL / Namespace: shared state across nodes
- Returns 429 with
Retry-After when exceeded---
$3
If enabled, the node POSTs a compact JSON event to your endpoint:
`json
{
"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.$3
- 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)
---
$3
- โ
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/aud match 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---
$3
- 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 โ Ensure x-timestamp within Max Skew
- 401 replay_detected โ Use a fresh x-nonce for every request
- 401 bad_signature / bad_signature_algo โ Check algorithm/header and base string mode
- 401 invalid_token / missing_bearer โ Send Authorization: Bearer ; 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---
$3
- 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!
`bash
npm ci
npm run build
``Open a PR with what changed and how to test it.
ยฉ 2025 prokodo.
Visit us at prokodo.com.