Webhook Security
Webhook Securitylink
Protect your webhook endpoint by verifying that requests originate from Infraspeak using HMAC signatures.
When subscribing to a webhook, you have the option to set a secret key. For each webhook, Infraspeak generates a keyed hash value from the request body, using the HMAC method with SHA-256 hashing algorithm and the secret as the HMAC key. The calculated message digest is added to the X-Hook-Secret HTTP header.
Why Verify Webhooks?link
Without verification, anyone who discovers your webhook URL could send fake events:
Attacker → Fake Event → Your Endpoint → Unintended Actions
With HMAC verification:
Attacker → Fake Event → Your Endpoint → Signature Invalid → Rejected
Infraspeak → Real Event → Your Endpoint → Signature Valid → Processed
How HMAC Verification Workslink
- You configure a
secretwhen creating the webhook - Infraspeak signs each payload with HMAC-SHA256 using your secret
- The signature is sent in the
X-Hook-Secretheader - You compute the expected signature and compare
Setting Up a Secretlink
Secret requirements:
- Minimum 6 characters
- Maximum 255 characters
- Use a cryptographically secure random string
Generating a Secure Secretlink
import secrets
# Generate a secure random secret
webhook_secret = secrets.token_urlsafe(32)
print(webhook_secret) # e.g., "Qx3mK9Lp2vNw8yZa5cBt1rHj..."
const crypto = require('crypto');
// Generate a secure random secret
const webhookSecret = crypto.randomBytes(32).toString('base64url');
console.log(webhookSecret);
// Generate a secure random secret
$webhookSecret = bin2hex(random_bytes(32));
echo $webhookSecret;
Verifying Signatureslink
From your end, when the endpoint receives a webhook, the goal is to compute a hash from the request body, using the same method (HMAC with SHA-256) and the secret you provided as the HMAC key, and ensure the hash from Infraspeak matches.
The comparison using a plain operator (==) is not advised. To protect against timing attacks, use a constant-time string comparison method.
Note: compute the hash from the raw/original request body received. If you use any pre-parsed body (e.g.: prettified, transformed/converted/decoded by the framework), the computed hash will mismatch.
Security Best Practiceslink
- Only register HTTPS URLs for webhooks:
Good
✓ https://your-app.com/hook
Bad
✗ http://your-app.com/hook
- Store secrets securely, never hardcode secrets in source code:
# Bad
WEBHOOK_SECRET = 'my-secret'
# Good
import os
WEBHOOK_SECRET = os.environ.get('INFRASPEAK_WEBHOOK_SECRET')
- Use constant-time string comparison to prevent timing attacks:
# Bad - vulnerable to timing attacks
if expected == signature:
...
# Good - constant-time comparison
import hmac
if hmac.compare_digest(expected, signature):
...
- Log security/failed events verification attempts for monitoring:
@app.route('/webhook', methods=['POST'])
def handle_webhook():
signature = request.headers.get('X-Hook-Secret')
client_ip = request.remote_addr
if not verify_signature(request.data, signature):
app.logger.warning(
f"Invalid webhook signature from {client_ip}",
extra={
'event': 'webhook_signature_invalid',
'ip': client_ip,
'path': request.path
}
)
abort(401)
# Continue processing...
- Implement rate limiting to protect against replay attacks and abuse.
Handling Webhooks Without Secretslink
If you haven't configured a secret, you can still add security layers:
- IP Allowlisting: restrict to Infraspeak's IP ranges (contact support for current list)
- Validate payload structure:
def validate_payload(payload):
"""Validate webhook payload structure."""
if not isinstance(payload, dict):
return False
if 'data' not in payload:
return False
if not isinstance(payload['data'], dict):
return False
if 'id' not in payload['data']:
return False
if 'type' not in payload['data']:
return False
return True