Unsecured n8n webhooks can be triggered by anyone who knows the URL, exposing your workflows to abuse and data injection. Secure them by enabling authentication (Basic Auth, Header Auth, or JWT), validating HMAC signatures from providers like GitHub and Stripe, and adding IP allowlisting with a reverse proxy. These layers ensure only authorized services can trigger your workflows.
Why Webhook Security Matters in n8n
By default, n8n webhooks accept requests from any source without authentication. In production, this means anyone who discovers or guesses your webhook URL can trigger your workflow, potentially injecting malicious data, consuming resources, or manipulating downstream systems. This tutorial covers three layers of webhook security: built-in authentication options in the Webhook node, HMAC signature verification for provider webhooks, and IP allowlisting at the network level. Implementing at least two of these layers provides strong protection.
Prerequisites
- A running n8n instance with at least one webhook workflow
- Access to the n8n editor
- Basic understanding of HTTP headers and authentication
- A reverse proxy (Nginx or Caddy) for IP allowlisting (optional but recommended)
Step-by-step guide
Enable Basic Auth on a webhook
Enable Basic Auth on a webhook
Open your Webhook node and expand the Authentication section. Select 'Basic Auth' from the dropdown. Enter a username and password. When Basic Auth is enabled, every request to the webhook must include an Authorization header with the base64-encoded credentials. Requests without valid credentials receive a 401 Unauthorized response. This is the simplest authentication method and works well for internal integrations.
1# Send a request with Basic Auth using curl2curl -X POST https://n8n.yourdomain.com/webhook/my-endpoint \3 -u "myuser:mysecretpassword" \4 -H "Content-Type: application/json" \5 -d '{"event": "order_created", "order_id": 123}'Expected result: Requests with valid credentials trigger the workflow. Requests without credentials receive a 401 response.
Use Header Auth for API key-style security
Use Header Auth for API key-style security
In the Webhook node's Authentication section, select 'Header Auth'. Set the Header Name (for example, X-API-Key) and the Header Value (your secret key). n8n checks every incoming request for this header and value. This method is common for service-to-service integrations where the calling service can include custom headers.
1# Send a request with Header Auth2curl -X POST https://n8n.yourdomain.com/webhook/my-endpoint \3 -H "X-API-Key: your_secret_api_key_here" \4 -H "Content-Type: application/json" \5 -d '{"event": "payment_received", "amount": 49.99}'Expected result: Only requests with the correct header name and value trigger the workflow.
Verify HMAC signatures from webhook providers
Verify HMAC signatures from webhook providers
Services like GitHub, Stripe, and Shopify sign their webhook payloads with an HMAC-SHA256 signature. The signature is sent in a header (for example, X-Hub-Signature-256 for GitHub). Add a Code node immediately after the Webhook node to verify the signature before processing the payload. If the signature does not match, stop the workflow and return an error.
1const crypto = require('crypto');23// Get the raw body and signature from the webhook4const rawBody = JSON.stringify($input.first().json.body);5const signature = $input.first().json.headers['x-hub-signature-256'];6const secret = 'your_webhook_secret_here';78// Calculate expected signature9const expectedSignature = 'sha256=' + crypto10 .createHmac('sha256', secret)11 .update(rawBody)12 .digest('hex');1314// Compare signatures using timing-safe comparison15const isValid = crypto.timingSafeEqual(16 Buffer.from(signature || ''),17 Buffer.from(expectedSignature)18);1920if (!isValid) {21 throw new Error('Invalid webhook signature');22}2324return $input.all();Expected result: Only requests with a valid HMAC signature pass through to the rest of the workflow.
Add IP allowlisting with a reverse proxy
Add IP allowlisting with a reverse proxy
Configure your reverse proxy to only forward requests to the webhook path from known IP addresses. This is especially useful for provider webhooks where the sending service publishes their IP ranges (GitHub, Stripe, etc.). Add an allow list in your Nginx or Caddy configuration for the /webhook/ path.
1# Nginx configuration for IP allowlisting on webhook paths2location /webhook/ {3 # GitHub webhook IP ranges (check GitHub docs for current list)4 allow 140.82.112.0/20;5 allow 185.199.108.0/22;6 allow 192.30.252.0/22;7 deny all;89 proxy_pass http://localhost:5678;10 proxy_set_header Host $host;11 proxy_set_header X-Real-IP $remote_addr;12 proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;13 proxy_set_header X-Forwarded-Proto $scheme;14}Expected result: Only requests from allowed IP addresses reach the n8n webhook. All other IPs receive a 403 Forbidden response.
Use a random, unguessable webhook path
Use a random, unguessable webhook path
As a baseline security measure, use a long random string as your webhook path instead of a descriptive name. While this is not a replacement for authentication, it makes it much harder for attackers to discover your webhook URL through guessing or scanning. Generate a UUID or random string and use it as the path.
1# Generate a random webhook path2openssl rand -hex 163# Example output: a3f8b2c1d4e5f6a7b8c9d0e1f2a3b4c545# Use it as the webhook path: /webhook/a3f8b2c1d4e5f6a7b8c9d0e1f2a3b4c5Expected result: The webhook URL contains a random path that cannot be guessed by scanning.
Combine multiple security layers
Combine multiple security layers
For maximum security, combine several of these techniques. Use Header Auth on the Webhook node for authentication, add HMAC verification in a Code node for provider webhooks, set up IP allowlisting in your reverse proxy, and use a random webhook path. Each layer blocks a different type of attack, providing defense in depth.
Expected result: Your webhook is protected by multiple independent security layers, each blocking different attack vectors.
Complete working example
1// Code node: Verify HMAC-SHA256 webhook signature2// Supports GitHub (X-Hub-Signature-256), Stripe (Stripe-Signature),3// and generic HMAC signatures4//5// Place this node immediately after the Webhook node67const crypto = require('crypto');89// Configuration — update these for your provider10const WEBHOOK_SECRET = 'your_webhook_secret_here';11const SIGNATURE_HEADER = 'x-hub-signature-256'; // GitHub12// const SIGNATURE_HEADER = 'stripe-signature'; // Stripe13// const SIGNATURE_HEADER = 'x-signature'; // Custom1415// Get the request data16const headers = $input.first().json.headers;17const body = $input.first().json.body;18const receivedSignature = headers[SIGNATURE_HEADER];1920if (!receivedSignature) {21 throw new Error(`Missing signature header: ${SIGNATURE_HEADER}`);22}2324// Calculate expected HMAC signature25const rawBody = typeof body === 'string' ? body : JSON.stringify(body);26const hmac = crypto.createHmac('sha256', WEBHOOK_SECRET);27hmac.update(rawBody);28const calculatedSignature = 'sha256=' + hmac.digest('hex');2930// Timing-safe comparison to prevent timing attacks31try {32 const isValid = crypto.timingSafeEqual(33 Buffer.from(receivedSignature),34 Buffer.from(calculatedSignature)35 );3637 if (!isValid) {38 throw new Error('Signature verification failed');39 }40} catch (error) {41 if (error.message === 'Signature verification failed') throw error;42 // Buffer length mismatch also means invalid signature43 throw new Error('Signature verification failed: length mismatch');44}4546// Signature is valid — pass the data through47return [{48 json: {49 verified: true,50 payload: body51 }52}];Common mistakes when securing Webhooks in n8n
Why it's a problem: Leaving webhooks open without any authentication in production
How to avoid: Enable at least Basic Auth or Header Auth on every production webhook. It takes 30 seconds to configure.
Why it's a problem: Using string comparison (===) instead of timingSafeEqual for HMAC verification
How to avoid: Always use crypto.timingSafeEqual to prevent timing attacks that can leak the secret one character at a time.
Why it's a problem: Hardcoding the webhook secret in the Code node
How to avoid: Store the secret as an n8n environment variable and access it with {{ $env.WEBHOOK_SECRET }} or process.env.WEBHOOK_SECRET.
Why it's a problem: Not updating IP allowlists when the provider changes their IP ranges
How to avoid: Check provider documentation periodically. Set a calendar reminder to review IP ranges quarterly.
Best practices
- Never leave production webhooks without at least one authentication layer
- Use Header Auth or Basic Auth as the minimum security for all webhooks
- Verify HMAC signatures for provider webhooks (GitHub, Stripe, Shopify) to ensure payload integrity
- Use crypto.timingSafeEqual for signature comparison to prevent timing attacks
- Set up IP allowlisting at the reverse proxy level for known webhook senders
- Use random, unguessable webhook paths as a baseline defense against URL scanning
- Rotate webhook secrets periodically and update both the provider and n8n configuration
- Log rejected requests (invalid auth, bad signatures) for security monitoring
Still stuck?
Copy one of these prompts to get a personalized, step-by-step explanation.
How do I secure n8n webhooks in production? I need authentication, HMAC signature verification for GitHub webhooks, and IP allowlisting. Show me the configuration for each security layer.
Add security to my n8n webhook workflow. Configure Header Auth on the Webhook node with an X-API-Key header. Then add a Code node that verifies GitHub HMAC-SHA256 signatures using the X-Hub-Signature-256 header. Show me the Nginx config for IP allowlisting.
Frequently asked questions
Which authentication method should I use for n8n webhooks?
Use Header Auth (API key) for service-to-service integrations where you control both sides. Use Basic Auth for simple setups. Use JWT for multi-tenant applications. For provider webhooks (GitHub, Stripe), use HMAC signature verification.
Can I use JWT authentication with n8n webhooks?
Yes. Select 'JWT Auth' in the Webhook node's Authentication section. Configure the JWT secret or JWKS endpoint. n8n validates the JWT token in the Authorization header on each request.
How do I find the IP addresses of webhook providers?
Most providers publish their IP ranges. GitHub: api.github.com/meta. Stripe: stripe.com/docs/ips. Shopify: help.shopify.com. Check the provider's documentation for the current list.
Does HMAC verification work with the Webhook node's raw body?
The HMAC signature must be computed on the exact raw request body. If n8n parses the body before your Code node runs, the JSON re-serialization may differ from the original. Test with your specific provider to ensure the computed signature matches.
Can I rate-limit webhook requests in n8n?
n8n does not have built-in rate limiting. Configure rate limiting at the reverse proxy level (Nginx limit_req module or Caddy rate_limit plugin) to throttle requests per IP address.
Can RapidDev help me secure my n8n webhook integrations?
Yes. RapidDev can implement comprehensive webhook security including authentication, HMAC verification, IP allowlisting, rate limiting, and security monitoring for production n8n deployments.
Talk to an Expert
Our team has built 600+ apps. Get personalized help with your project.
Book a free consultation