npm package: @prokodo/n8n-nodes-secure-webhook
Description: 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.
Author: prokodo
Latest version: 0.0.1

<p align="center">
  <a href="https://www.prokodo.com" target="_blank" rel="noopener noreferrer">
    <img src="https://cdn.prokodo.com/prokodo_logo_1a3bb7867c/prokodo_logo_1a3bb7867c.webp" alt="prokodo – Empowering Digital Innovations" height="58" />
  </a>
</p>
<h1 align="center">n8n Secure Webhook</h1>
<h2 align="center">Defense-in-Depth for Public APIs</h2>

**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](https://www.prokodo.com).**

> 🇺🇸 Need help implementing **secure n8n webhooks & workflows** (HMAC, JWT/JWKS, mTLS, Redis, rate limits)?  
> **prokodo — n8n Security & Automation** → [click here](https://www.prokodo.com/en/?utm_source=github&utm_medium=readme_top&utm_campaign=secure_n8n)
>
> 🇩🇪 Sie brauchen Unterstützung bei **sicheren n8n Webhooks & Workflows** (HMAC, JWT/JWKS, mTLS, Redis, Rate Limits)?  
> **prokodo — n8n Security & Automation** → [hier klicken](https://www.prokodo.com/de/?utm_source=github&utm_medium=readme_top&utm_campaign=secure_n8n)

[![npm](https://img.shields.io/npm/v/@prokodo/n8n-nodes-secure-webhook?style=flat&color=3178c6&label=npm)](https://www.npmjs.com/package/@prokodo/n8n-nodes-secure-webhook)
[![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)

---

## ✨ 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 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 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):**
```bash
# 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):

```bash
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)

```bash
# 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:

```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://<your-host>/webhook/secure`

## 🔐 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)

### 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-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

---

### 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 like `sha256=<hex>`
- **HMAC Algorithm** — `sha256` (default) or `sha512`

**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** — `none` or `https`
- **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 `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"
```

---

### 2) JWT (JWKS)

#### 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) API Key (Internal)

**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}'
```

---

### 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)

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;
}
```

### 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-After` when exceeded

---

### 🗂 Audit Export (Best-Effort)
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.

### 🔒 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`/`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

---

### 🧯 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`** → 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 <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!
```bash
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](https://www.prokodo.com).