Webhook Security
This guide covers security best practices and implementation details for both incoming and outgoing webhooks in the GxP platform.
HMAC Signature Verification
GxP uses HMAC-SHA256 signatures to ensure webhook authenticity and integrity. Both incoming and outgoing webhooks use this mechanism.
How HMAC Signatures Work
- The sender computes a hash of the request payload using a shared secret
- The signature is included in the request headers
- The receiver recomputes the hash and compares it to the received signature
- If signatures match, the request is authentic and unmodified
Signature Format
All GxP webhook signatures use the format:
sha256={hex_encoded_signature}
Outgoing Webhook Signatures
When GxP sends webhooks to your server, each request includes these security headers:
| Header | Description |
|---|---|
X-GxP-Signature | HMAC-SHA256 signature of the payload |
X-GxP-Event | Event type (e.g., attendee.created) |
X-GxP-Delivery-ID | Unique delivery identifier |
X-GxP-Timestamp | Unix timestamp when the webhook was sent |
Verifying Signatures
PHP
<?php
function verifyGxPWebhook(string $payload, string $signature, string $secret): bool
{
$expectedSignature = 'sha256=' . hash_hmac('sha256', $payload, $secret);
return hash_equals($expectedSignature, $signature);
}
// In your webhook handler
$payload = file_get_contents('php://input');
$signature = $_SERVER['HTTP_X_GXP_SIGNATURE'] ?? '';
$secret = getenv('GXP_WEBHOOK_SECRET');
if (!verifyGxPWebhook($payload, $signature, $secret)) {
http_response_code(401);
echo json_encode(['error' => 'Invalid signature']);
exit;
}
// Process the verified webhook
$event = json_decode($payload, true);
Node.js
const crypto = require('crypto');
function verifyGxPWebhook(payload, signature, secret) {
const expectedSignature = 'sha256=' +
crypto.createHmac('sha256', secret)
.update(payload)
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expectedSignature)
);
}
// Express.js example
app.post('/webhooks/gxp', (req, res) => {
const payload = JSON.stringify(req.body);
const signature = req.headers['x-gxp-signature'];
const secret = process.env.GXP_WEBHOOK_SECRET;
if (!verifyGxPWebhook(payload, signature, secret)) {
return res.status(401).json({ error: 'Invalid signature' });
}
// Process the verified webhook
console.log('Verified event:', req.body.event);
res.status(200).send('OK');
});
Python
import hmac
import hashlib
def verify_gxp_webhook(payload: bytes, signature: str, secret: str) -> bool:
expected_signature = 'sha256=' + hmac.new(
secret.encode('utf-8'),
payload,
hashlib.sha256
).hexdigest()
return hmac.compare_digest(expected_signature, signature)
# Flask example
from flask import Flask, request, jsonify
@app.route('/webhooks/gxp', methods=['POST'])
def handle_gxp_webhook():
payload = request.get_data()
signature = request.headers.get('X-GxP-Signature', '')
secret = os.environ['GXP_WEBHOOK_SECRET']
if not verify_gxp_webhook(payload, signature, secret):
return jsonify({'error': 'Invalid signature'}), 401
# Process the verified webhook
event = request.get_json()
return jsonify({'status': 'ok'}), 200
Go
package main
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"io"
"net/http"
"os"
)
func verifyGxPWebhook(payload []byte, signature, secret string) bool {
mac := hmac.New(sha256.New, []byte(secret))
mac.Write(payload)
expectedSignature := "sha256=" + hex.EncodeToString(mac.Sum(nil))
return hmac.Equal([]byte(expectedSignature), []byte(signature))
}
func webhookHandler(w http.ResponseWriter, r *http.Request) {
payload, _ := io.ReadAll(r.Body)
signature := r.Header.Get("X-GxP-Signature")
secret := os.Getenv("GXP_WEBHOOK_SECRET")
if !verifyGxPWebhook(payload, signature, secret) {
http.Error(w, "Invalid signature", http.StatusUnauthorized)
return
}
// Process the verified webhook
w.WriteHeader(http.StatusOK)
}
Timestamp Validation
To prevent replay attacks, also validate the X-GxP-Timestamp header:
<?php
function isTimestampValid(int $timestamp, int $toleranceSeconds = 300): bool
{
$currentTime = time();
return abs($currentTime - $timestamp) <= $toleranceSeconds;
}
// In your webhook handler
$timestamp = (int) ($_SERVER['HTTP_X_GXP_TIMESTAMP'] ?? 0);
if (!isTimestampValid($timestamp)) {
http_response_code(401);
echo json_encode(['error' => 'Request timestamp too old']);
exit;
}
Incoming Webhook Security
When sending webhooks TO GxP, different authentication methods are used depending on the endpoint.
HMAC Signature (Most Endpoints)
For endpoints requiring HMAC authentication:
# Generate signature
PAYLOAD='{"gateway_id":"GW-001","status":"online"}'
SECRET="your-webhook-secret"
SIGNATURE=$(echo -n "$PAYLOAD" | openssl dgst -sha256 -hmac "$SECRET" | cut -d' ' -f2)
# Send webhook
curl -X POST "https://api.gramercy.cloud/webhook/v1/mqtt-gateway-status" \
-H "Content-Type: application/json" \
-H "X-GxP-Signature: sha256=$SIGNATURE" \
-d "$PAYLOAD"
GCP ID Token (Google Cloud Endpoints)
For GCP Pub/Sub and Cloud Storage notifications:
# Token is automatically included by GCP services
# Configure your GCP service to use the GxP service account
curl -X POST "https://api.gramercy.cloud/webhook/v1/access-zone-presence-update" \
-H "Authorization: Bearer $(gcloud auth print-identity-token)" \
-H "Content-Type: application/json" \
-d "$PAYLOAD"
Secret Management
Generating Secrets
Generate strong webhook secrets:
# Generate a 32-character random secret
openssl rand -hex 32
# Or using PHP
php -r "echo bin2hex(random_bytes(32));"
# Or using Node.js
node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"
Storing Secrets
- Environment Variables: Store secrets in environment variables, never in code
- Secret Management Services: Use services like AWS Secrets Manager, Google Secret Manager, or HashiCorp Vault
- Encryption at Rest: Ensure secrets are encrypted when stored
Rotating Secrets
When rotating webhook secrets:
- Generate a new secret
- Update the webhook subscription with the new secret
- Configure your server to accept both old and new signatures temporarily
- Remove the old secret after all in-flight webhooks are processed
<?php
function verifyWithMultipleSecrets(string $payload, string $signature, array $secrets): bool
{
foreach ($secrets as $secret) {
$expectedSignature = 'sha256=' . hash_hmac('sha256', $payload, $secret);
if (hash_equals($expectedSignature, $signature)) {
return true;
}
}
return false;
}
// During rotation, check both secrets
$secrets = [
getenv('GXP_WEBHOOK_SECRET'),
getenv('GXP_WEBHOOK_SECRET_OLD'),
];
$isValid = verifyWithMultipleSecrets($payload, $signature, array_filter($secrets));
IP Allowlisting
For additional security, configure IP allowlists for incoming webhooks.
GxP Outgoing Webhook IPs
GxP sends outgoing webhooks from these IP ranges:
# Production
34.102.136.180/32
35.186.224.25/32
# Check the latest IPs at:
# https://api.gramercy.cloud/api-specs/webhook-ips.json
Configuring Allowlists
- Navigate to Project Settings > Integrations > Webhooks
- Select the webhook subscription
- Add allowed IP addresses or CIDR ranges
- Save changes
TLS/SSL Requirements
All webhook communication must use HTTPS:
- Minimum TLS Version: TLS 1.2
- Certificate Requirements: Valid certificate from a trusted CA
- Self-Signed Certificates: Not supported in production
Testing with Self-Signed Certificates
For development only:
// In your webhook subscription configuration (development only)
'ssl_verify' => false
Warning: Never disable SSL verification in production.
Rate Limiting
Outgoing Webhook Rate Limits
GxP rate limits outgoing webhook deliveries:
| Limit Type | Value |
|---|---|
| Per endpoint per minute | 1000 requests |
| Per project per minute | 10000 requests |
| Concurrent connections | 10 |
Incoming Webhook Rate Limits
Incoming webhook endpoints have rate limits:
| Endpoint | Rate Limit |
|---|---|
mqtt-gateway-status | 100/minute |
access-zone-presence-update | Custom (waldo-webhooks group) |
| Other endpoints | 60/minute |
Error Handling
Handling Verification Failures
When signature verification fails:
- Log the failure for security auditing
- Return HTTP 401 Unauthorized
- Do not process the payload
- Monitor for repeated failures (potential attack)
<?php
function handleVerificationFailure(string $signature, string $remoteIp): void
{
Log::warning('Webhook signature verification failed', [
'signature' => substr($signature, 0, 20) . '...',
'remote_ip' => $remoteIp,
'timestamp' => now(),
]);
// Optionally: Track failure rate and alert on anomalies
$failureKey = "webhook_failures:{$remoteIp}";
$failures = Cache::increment($failureKey);
if ($failures > 10) {
// Alert security team
dispatch(new SecurityAlertJob('Excessive webhook verification failures', [
'ip' => $remoteIp,
'failures' => $failures,
]));
}
}
Security Checklist
Before going to production, verify:
- HMAC signature verification is implemented
- Timestamp validation prevents replay attacks
- Webhook secrets are stored securely
- HTTPS is enforced for all endpoints
- IP allowlisting is configured (optional but recommended)
- Rate limiting is in place
- Verification failures are logged
- Secret rotation procedure is documented
- Error responses don't leak sensitive information