Last updated: Jun 08, 2026

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

  1. You configure a secret when creating the webhook
  2. Infraspeak signs each payload with HMAC-SHA256 using your secret
  3. The signature is sent in the X-Hook-Secret header
  4. 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

  1. Only register HTTPS URLs for webhooks:
Good
✓ https://your-app.com/hook

Bad
✗ http://your-app.com/hook
  1. 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')
  1. 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):
    ...
  1. 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...
  1. 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