Five years ago, when we first plumbed Stripe into our billing service, the senior engineer at the time added an IP allow-list to the webhook endpoint. The thinking was reasonable in isolation — defence in depth, surface area, etc. — and it sat untouched in the load balancer config until last week, when Stripe rotated a part of their range and our 3 a.m. pager went off.
The fix wasn’t to update the allow-list. The fix was to delete it. Stripe signs every webhook with Stripe-Signature and ships an HMAC-SHA256 of the raw body plus a timestamp. Verify that with your endpoint secret and you have a stronger guarantee than any IP filter could offer: not just ‘this came from Stripe’s network’ but ‘this came from Stripe and hasn’t been tampered with and is less than five minutes old’.
The trap is using your framework’s parsed JSON body. The signature is over the raw bytes; the moment your middleware re-serialises with even a different key order, verification fails. We pin the route to a raw-body parser and pass the buffer straight through.
Also: the timestamp matters. Without a freshness check, a captured payload can be replayed indefinitely. The Stripe-Signature header includes a t= value; reject anything outside a five-minute window and you’ve closed the loop.
Useful generalisation: prefer cryptographic guarantees over network ones wherever the provider offers them. Networks reshape themselves; signatures don’t.