Most webhook receivers pick one sender check, usually whatever the provider documents first, and never ask which threat it actually stops. HMAC signing, mutual TLS and IP allowlisting answer different questions: HMAC proves a holder of a shared secret produced these exact bytes, mTLS proves a holder of a private key opened this connection, and an allowlist proves only that the packet came from an address range. Each fails in a different way when a secret leaks, a request is replayed or the provider's egress addresses are shared with thousands of other customers. This guide compares the three by threat, architecture and failure mode, with layering patterns, worked scenarios and a decision checklist, for application security engineers, platform teams and architects. Signature implementation detail lives in the Webhook Secrets Security Guide; this page is about choosing.
Key takeaways
- Only message signatures bind identity to content. HMAC (or an asymmetric signature) covers the body and, in good schemes, a timestamp and message ID; mTLS and allowlisting authenticate the pipe, not the message.
- IP allowlisting is not authentication. Provider egress ranges are shared by every customer of that provider, so an allowlist admits an attacker who registers their own account and points it at your URL. GitHub's own documentation does not recommend allowing by IP address.
- mTLS moves the trust decision to your TLS terminator. It removes secrets from application code, but the check is only as good as how the terminator forwards the verified identity and whether anything behind it can be reached directly.
- Replay is a property of the scheme, not the transport. Signed timestamps plus deduplication on a delivery ID stop replay; mTLS and allowlists do nothing against a request resent from a permitted source.
- The webhook secret is a non-human credential. It needs an owner, a store, a rotation window and an exposure runbook, and the choice of mechanism decides how bad a leak is and how fast you can recover.
The short answer
Require message signing (HMAC, or an asymmetric scheme where the provider offers one) on every webhook route, with timestamp tolerance and deduplication. Add mTLS where the provider supports it and you already terminate client certificates, because it gives a second, independent factor that survives a leaked signing secret. Use IP allowlisting as noise reduction and as a firewall requirement for private networks, never as the control that decides whether an event is genuine. If a provider offers only an allowlist and no signature, treat every event as untrusted input and re-fetch the object from the provider API before acting on it.
A quick test for any design: assume an attacker has a free account with the same provider. Which check rejects their request to your endpoint? If the answer is "none", the design relies on the URL staying secret.
How each mechanism authenticates the sender
HMAC message signing
The provider and the receiver share a secret per endpoint. For each delivery the provider computes HMAC-SHA256 over a defined string, usually an ID, a timestamp and the raw body, and sends the result in a header. The receiver rebuilds the same string from the bytes it received, computes the HMAC with its copy of the secret and compares in constant time. The Standard Webhooks specification defines this as the v1 scheme with secrets of 24 to 64 random bytes, and an asymmetric v1a scheme using ed25519 in which the receiver holds only a public key. Verification happens in application code or a gateway, so it survives proxies that do not rewrite the body.
A close relative is a provider-signed bearer token. Google Cloud Pub/Sub push subscriptions send an OpenID Connect JWT in the Authorization header, signed by Google, and the receiver checks the signature, the aud claim and that the email claim matches the push service account. The token is not bound to the body and can be up to an hour old, so deduplication still matters.
Mutual TLS
The provider presents a client certificate during the TLS handshake and your terminator validates the chain and the identity in it. Slack documents this as an alternative to request signing: the terminator accepts client certificates under any DigiCert root, extracts the subject alternative name platform-tls-client.slack.com (or the common name), and injects it into a header such as X-Client-Certificate-SAN for the application to check. Slack requires that the application knows the terminator added the header, and rejects any other domain, including other slack.com subdomains. Verification is per connection, not per message, and it ends at the terminator: from there to the application the request is protected only by your internal network and header hygiene. See the mTLS Guide for handshake and certificate lifecycle detail.
IP allowlisting
The receiver, its firewall or its WAF accepts connections only from the provider's published egress addresses. GitHub publishes its webhook delivery addresses through GET /meta and says it changes them from time to time; Stripe publishes a webhook IP list with JSON and text downloads and gives seven days' notice of changes through its API announce mailing list; Svix publishes regional source ranges but states static source IPs are not guaranteed on every pricing tier; Twilio states it has no fixed range of addresses that issue webhooks and suggests receiving on a DMZ load balancer. The check is cheap and sheds scanner traffic, but says nothing about who controls the request inside that range.
Side-by-side comparison
| Dimension | HMAC signing | mTLS | IP allowlisting |
|---|---|---|---|
| What it proves | A holder of the shared secret produced these exact bytes | A holder of the certificate's private key opened this connection | The connection came from a listed address range |
| Binds to message content | Yes, body plus any signed timestamp and ID | No, only the connection | No |
| Distinguishes tenants of the same provider | Yes, the secret is per endpoint | Usually no: one provider certificate serves every customer, so tenancy must come from the payload | No: every customer shares the egress ranges |
| Replay protection | Yes when the scheme signs a timestamp and ID and the receiver checks them | None at message level | None |
| Where it is enforced | Application code or webhook gateway | TLS terminator (load balancer, ingress, proxy) | Firewall, security group, WAF or edge |
| Secret held by receiver | Shared secret (v1) or public key only (asymmetric) | Trust anchors only, nothing secret | Nothing secret |
| Main operational burden | Secret storage and rotation, raw-body handling | Trust store, header forwarding, terminator configuration | Keeping ranges current, false rejects after provider changes |
| Provider support | Near universal | Offered by some providers | Depends on whether the provider publishes stable ranges |
Threat model: which check stops which attack
"Stops" means the check alone rejects the forged request; "partial" means it raises cost or narrows the source.
| Threat | HMAC signing | mTLS | IP allowlisting |
|---|---|---|---|
| Internet scanner or attacker with no provider account posts a forged event | Stops | Stops | Stops |
| Attacker with their own account at the same provider points it at your URL | Stops (their secret differs) | Does not stop: the provider's certificate is the same for every customer | Does not stop: traffic comes from the same ranges |
| Captured valid request resent later | Stops with timestamp tolerance and ID deduplication | Does not stop if resent through a permitted path | Does not stop |
| Signing secret leaked from a repository, CI log or dashboard | Fails open until rotated | Unaffected (no shared secret) | Partial: attacker must also originate from the ranges, which a same-provider account achieves |
| Body tampered by an intermediary | Stops | Stops on the wire; blind to changes after the terminator | Does not stop |
| Direct access to the application behind the terminator, bypassing it | Stops (verification is in the app) | Does not stop if the forwarded identity header can be set by the caller | Does not stop if the internal path is unfiltered |
| Provider changes egress addresses or certificate chain | Unaffected | Outage risk if the trust store or SAN check is stale | Outage risk if ranges are stale |
The same-provider row is why neither mTLS nor an allowlist can stand alone for multi-tenant providers. The leaked-secret row is why mTLS is a strong second layer on HMAC: an attacker then needs the provider's private key too.
Failure modes and a worked scenario for each
HMAC signing
- Fail-open configuration: code that verifies only "if a secret is configured" accepts everything when the variable is missing after a redeploy.
- Weak freshness: no timestamp check, or a tolerance of hours, turns any logged request into a reusable token. Stripe warns that a tolerance of 0 disables the recency check entirely.
- Legacy schemes accepted: Stripe tells receivers to ignore every scheme except
v1to prevent downgrade attacks. - Shared secret everywhere: one value across staging and production means a leak in the weakest environment forges events in the strongest.
Worked scenario: a payments receiver verifies Stripe signatures correctly, but the signing secret is also present in a CI variable that a build script printed to a log. An attacker who reads the log can sign arbitrary checkout.session.completed events. Detection comes from fulfilment events with no matching provider-side delivery. Response: roll the secret with immediate expiry and re-check every order fulfilled since the exposure against the Stripe API. Re-fetching objects before high-impact actions would have made the forged events no-ops.
mTLS
- Spoofable identity header: the terminator sets
X-Client-Certificate-SAN, but a second ingress path or a misconfigured proxy lets clients send that header themselves. - Loose name matching: accepting any subject under the provider's domain, or any certificate that chains to a public root, admits certificates the provider never intended for webhook delivery. Slack's documentation requires an exact match on one name.
- Optional client certificates: terminators configured to request but not require a certificate pass unauthenticated connections through with an empty header.
Worked scenario: a team moves Slack verification from HMAC to mTLS on a shared ingress controller. A second hostname on the same controller routes to the same service without client certificate validation, and the application trusts the SAN header on both. An attacker posts directly to the second hostname with a forged header and triggers slash command actions. The fix is to strip the header at every ingress, require client certificates on the webhook hostname only, and keep HMAC verification in place so that mTLS is an additional factor rather than a replacement.
IP allowlisting
- Allowlist as the only check: any account at the provider can deliver to your endpoint from permitted addresses.
- Manual lists: ranges copied once into a security group go stale; GitHub advises updating the list periodically and monitoring its API.
- Trusting forwarded headers: allowlisting on
X-Forwarded-Forinstead of the actual peer address lets clients choose their apparent source.
Worked scenario: an internal deployment service accepts GitHub push webhooks behind a firewall that allows only the webhook delivery ranges from GET /meta, with no secret configured. An attacker creates a repository in their own account, adds a webhook pointing at the service's URL (found in a public issue) and sends push events naming internal repositories. The deployment service builds and deploys attacker-chosen refs. Adding signature verification with a per-webhook secret closes the hole; the allowlist stays as a filter.
Layering patterns by risk tier
| Tier | Example events | Minimum | Recommended layering |
|---|---|---|---|
| Informational | Notifications, analytics, chat messages that only display | Signature with timestamp check | Signature, deduplication, rate limiting at the edge |
| State-changing | Ticket updates, CI triggers, CRM sync | Signature, timestamp, deduplication, fail closed | Plus allowlist at the edge where the provider publishes stable ranges, and tenant ownership checks on referenced objects |
| High-impact | Payment fulfilment, payouts, access grants, deployments | All of the above plus re-fetch of the object from the provider API before acting | Plus mTLS where offered, a dedicated webhook gateway, and alerting on unusual event volumes |
| Private network receiver | Webhooks into on-premises or VPC-only services | Signature verification at a DMZ or gateway, not on the internal service | Allowlist at the perimeter, gateway verifies and forwards over internal mTLS |
Small vs large organisations: a small team with a few integrations gets most of the value from official verification libraries, a secrets manager and an edge allowlist where available. A large organisation should terminate all inbound webhooks at one gateway that verifies, deduplicates and queues events, so provider secrets never reach internal services. See the API Gateway Security Guide.
Provider support to plan around
Providers are listed alphabetically. The table reflects each provider's public documentation as of November 2026; confirm current details before designing around them.
| Provider | Documented sender authentication | Source address guidance |
|---|---|---|
| GitHub | HMAC-SHA256 in X-Hub-Signature-256 with a per-webhook secret; X-GitHub-Delivery for replay detection | Delivery addresses via GET /meta; GitHub states it does not recommend allowing by IP address and advises monitoring its API if you do |
| Google Cloud Pub/Sub (push) | Google-signed OIDC JWT in the Authorization header; verify signature, aud, email and email_verified | Firewalls must permit push requests; authentication is carried in the token |
| Slack | HMAC-SHA256 signing secret with a five-minute timestamp window, or mTLS with SAN platform-tls-client.slack.com | Identity carried in the signature or client certificate |
| Stripe | HMAC-SHA256 v1 signatures over timestamp and payload with per-endpoint secrets | Published webhook IP list in JSON and text, seven days' notice of changes via mailing list; Stripe advises using IP allowlisting and signature verification together |
| Svix-delivered webhooks | Standard Webhooks style signatures (see the provider's verification docs) | Published regional source ranges, with static IPs on Pro and Enterprise tiers |
| Twilio | HMAC-SHA1 in X-Twilio-Signature keyed with the account auth token; SDK validation recommended | Receive on a DMZ load balancer and proxy inward, as Twilio uses no fixed webhook range |
For providers you build yourself, prefer an asymmetric scheme (Standard Webhooks v1a with ed25519, or RFC 9421 HTTP Message Signatures with created, expires and nonce parameters), so receivers hold no secret that could forge events to anyone else.
Rotation of webhook secrets as non-human identity
Every mechanism creates a credential somebody must own. Treat each as an entry in the non-human identity inventory with the provider, endpoint, owner, storage location and the actions it can trigger.
| Credential | Blast radius if exposed | Rotation model | Exposure response |
|---|---|---|---|
| HMAC signing secret | Forged events to that endpoint until rotated | Receiver accepts a list of secrets; provider overlap window (Stripe lets the previous secret stay active for up to 7 days; Standard Webhooks signs with old and new keys) | Roll with immediate expiry, replay-check events since exposure |
| Account credential used as signing key (Twilio auth token model) | Forged webhooks and full API access | Rotate as an API credential | Treat as an account compromise |
| Provider client certificate (mTLS) | Held by the provider; receiver holds trust anchors only | Track the provider's chain changes; keep trust bundle current | Remove trust in the affected chain, fall back to signature only |
| Asymmetric public key | None if leaked; private key stays with the sender | Publish new key before switching, accept both briefly | Sender rotates; receivers refresh keys |
| Allowlist entries | Not a credential, but stale entries widen access | Automated refresh from provider feeds | Remove ranges no longer published |
The rotation sequence (add new secret to the receiver, roll at the provider, watch failures, remove the old one) is in the Secrets Rotation Runbook Template. The design point: receivers that accept multiple secrets make rotation routine, and mTLS on top of HMAC turns a secret leak from an emergency into a scheduled rotation.
Implementing and moving between models
- Inventory: list every inbound webhook, the mechanisms its provider offers, what is enforced and its highest-impact action.
- Classify by tier: apply the risk tier table and record the target layering for each route.
- Add signatures in log-only mode: compute and log verification results for one to two weeks, fix raw-body and proxy issues, then enforce and fail closed.
- Add timestamp and deduplication: store delivery IDs for longer than the tolerance window and reject repeats.
- Automate allowlists: where providers publish ranges, refresh them from the feed on a schedule and alert when a refresh fails, rather than editing rules by hand.
- Pilot mTLS on one route: require client certificates on a dedicated hostname, strip the identity header at every ingress, test with the provider's delivery tools, and keep HMAC enforced throughout.
- Roll back safely: each layer switches independently: return a route to log-only signatures, relax mTLS on the dedicated hostname or widen the allowlist, without removing code.
Decision checklist
- Does the provider sign messages? If yes, enforce it everywhere. If no, re-fetch every object from the provider API before acting and press the provider for signing.
- Is the provider multi-tenant? If yes, mTLS and allowlists cannot distinguish your integration from another customer's; signatures or payload-to-tenant checks must.
- What happens if the secret leaks? If the answer is money moves or access is granted, add re-fetch and, where offered, mTLS.
- Does the receiver sit in a private network? Put verification at the perimeter gateway and use the allowlist there.
- Does the provider publish stable ranges with change notice? If not, an allowlist will cause outages; skip it.
Detection and measurement
- Log by layer: firewall drops, TLS handshake failures and signature failures separately, to tell probing from misconfiguration.
- Correlate with provider delivery logs: processed events with no matching provider-side delivery suggest a leaked secret.
Metrics that show the design is working:
- Signature coverage: percentage of webhook routes enforcing signature, timestamp and deduplication (target 100%).
- Layering conformance: percentage of high-impact routes meeting the tier's recommended layering.
- Allowlist freshness: hours since the last successful automated refresh per provider.
- Rejected requests by layer and reason: per thousand deliveries, with thresholds.
- Time to rotate after exposure: hours from detection to the old secret being invalid.
Common failure patterns
- Replacing signatures with mTLS or an allowlist: both authenticate the provider, not your integration, so a same-provider account gets through.
- Treating a secret URL as authentication: URLs leak through logs, chat and screenshots. See the Webhook URL Leak Check.
- Trusting forwarded identity: client certificate or source IP headers that callers can set themselves.
- Acting on payload fields: trusting amounts, account IDs or roles in the event instead of re-fetching high-impact objects.
Framework mapping
| Concern | NIST SP 800-53 Rev. 5 | NIST CSF 2.0 | ATT&CK v20 |
|---|---|---|---|
| Authenticating the sending service (HMAC, mTLS, signed token) | IA-9 Service Identification and Authentication | PR.AA-03 | T1078 Valid Accounts |
| Message integrity and freshness | SC-8 Transmission Confidentiality and Integrity, SC-23 Session Authenticity | PR.DS-02 | T1557 Adversary-in-the-Middle |
| Source address filtering | SC-7 Boundary Protection, AC-4 Information Flow Enforcement | PR.IR-01 | |
| Secret and key lifecycle | IA-5 Authenticator Management, SC-12 Cryptographic Key Establishment and Management | PR.AA-01 | T1552.001 Credentials In Files |
| Validating payloads before acting | SI-10 Information Input Validation | PR.AA-05 | |
| Logging verification outcomes | AU-2 Event Logging, SI-4 System Monitoring | DE.CM-09 |
In ISO/IEC 27001:2022 Annex A the closest controls are 5.17 Authentication information, 8.20 Networks security, 8.24 Use of cryptography and 8.15 Logging.
Practitioner checklist
- Enforce message signatures on every webhook route and fail closed when the secret cannot be loaded.
- Check signed timestamps within about five minutes and deduplicate on the delivery or event ID.
- Never use mTLS or an allowlist as the only check for a multi-tenant provider.
- Where you add mTLS, require client certificates, match the exact provider name and strip identity headers at every ingress.
- Refresh provider IP ranges automatically and allowlist on the real peer address, not forwarded headers.
- Re-fetch high-impact objects from the provider API before acting on an event.
- Register each signing secret as a non-human credential with owner, store and rotation date; build receivers that accept multiple secrets.
- Log rejections by layer and reason and correlate processed events with provider delivery logs.
- When you are the sender, offer asymmetric signatures so receivers hold no forging secret.
Standards and references
- Standard Webhooks: Specification
- IETF: RFC 9421 HTTP Message Signatures
- Slack Developer Docs: Verifying requests from Slack
- GitHub Docs: Best practices for using webhooks
- Stripe Docs: Receive Stripe events in your webhook endpoint
- Stripe Docs: Domains and IP addresses
- Google Cloud: Authenticate push subscriptions
Related NHI Mgmt Group resources: Webhook Secrets Security Guide · mTLS Guide · mTLS vs Signed Tokens for Service Authentication · Web Bot Auth and HTTP Message Signatures for Agents · Secrets Management Guide