Skip to main content
Version: v1 (Current)

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

  1. The sender computes a hash of the request payload using a shared secret
  2. The signature is included in the request headers
  3. The receiver recomputes the hash and compares it to the received signature
  4. 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:

HeaderDescription
X-GxP-SignatureHMAC-SHA256 signature of the payload
X-GxP-EventEvent type (e.g., attendee.created)
X-GxP-Delivery-IDUnique delivery identifier
X-GxP-TimestampUnix 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

  1. Environment Variables: Store secrets in environment variables, never in code
  2. Secret Management Services: Use services like AWS Secrets Manager, Google Secret Manager, or HashiCorp Vault
  3. Encryption at Rest: Ensure secrets are encrypted when stored

Rotating Secrets

When rotating webhook secrets:

  1. Generate a new secret
  2. Update the webhook subscription with the new secret
  3. Configure your server to accept both old and new signatures temporarily
  4. 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

  1. Navigate to Project Settings > Integrations > Webhooks
  2. Select the webhook subscription
  3. Add allowed IP addresses or CIDR ranges
  4. 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 TypeValue
Per endpoint per minute1000 requests
Per project per minute10000 requests
Concurrent connections10

Incoming Webhook Rate Limits

Incoming webhook endpoints have rate limits:

EndpointRate Limit
mqtt-gateway-status100/minute
access-zone-presence-updateCustom (waldo-webhooks group)
Other endpoints60/minute

Error Handling

Handling Verification Failures

When signature verification fails:

  1. Log the failure for security auditing
  2. Return HTTP 401 Unauthorized
  3. Do not process the payload
  4. 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