Webhook Signatures: HMAC and Replay Prevention
Your service delivers webhooks to customer endpoints. How does the customer know the POST came from you and not an attacker? Without authentication, any party that knows a customer’s webhook URL can forge events. Webhook signatures solve this: you sign each payload with a shared secret, the customer verifies the signature before processing.
HMAC-SHA256 Signing#
Generate a secret per customer endpoint (not one global secret: if one leaks, only that customer is affected). On delivery, compute a signature over the payload:
signature = HMAC-SHA256(secret, payload_body)
Send the signature in a header: X-Webhook-Signature: sha256=<hex_digest>. The customer computes the same HMAC with their stored secret and compares. If it matches, the payload is authentic and unmodified.
Use a constant-time comparison (not string equality) to prevent timing attacks that could leak the expected signature byte by byte.
Replay Attacks#
HMAC proves authenticity but not freshness. An attacker who captured a legitimate webhook can replay it hours later. The signature is still valid: same payload, same secret, same HMAC.
Fix: include a timestamp in the payload and the HMAC computation:
signature = HMAC-SHA256(secret, timestamp + "." + payload_body)
Send X-Webhook-Timestamp: 1722556800 alongside the signature. On receipt, customers reject events where the timestamp is more than 5 minutes old. Even a captured webhook is useless after 5 minutes.
Secret Rotation Without Downtime#
Customers need to rotate their webhook secrets periodically (security hygiene, suspected leak). Rotating immediately breaks in-flight webhooks signed with the old secret.
Two-phase rotation: accept both old and new secret for a 10-minute overlap window. Customer generates a new secret and registers it. During overlap, your service tries verification with both secrets. Customer confirms rotation complete. You drop the old secret.
Store secrets with version numbers. The webhook header can include X-Webhook-Secret-Version: 2 so the customer knows which secret to try first, avoiding unnecessary HMAC computations.
What Not to Sign#
Don’t sign using query parameters (logged by proxies and web servers). Don’t put secrets in URLs. Header-based signatures avoid accidental secret exposure in access logs.
At Salesforce#
Salesforce’s outbound message system (the legacy webhook mechanism for workflow rules) used a certificate-based signature scheme rather than HMAC. Each delivery was signed with a Salesforce-issued X.509 certificate, and customers verified against a published public key. This avoided per-customer secret management but created a shared-failure surface: when the signing certificate was rotated once every 2 years, customers who hadn’t updated their trust store started rejecting all deliveries simultaneously. 140 customers affected in a single rotation event. HMAC-per-customer would have scoped the rotation risk to one customer at a time.
What I’m Learning#
HMAC-SHA256 with a per-endpoint secret plus timestamp replay prevention covers the main threat model for webhook security. The complexity is in secret rotation: two-phase overlap with versioned secrets prevents the cutover gap. Certificate-based schemes shift secret management to certificate lifecycle and create correlated failure on rotation.
What webhook security mechanism have you implemented, and how did you handle secret rotation in production?