n8n Webhook Security: Verifying Signatures

n8n Webhook Security: Verifying Signatures

n8n webhook security usually stops at turning on an authentication option and calling it done. That is the gap worth closing, because n8n’s built-in webhook authentication does not do what most people assume it does. A provider that signs its webhook payloads with a cryptographic signature, the way Stripe, GitHub, and most serious platforms do, is not verified by n8n’s own Header Auth setting, no matter how correctly that setting is configured.

n8n Webhook Security: What “Authentication” Actually Checks

The Webhook node’s own settings offer exactly three ways to require authentication before it accepts a request. Per n8n’s own documentation on the Webhook node, “you can require authentication for any service calling your webhook URL,” choosing from Basic auth, Header auth, JWT auth, or None. Not one of the three computes anything against the request body. Basic auth checks a username and password. JWT auth validates a signed token against a passphrase or key. Header auth, the option most often reached for when a provider mentions a “signature header,” is documented plainly on n8n’s own webhook credentials page: “use this generic authentication if your app or service supports header authentication. To configure this credential, enter: the header Name you need to pass… the Value for the header.” That is a fixed name paired with one fixed, pre-configured value. n8n checks whether the incoming header matches that stored value exactly. It does not compute a hash of anything.

A provider’s real signature header works differently, and the difference is exactly what Header Auth cannot do. Stripe, GitHub, and comparable platforms compute an HMAC, a keyed cryptographic hash, using a shared secret, and send the result in a header that is different on every single request, because the underlying data being hashed is different on every single request. The exact bytes fed into that hash vary by provider: GitHub hashes the raw request body directly, while Stripe hashes a constructed string. Per Stripe’s own webhook documentation, “Stripe generates signatures using a hash-based message authentication code (HMAC) with SHA-256,” computed over “a `signed_payload` string” built by concatenating the request timestamp, a literal full stop, and the raw JSON body, not the body alone. Either way, the signature depends on the specific bytes of that individual request. Configuring Header Auth with a static expected value has nothing to compare that dynamic, per-request signature against; there is no version of “the value for the header” that is correct for more than one request at a time. Turning on Header Auth against a provider’s signature header either fails on the very first real webhook, or, if left disabled to avoid that, provides no actual verification at all.

Verifying a Real Signature Needs Its Own Step

Checking a provider’s real signature has to happen as an explicit step inside the workflow, immediately after the Webhook node, using a Code node to compute the same HMAC the provider computed and compare the two values. That computation needs the same shared secret both sides already agree on, the same hashing algorithm the provider documents for that specific header, and, critically, the exact same bytes the provider signed, not n8n’s own interpretation of what those bytes contain. Getting any one of those three wrong produces a computed signature that never matches the one in the header, which either blocks every legitimate request or, more dangerously, gets “fixed” by removing the check rather than by finding the actual mismatch.

On a self-hosted instance, that Code node has one prerequisite easy to miss until it fails: per n8n’s own configuration documentation, “for security reasons, the Code node restricts importing modules” by default, and a built-in module like `crypto` has to be explicitly allowed via the `NODE_FUNCTION_ALLOW_BUILTIN` environment variable before a Code node can `require` it; where Task Runners are enabled, that variable has to be set on the runner rather than on the main n8n process. The workflow also needs to actually stop when the comparison fails, not just note that it failed. A Code node that computes a mismatch and lets the workflow continue anyway has built a check that observes but never enforces, which is a different, weaker thing than the verification a signed webhook is meant to provide. The stop has to happen before any node that writes the payload’s data anywhere, not after.

How the two hash values are compared matters too, in a way that is easy to overlook when the comparison itself already feels like the hard part. A plain equality check on two strings can, on some implementations, return slightly faster when an early character fails to match than when a late one does, and that timing difference is small but measurable, which is the basis of a timing attack against naively compared secrets. Using a constant-time comparison function, one written specifically to take the same amount of time regardless of where a mismatch occurs, closes that gap. Most languages capable of running inside an n8n Code node ship one in their standard cryptography library; Node.js’s own `crypto.timingSafeEqual`, per its documentation, “compares the underlying bytes… using a constant-time algorithm” and “is suitable for comparing HMAC digests.” Reaching for it instead of a plain equals check is a small, cheap habit worth building into the comparison from the start rather than retrofitting later.

What n8n’s built-in webhook authentication does and does not verifyn8n’s Webhook node offers Basic auth, Header auth, and JWT auth. Header auth checks that an incoming header matches one fixed, pre-configured value, which is a different thing from computing a cryptographic signature over the request body. Verifying a provider’s real HMAC signature requires an explicit Code node step after the Webhook node, computing the same hash the provider computed over the raw request body and comparing it to the signature header, then stopping the workflow if the values do not match. Webhook nodereceives the request Header authchecks a fixed value only Real signaturestill unverified here Code nodecomputes HMAC over raw body Match: continue the workflowmismatch: stop before any write

