The GitHub Webhook Server provides two complementary verification mechanisms to ensure webhook authenticity: IP allowlisting and HMAC-SHA256 signature verification.
Overview
Webhook verification protects against:
Unauthorized webhook deliveries from non-GitHub sources
Replay attacks using intercepted webhook payloads
Man-in-the-middle attacks attempting to inject malicious data
DDoS attacks from spoofed webhook sources
IP Allowlist Verification
Restrict webhook processing to requests originating from trusted IP ranges.
GitHub IP Ranges
Enable GitHub IP verification:
# config.yaml
verify-github-ips : true
Or via environment variable:
# docker-compose.yaml
environment:
- VERIFY_GITHUB_IPS= 1 # or "true"
Verified IP ranges:
The server automatically validates against GitHub’s current IP ranges:
192.30.252.0/22 - GitHub.com webhooks
185.199.108.0/22 - GitHub Pages and Actions
140.82.112.0/20 - GitHub Services
143.55.64.0/20 - GitHub Enterprise Cloud
IP ranges are fetched dynamically from GitHub’s API to ensure accuracy.
Cloudflare IP Ranges
If your webhook server sits behind Cloudflare:
# config.yaml
verify-cloudflare-ips : true
verify-github-ips : false # Disable GitHub IP check when using Cloudflare
Or via environment variable:
environment:
- VERIFY_CLOUDFLARE_IPS= 1
When to use Cloudflare verification:
Webhook server deployed behind Cloudflare proxy
Using Cloudflare Tunnel (cloudflared)
Cloudflare DDoS protection enabled
When using Cloudflare, disable GitHub IP verification (verify-github-ips: false) because all requests will originate from Cloudflare IP ranges, not GitHub IPs.
Combined IP Verification
Incorrect configuration (both enabled):
# ❌ WRONG - Don't enable both simultaneously
verify-github-ips : true
verify-cloudflare-ips : true
This will reject all webhooks because requests cannot originate from both GitHub and Cloudflare IPs simultaneously.
Correct configurations:
# ✅ Direct GitHub webhooks (no proxy)
verify-github-ips : true
verify-cloudflare-ips : false
# ✅ Cloudflare proxy in front of webhook server
verify-github-ips : false
verify-cloudflare-ips : true
IP Verification Logging
Monitor IP verification in logs:
# Successful verification
grep "IP verified" webhook-server.log
# Failed verification attempts
grep "IP not in allowlist" webhook-server.log
# Show rejected IP addresses
grep "Unauthorized IP" webhook-server.log | awk '{print $NF}'
Testing IP Verification
Test from allowed IP:
# From GitHub IP range (will succeed)
curl -X POST http://your-server:5000/webhook_server \
-H "Content-Type: application/json" \
-H "X-GitHub-Event: ping" \
-d '{"zen": "test"}'
Test from blocked IP:
# From non-GitHub IP (will be rejected)
curl -X POST http://your-server:5000/webhook_server \
-H "Content-Type: application/json" \
-H "X-GitHub-Event: ping" \
-d '{"zen": "test"}'
# Response: {"detail": "Unauthorized IP address"}
HMAC-SHA256 Signature Verification
GitHub signs all webhook payloads using HMAC-SHA256 to verify authenticity.
In configuration file:
# config.yaml
webhook-secret : "your-secure-random-secret" # pragma: allowlist secret
Via environment variable (recommended):
# .env or docker-compose.yaml
WEBHOOK_SECRET = your-secure-random-secret
Generate strong webhook secret:
# Linux/macOS - 32 random bytes in hex
openssl rand -hex 32
# Output example:
a3f7c8e9d2b1f6a4e8c7d9f1a2b3c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1
Navigate to repository settings: Settings → Webhooks → Add webhook
Set Payload URL : https://your-domain.com/webhook_server
Set Content type : application/json
Set Secret : Enter the same secret from your configuration
Select events or choose “Send me everything”
Ensure Active is checked
Click “Add webhook”
How Signature Verification Works
GitHub’s signing process:
GitHub creates HMAC-SHA256 signature of payload using your secret
Signature sent in X-Hub-Signature-256 header: sha256=<signature>
Webhook server receives payload and header
Server verification process:
Server extracts signature from X-Hub-Signature-256 header
Server computes HMAC-SHA256 of received payload using configured secret
Server compares computed signature with GitHub’s signature
If signatures match, webhook is authentic
Example verification:
import hmac
import hashlib
def verify_signature ( payload : bytes , signature_header : str , secret : str ) -> bool :
# Extract signature from header
expected_signature = signature_header.replace( 'sha256=' , '' )
# Compute HMAC-SHA256 signature
computed_signature = hmac.new(
key = secret.encode( 'utf-8' ),
msg = payload,
digestmod = hashlib.sha256
).hexdigest()
# Constant-time comparison to prevent timing attacks
return hmac.compare_digest(computed_signature, expected_signature)
Signature Verification Logging
Monitor signature verification:
# Successful verification
grep "Signature verified" webhook-server.log
# Failed verification (incorrect secret)
grep "signatures didn't match" webhook-server.log
# Missing signature header
grep "No signature provided" webhook-server.log
Testing Signature Verification
Test webhook with valid signature:
#!/bin/bash
SECRET = "your-webhook-secret"
PAYLOAD = '{"zen": "test webhook"}'
SIGNATURE = $( echo -n " $PAYLOAD " | openssl dgst -sha256 -hmac " $SECRET " | sed 's/^.*= //' )
curl -X POST http://your-server:5000/webhook_server \
-H "Content-Type: application/json" \
-H "X-GitHub-Event: ping" \
-H "X-Hub-Signature-256: sha256= $SIGNATURE " \
-d " $PAYLOAD "
Test with invalid signature:
curl -X POST http://your-server:5000/webhook_server \
-H "Content-Type: application/json" \
-H "X-GitHub-Event: ping" \
-H "X-Hub-Signature-256: sha256=invalid_signature" \
-d '{"zen": "test"}'
# Response: {"detail": "Invalid signature"}
Configuration Examples
Production Configuration
Maximum security (recommended):
# config.yaml
webhook-secret : "${WEBHOOK_SECRET}" # From environment variable
verify-github-ips : true
verify-cloudflare-ips : false
disable-ssl-warnings : true
# docker-compose.yaml
environment:
- WEBHOOK_SECRET= ${ WEBHOOK_SECRET } # From .env file
- VERIFY_GITHUB_IPS= 1
With Cloudflare proxy:
# config.yaml
webhook-secret : "${WEBHOOK_SECRET}"
verify-github-ips : false # Disabled - behind Cloudflare
verify-cloudflare-ips : true
Development Configuration
Testing without verification (NOT for production):
# config.yaml - development only
verify-github-ips : false
verify-cloudflare-ips : false
# webhook-secret: "" # Optional - disable signature verification
Never disable verification in production environments. This leaves your webhook server vulnerable to unauthorized access and malicious payloads.
Testing with smee.io
When using smee.io for local development:
# config.yaml
webhook-ip : https://smee.io/your-channel
webhook-secret : "your-test-secret"
verify-github-ips : false # smee.io proxies requests
verify-cloudflare-ips : false
Troubleshooting
Issue: Webhooks Rejected with “Unauthorized IP”
Symptom:
ERROR: Unauthorized IP address: 203.0.113.42
Solutions:
Check if using proxy/CDN:
# If behind Cloudflare
verify-cloudflare-ips : true
verify-github-ips : false
Verify GitHub IP ranges:
# Fetch current GitHub IP ranges
curl https://api.github.com/meta | jq '.hooks'
Check reverse proxy configuration:
# Ensure X-Forwarded-For header is passed
proxy_set_header X-Forwarded-For $ proxy_add_x_forwarded_for ;
Issue: Signature Verification Fails
Symptom:
ERROR: Webhook signatures didn't match
Solutions:
Verify secret matches GitHub configuration:
Check GitHub webhook settings
Ensure no extra whitespace in secret
Secret is case-sensitive
Check environment variable:
# Verify secret is loaded
docker exec github-webhook-server env | grep WEBHOOK_SECRET
Verify payload not modified:
Reverse proxy must pass raw payload unchanged
Disable request body buffering if necessary
Check webhook secret syntax:
# ✅ CORRECT
webhook-secret : "my-secret-123"
# ❌ WRONG - missing quotes
webhook-secret : my-secret-123
Issue: All Webhooks Rejected (Both IPs Enabled)
Symptom:
ERROR: IP verification failed
Solution:
# ❌ WRONG - both enabled
verify-github-ips : true
verify-cloudflare-ips : true
# ✅ CORRECT - choose one
verify-github-ips : true
verify-cloudflare-ips : false
Symptom:
WARNING: No X-Hub-Signature-256 header found
Solutions:
Verify GitHub webhook configuration:
Ensure “Secret” field is configured in GitHub
Re-save webhook configuration in GitHub
Check reverse proxy:
# Ensure headers are passed
proxy_pass_request_headers on ;
Test with manual curl:
# Include signature header
curl -X POST http://localhost:5000/webhook_server \
-H "X-Hub-Signature-256: sha256=..." \
-d '{...}'
Security Best Practices
1. Always Enable Both Verifications
For maximum security, enable both IP and signature verification:
webhook-secret : "${WEBHOOK_SECRET}" # HMAC signature
verify-github-ips : true # IP allowlist
2. Rotate Webhook Secrets Regularly
Implement secret rotation schedule:
Generate new webhook secret
Update GitHub webhook configuration with new secret
Update webhook server configuration
Restart webhook server
Verify webhook deliveries succeed
3. Monitor Verification Failures
Set up alerts for verification failures:
# Alert on signature verification failures
grep "signatures didn't match" webhook-server.log | \
mail -s "Webhook Security Alert" [email protected]
4. Use Environment Variables for Secrets
Never commit secrets to version control:
# config.yaml
webhook-secret : "${WEBHOOK_SECRET}" # ✅ From environment
# ❌ NEVER do this:
# webhook-secret: "hardcoded-secret-123"
5. Test Verification Before Production
Always test webhook verification:
Configure webhook in GitHub
Trigger test webhook (Settings → Webhooks → Recent Deliveries → Redeliver)
Verify successful delivery in logs
Test with invalid signature to confirm rejection
Next Steps
Security Overview Comprehensive security architecture and best practices
Log Viewer Security Secure the unauthenticated log viewer endpoints