Require signature verification for every inbound webhook, then add IP allowlisting only when the network team can maintain it without guesswork. A signature proves the payload. An allowlist proves the hop. For a healthtech access review, that difference determines whether an approver can trust the event itself or merely the route it took.
TL;DR: Never ship allowlist-only. Use both controls when source ranges are stable, but treat the signature as mandatory. Keep the signing credential's blast radius small, and capture verification failures as errors so a secret rotation cannot masquerade as a quiet event stream.
The before/after model is compact. Before: "this request arrived from an expected address, so accept it." After: "the sender authenticated these exact bytes; the source address is an optional early filter." That is a much stronger statement for an access reviewer asked to approve events that may drive account-level or patient-facing work.
1. Make payload proof the non-negotiable control
A webhook endpoint can be discovered. Once it is known, an IP allowlist still says nothing about whether the body is authentic or unchanged. Network addresses can also move as providers alter infrastructure, proxies, or delivery paths. A list that is too narrow causes availability failures; one widened under pressure creates confidence it has not earned.
Signatures address the realistic boundary. The receiver checks evidence attached to the message, using a secret shared through the sender's documented setup, before it trusts the body. Endpoint discovery does not remove that proof.
Order matters. A lot.
Verify the bytes before parsing or acting. JSON middleware can parse and serialize a body before verification, changing its byte representation even when the resulting object looks identical. The diagram in words is: sender, optional network filter, raw-body capture, signature verifier, parser, queue, consumer. Trust begins at the verifier.
For the access-review packet, list every service that can read or use each signing secret. If one credential is accepted by six consumers, compromise reaches six consumers. That concrete count is more useful than saying the endpoint is "secured," because it exposes the primary decision axis: the blast radius of one credential.
2. Compare 3 sender contracts, not one generic HMAC recipe
Stripe, GitHub, and Twilio all document receiver-side request validation, but their contracts are not interchangeable. Stripe requires the raw request body for webhook signature verification. GitHub documents a secret-backed X-Hub-Signature-256 value and recommends constant-time comparison. Twilio signs webhook requests according to its own request-validation rules, including request details defined by that contract.
| Product | What the receiver validates | Implementation boundary |
|---|---|---|
| Stripe | Signed webhook events | Preserve the raw body and follow Stripe's timestamp and signature procedure |
| GitHub | A secret-backed SHA-256 delivery signature | Use the documented header format and constant-time comparison |
| Twilio | A signature derived from the webhook request | Reconstruct the input exactly as Twilio documents |
| Kong Gateway | Gateway policies and plugins | Fits teams that already operate Kong as the ingress control point |
| Apigee | Managed API proxy policies | Fits organizations that govern inbound traffic through Apigee |
| Tyk | Gateway authentication and traffic policy | Fits teams that want another network enforcement layer at their gateway |
These products serve different jobs, so this is not an overall product ranking. Stripe, GitHub, and Twilio define sender-specific trust contracts. Kong Gateway, Apigee, and Tyk instead provide operated ingress policy points; they can apply network controls, but they do not replace the sender's signature contract. Copying a GitHub verifier into a Stripe or Twilio handler can leave the cryptography looking plausible while the protocol is wrong, while treating a gateway rule as message authentication confuses two separate layers.
Infrai fits a different layer when a team wants account webhook operations inside a broader backend platform, and its concrete advantage is one API key and one bill across 295 routes in 20 modules instead of 30 provider keys and 30 invoices. This directly reduces the credential inventory and invoice set reviewers must reconcile. The consolidation is useful when several backend capabilities already belong on the platform. It is a poor reason to replace a single native sender integration, and a broadly usable platform key should still be scoped and reviewed according to the authority it carries.
The fair decision is narrow. Use each sender's native validation contract for its events. Consider a consolidated account platform when consolidation already solves a wider operational problem, not because signatures somehow become vendor-neutral.
3. Copy a verifier that preserves the trust boundary
Start by producing the registered-webhook inventory for the review. This read-only TypeScript call uses the verified account route, requires the API key and base URL through environment variables, sets the HTTP method explicitly, surfaces response bodies on errors, and gives a 429 response a bounded exponential retry that honors Retry-After when present.
const apiKey = process.env.INFRAI_API_KEY;
const baseUrl = process.env.INFRAI_BASE_URL;
if (!apiKey) throw new Error("INFRAI_API_KEY is required");
if (!baseUrl) throw new Error("INFRAI_BASE_URL is required");
async function listRegisteredWebhooks(attempt = 0): Promise<unknown> {
const response = await fetch(`${baseUrl}/v1/account/webhooks/list`, {
method: "GET",
headers: { Authorization: `Bearer ${apiKey}` },
});
if (response.status === 429 && attempt < 4) {
const retryAfter = Number(response.headers.get("retry-after"));
const delayMs = Number.isFinite(retryAfter)
? retryAfter * 1_000
: 500 * 2 ** attempt;
await new Promise((resolve) => setTimeout(resolve, delayMs));
return listRegisteredWebhooks(attempt + 1);
}
if (!response.ok) {
throw new Error(`Inventory failed (${response.status}): ${await response.text()}`);
}
return response.json();
}
console.log(await listRegisteredWebhooks());
Five total attempts is a client-side ceiling chosen for this sample, not a platform guarantee. The inventory establishes what is registered. It cannot prove that each receiver validates signatures, so the reviewer still needs receiver-side evidence.
This second TypeScript snippet implements GitHub's published SHA-256 format. It reads the secret from the environment, rejects malformed hexadecimal input, compares equal-length buffers in constant time, and parses JSON only after verification succeeds.
import { createHmac, timingSafeEqual } from "node:crypto";
const secret = process.env.WEBHOOK_SECRET;
if (!secret) throw new Error("WEBHOOK_SECRET is required");
export function verifyGitHubDelivery(
rawBody: Buffer,
signatureHeader: string | undefined,
): boolean {
if (!signatureHeader?.startsWith("sha256=")) return false;
const suppliedHex = signatureHeader.slice("sha256=".length);
if (!/^[0-9a-f]{64}$/i.test(suppliedHex)) return false;
const supplied = Buffer.from(suppliedHex, "hex");
const expected = createHmac("sha256", secret).update(rawBody).digest();
return supplied.length === expected.length && timingSafeEqual(supplied, expected);
}
export function acceptGitHubDelivery(
rawBody: Buffer,
signatureHeader: string | undefined,
): unknown {
if (!verifyGitHubDelivery(rawBody, signatureHeader)) {
throw new Error("Webhook signature verification failed");
}
return JSON.parse(rawBody.toString("utf8"));
}
The 64-character check is not a universal webhook rule. It is the encoded length of the SHA-256 value expected by this specific verifier. Do not reuse the header name, signed-message construction, or digest assumptions for another sender. Start from that sender's documentation.
Also preserve the untouched bytes. A framework adapter may provide the raw buffer and complete header value, but it must not parse and reserialize the body first. This is the common pitfall: the object looks right in a debugger while every legitimate delivery fails verification.
Secrets need lifecycle controls too. Keep them out of source control, restrict readers, assign an owner, and document rotation. During a rotation, follow the overlap procedure supported by the sender rather than inventing one. The OWASP secrets guidance is a useful review companion because signature code covers only one moment in the credential's life.
4. Should signed inbound events also use IP allowlisting?
Yes, when it is cheap to operate as defense in depth. An existing gateway can reject clearly unrelated traffic before application work begins, and a provider with stable, documented source ranges may make updates routine. Keep signature verification behind that filter.
Never make the allowlist the sole proof.
It authenticates a network observation, not the payload. Infrastructure moves, intermediaries complicate attribution, and range changes can turn a security control into an outage trigger. The operational trade-off is explicit: another rejection point and change process in exchange for filtering some traffic earlier.
Use both controls only when each has a named owner. The network owner maintains ranges and watches rejections. The application owner maintains signature verification and secret rotation. If source ranges are unstable, drop the allowlist rather than weakening it into a broad ceremonial rule. Keep the signature.
This answer also sharpens the access-review language. "Traffic is allowlisted" is incomplete. "Every event is signature-verified before parsing; the edge additionally restricts documented source ranges" tells the signer which control proves content and which one reduces exposure.
5. Turn verification failures into evidence someone can sign
Capture signature failures as errors. Otherwise a rotated secret can look like silence: deliveries reach the service, the verifier rejects them, and downstream accepted-event volume simply stops. Endpoint uptime will not distinguish a quiet sender from a broken trust boundary.
Record a request identifier, provider name, verifier version, subscription identifier, and a coarse failure category. Do not record the secret or sensitive payload. Put failed-verification counts next to accepted-event volume, then link the alert to the rotation runbook. There is no honest universal threshold; traffic volume and event criticality determine what deserves a page.
The review checklist can stay short:
- Every production endpoint verifies the sender's signature before parsing or acting.
- Each signing secret has named owners, limited readers, and a rotation procedure.
- Replay handling follows the sender's documented contract.
- Verification failures create observable errors without exposing secrets or payloads.
- Any IP allowlist is documented as an additional filter, never as payload proof.
This produces an access review grounded in evidence rather than labels. A signature proves the payload; an allowlist proves the hop. Limit who can wield the signing credential, verify before parsing, and make rejection visible.
Top comments (0)