The Raw Body Requirement Most Signature Checks Get Wrong

A signature comparison built correctly can still fail for a reason that has nothing to do with the comparison logic itself: n8n’s default behaviour parses an incoming JSON body into a structured object before a workflow ever sees it, and re-serialising that structured object back into text is not guaranteed to produce byte-for-byte the same string the provider originally signed, and frequently does not. Key order, whitespace, and number formatting can all shift during that round trip, and an HMAC is sensitive to every one of those bytes; the failure is non-deterministic rather than consistent, so a check built on the reserialised body can pass in testing and still fail intermittently once it meets real-world payload variation. Per n8n’s own documentation, the Webhook node has an option specifically for this, Raw Body, which the docs describe as letting you “specify that the Webhook node will receive data in a raw format, such as JSON or XML.” Enabling it is what makes the exact original bytes available to compute the signature against, rather than n8n’s own reinterpretation of them.

Skipping this option produces a specific, confusing failure mode: the shared secret is correct, the hashing algorithm is correct, and the comparison logic is correct, and the signature can still fail to match, often intermittently rather than every time, because the bytes being hashed are not reliably the bytes the provider actually signed. An intermittent failure like that looks identical to a genuinely wrong secret from the outside, which is why it is worth checking Raw Body first, before assuming the credential itself needs rotating.

Where Teams Get This Wrong

The most common mistake is treating Header Auth as if it were signature verification because both involve a header. Header Auth blocks requests that do not carry a specific pre-agreed value; a real signature check blocks requests whose payload does not match a value computed fresh, per request, from that payload. Configuring the first does not deliver the second.

The second common mistake is building the signature comparison in the wrong place. n8n’s own documentation on the Webhook node’s options notes that the “Only Run If” expression option “runs after IP allowlist and authentication checks” and, worth knowing specifically, “if the expression fails to evaluate, n8n logs a warning and lets the request through rather than blocking it.” An expression field that fails open on its own error is not the place to put a security check that has to fail closed; a Code node that explicitly stops the workflow on a mismatch does not share that failure mode.

The third common mistake is forgetting the Raw Body option and debugging the resulting signature mismatch as if the secret itself were wrong, rotating credentials that were never the actual problem, sometimes more than once, before anyone checks whether the workflow is hashing the same bytes the provider signed.

The fourth common mistake is treating IP allowlisting as a substitute for signature verification rather than a complement to it. n8n’s own IP(s) Allowlist option is real and useful, throwing “a 403 error” for requests from outside an approved list, but a provider’s published IP ranges can change, and an allowlist alone says nothing about whether the specific payload arriving from an approved address has actually been tampered with in transit. The two checks answer different questions and are worth running together, not as alternatives to each other.

The fifth common mistake is testing a signature check only against a hand-crafted request that is already known to be valid. That confirms the comparison logic can recognise a correct signature; it says nothing about whether the check actually rejects an incorrect one, since a bug that always returns true would pass that same test perfectly. Sending at least one deliberately wrong signature through the same workflow, and confirming the check stops it, is the test that actually exercises the failure path rather than only the success path.

For the wider webhook setup this specific check sits inside, see Advanced n8n Webhook Listeners for Real-Time SaaS and RevOps Automation. For the automation build work this applies to directly, see n8n Consultancy. For the CRM foundation these webhooks usually write into, see HubSpot Consultancy.

Go deeper: Advanced n8n Error Handling Strategies · n8n vs Zapier for RevOps Automation

Book your free audit

Frequently Asked Questions

Does turning on Header Auth in n8n verify a provider’s webhook signature?

No. n8n’s own documentation confirms Header Auth checks an incoming header against one fixed, pre-configured value, not a computed hash of the request. A provider’s real signature is different on every request, so a static value has nothing correct to compare it against.

How do you actually verify a webhook signature in n8n?

With a Code node placed immediately after the Webhook node, computing the same HMAC the provider computes over the raw request body using the shared secret, comparing it to the signature header, and stopping the workflow explicitly if the two values do not match.

Why does a correctly configured signature check sometimes still fail to match?

Usually because the Raw Body option is not enabled. n8n parses JSON into a structured object by default, and re-serialising it is not guaranteed to reproduce the exact original bytes the provider signed, which changes the computed hash even though the secret and algorithm are both correct.

Is IP allowlisting enough to secure an n8n webhook on its own?

No. n8n’s own IP(s) Allowlist option blocks requests from unapproved addresses, but it says nothing about whether a payload from an approved address has been tampered with, and a provider’s published IP ranges can change. Signature verification and IP allowlisting answer different questions and work best together.

Discover more from Equanax

Subscribe now to keep reading and get access to the full archive.

Continue reading