In regulated industries — financial services, healthcare, enterprise SaaS — security teams often require that inbound HTTP traffic be restricted to known source IP ranges. For webhook integrations, that means maintaining a firewall allowlist of your provider's egress IPs: if the source IP isn't on the list, the connection is dropped before it reaches your application.
The logic is sound in isolation. In practice, static IP allowlists for webhooks are a reliability trap. Provider IP ranges change when they expand regions, migrate infrastructure, or add DDoS mitigation capacity. When that happens and your rules haven't updated, webhooks drop silently — no error, no retry, no alert. Just missing events.
This post covers how to automate CIDR synchronization so IP allowlisting doesn't become an operational liability, where it genuinely adds value, and where HMAC signature verification is a more robust primary control.
Why Provider IP Ranges Change
Major webhook providers change their egress ranges for predictable reasons: cloud region expansion, infrastructure migrations to new providers, load balancer fleet rotations, and anti-abuse infrastructure changes. The change cadence is not theoretical:
| Provider | CIDR endpoint | Typical change frequency |
|---|---|---|
| Stripe | https://stripe.com/files/ips/ips_webhooks.txt | Several times per year |
| GitHub | https://api.github.com/meta → hooks field | Several times per year |
| Shopify | https://www.shopify.com/shopify.json | Irregular |
| Twilio | Support docs only — no machine-readable feed | Infrequent |
| PagerDuty | Developer docs only — no machine-readable feed | Irregular |
Three of five major providers publish machine-readable feeds; two publish documentation only. For the docs-only providers, your options are to automate scraping (brittle) or set a calendar reminder to audit manually (operationally expensive and easy to forget).
How Static Allowlists Fail
Silent delivery failures. When a provider adds a new egress CIDR and your rules don't include it, webhooks from that range are dropped at the network layer. Your application never sees the request — no retry fires, no error is logged. The failure is invisible until a customer reports a missing event or a downstream process stalls.
IPv6 blind spots. Most allowlists are written for IPv4. GitHub and Stripe both publish IPv6 ranges in their feeds. If your security groups don't include the IPv6 CIDR blocks, traffic from those addresses is silently dropped.
Shared egress infrastructure. Some providers route delivery through infrastructure shared with their own customers or other tenants. Allowlisting their webhook ranges may permit traffic from other organizations that happen to share the same cloud CIDR. IP allowlisting validates infrastructure origin, not request authenticity.
Rule accumulation and range reassignment. Old ranges aren't always immediately decommissioned. If you add rules when providers expand but never remove them when they contract, your allowlist grows unbounded. If a retired CIDR is later reassigned to a different organization, your rules now permit traffic you never intended to allow.
Automating CIDR Synchronization
The reliable approach is to fetch current IP ranges on a schedule, diff them against your existing rules, and apply the delta atomically. Here's a Go implementation that fetches from Stripe and GitHub:
package cidr
import (
"encoding/json"
"fmt"
"io"
"net/http"
"strings"
)
const (
stripeWebhookIPsURL = "https://stripe.com/files/ips/ips_webhooks.txt"
githubMetaURL = "https://api.github.com/meta"
)
// FetchStripeWebhookCIDRs returns Stripe's current webhook egress CIDR blocks.
func FetchStripeWebhookCIDRs() ([]string, error) {
resp, err := http.Get(stripeWebhookIPsURL)
if err != nil {
return nil, fmt.Errorf("fetching stripe IPs: %w", err)
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("reading stripe response: %w", err)
}
var cidrs []string
for _, line := range strings.Split(strings.TrimSpace(string(body)), "\n") {
line = strings.TrimSpace(line)
if line != "" && !strings.HasPrefix(line, "#") {
cidrs = append(cidrs, line)
}
}
return cidrs, nil
}
// FetchGitHubHookCIDRs returns GitHub's current webhook delivery CIDR blocks.
func FetchGitHubHookCIDRs() ([]string, error) {
resp, err := http.Get(githubMetaURL)
if err != nil {
return nil, fmt.Errorf("fetching github meta: %w", err)
}
defer resp.Body.Close()
var meta struct {
Hooks []string `json:"hooks"`
}
if err := json.NewDecoder(resp.Body).Decode(&meta); err != nil {
return nil, fmt.Errorf("decoding github meta: %w", err)
}
return meta.Hooks, nil
}Run this on a schedule — every 6 hours is a reasonable interval. Most providers publish updated ranges hours before routing traffic through them, giving you a window to apply the delta before deliveries begin.
For AWS, the sync step updates security group rules. The key discipline is tagging every managed rule with a description so your script can identify and revoke only the rules it owns, leaving manually added rules untouched:
#!/usr/bin/env bash
# sync-stripe-webhook-ips.sh — run every 6 hours via cron or ECS Scheduled Task
set -euo pipefail
GROUP_ID="${SECURITY_GROUP_ID:?}"
DESCRIPTION="stripe-webhook-managed"
PORT=443
# Fetch current Stripe ranges
CURRENT=$(curl -sf https://stripe.com/files/ips/ips_webhooks.txt | grep -v '^#' | grep -v '^$')
# Revoke existing managed rules
aws ec2 describe-security-groups \
--group-ids "$GROUP_ID" \
--query "SecurityGroups[0].IpPermissions[?FromPort==\`${PORT}\`].IpRanges[?Description==\`${DESCRIPTION}\`].CidrIp" \
--output text \
| tr '\t' '\n' | grep -v '^$' \
| while read -r OLD_CIDR; do
echo "Revoking $OLD_CIDR"
aws ec2 revoke-security-group-ingress \
--group-id "$GROUP_ID" \
--protocol tcp --port "$PORT" \
--cidr "$OLD_CIDR" || true
done
# Add current ranges
while IFS= read -r CIDR; do
echo "Authorizing $CIDR"
aws ec2 authorize-security-group-ingress \
--group-id "$GROUP_ID" \
--ip-permissions "[{
\"IpProtocol\": \"tcp\",
\"FromPort\": ${PORT},
\"ToPort\": ${PORT},
\"IpRanges\": [{\"CidrIp\": \"${CIDR}\", \"Description\": \"${DESCRIPTION}\"}]
}]" 2>/dev/null || true # ignore duplicate errors
done <<< "$CURRENT"
echo "Sync complete."Add alerting on script failure — if the fetch times out or the provider's endpoint returns a non-200, you want to know before your existing rules expire and newly routed traffic gets blocked. A dead-man's-switch in your monitoring system (alert if the job hasn't run successfully in 12 hours) is worth setting up.
Defense in Depth: IP Filtering Plus Signature Verification
IP allowlisting and HMAC signature verification are complementary, not competing, controls. Running both gives you defense in depth.
IP filtering operates at the network layer. It's fast, cheap, and eliminates most opportunistic scanners and drive-by requests before they consume application resources. A correctly maintained allowlist means your webhook handler never receives a request from an IP that shouldn't be sending one.
Signature verification operates at the application layer. It cryptographically authenticates each individual request — regardless of where it originated. A valid HMAC signature can only be produced by someone who holds the secret, which means even traffic that bypasses IP filtering is rejected without a valid signature.
The practical division:
- ›Network layer blocks unknown sources → reduces noise and attack surface.
- ›Application layer verifies every admitted request → authenticates the payload cryptographically.
Neither layer alone is sufficient. IP filtering without signature verification is vulnerable to any attacker who can send traffic from an allowlisted CIDR — trivial to do from shared cloud infrastructure. Signature verification without IP filtering admits unauthenticated traffic to your application, consuming parsing and HMAC computation resources at whatever rate attackers want to send.
When IP Allowlisting Provides Less Value Than It Appears
Two scenarios where IP allowlisting's guarantees are weaker than expected:
Shared egress infrastructure. If a provider routes webhooks through cloud compute that other tenants can also use (common with major cloud providers), your allowlist may include ranges from which an attacker can send traffic. The allowlist validates that the request came from infrastructure the provider uses — it does not validate that the provider sent it. For Stripe and GitHub, this risk is low because they use dedicated egress ranges. For smaller providers without published IP feeds, it may be significant.
DNS rebinding and redirect vulnerabilities. IP allowlisting restricts inbound connections to your webhook endpoint. It provides no protection against outbound requests your application makes as a result of processing a webhook payload. If your webhook handler is vulnerable to SSRF or follows untrusted redirects, an attacker who can send traffic from an allowlisted IP can still exploit those paths. IP filtering and application security are separate concerns.
Decision Framework
| Your situation | Recommended posture |
|---|---|
| Provider publishes machine-readable CIDR feed | Automate sync every 6h; verify signatures; alert on sync failure |
| Provider publishes docs-only CIDR list | Audit quarterly; rely primarily on signature verification |
| Provider uses shared cloud egress | IP allowlisting adds little value; rely on signature verification |
| Compliance mandates IP allowlisting | Automate sync + always verify signatures as the primary control |
| No compliance requirement | Skip IP allowlisting; HMAC signature verification alone is sufficient |
For most teams, HMAC signature verification is the right primary control. It's cryptographically stronger than source IP validation, works regardless of which datacenter or cloud region the provider uses, doesn't break when providers add capacity, and requires no maintenance when provider infrastructure changes. IP allowlisting makes sense as an additional layer when compliance mandates it — but treat it as defense in depth, not a substitute for request-level authentication.
GetHook verifies inbound webhook signatures at the gateway layer using provider presets for Stripe, GitHub, and Shopify, so your backend receives pre-authenticated events without managing CIDR lists or reimplementing HMAC verification per provider. See how inbound verification works →