Webhook Security
Signature verification
Every webhook delivery includes a X-Govern-Signature-256 header containing an HMAC-SHA256 signature of the raw request body using your webhook’s signing secret.
Always verify signatures before processing webhook events. Without verification, an attacker could send forged events to your endpoint.
How to verify
TypeScript / Node.js
import { createHmac, timingSafeEqual } from 'crypto';
function verifyGovernWebhook( payload: Buffer, // raw request body (not parsed JSON) signature: string, // X-Govern-Signature-256 header value secret: string // your webhook signing secret): boolean { const hmac = createHmac('sha256', secret); hmac.update(payload); const expected = 'sha256=' + hmac.digest('hex');
// Use timingSafeEqual to prevent timing attacks return timingSafeEqual( Buffer.from(signature, 'utf8'), Buffer.from(expected, 'utf8') );}
// Express exampleapp.post('/webhooks/govern', express.raw({ type: 'application/json' }), (req, res) => { const sig = req.headers['x-govern-signature-256'] as string; const secret = process.env.GOVERN_WEBHOOK_SECRET!;
if (!verifyGovernWebhook(req.body, sig, secret)) { return res.status(401).json({ error: 'Invalid signature' }); }
const event = JSON.parse(req.body.toString()); // Process event... res.status(200).send('OK');});Python
import hmacimport hashlibimport secrets
def verify_govern_webhook(payload: bytes, signature: str, secret: str) -> bool: expected = 'sha256=' + hmac.new( secret.encode('utf-8'), payload, hashlib.sha256 ).hexdigest() return secrets.compare_digest(signature, expected)
# FastAPI examplefrom fastapi import FastAPI, Request, HTTPException
app = FastAPI()
@app.post("/webhooks/govern")async def govern_webhook(request: Request): payload = await request.body() signature = request.headers.get("x-govern-signature-256", "") secret = os.environ["GOVERN_WEBHOOK_SECRET"]
if not verify_govern_webhook(payload, signature, secret): raise HTTPException(status_code=401, detail="Invalid signature")
event = await request.json() # Process event... return {"status": "ok"}Go
import ( "crypto/hmac" "crypto/sha256" "encoding/hex" "crypto/subtle")
func verifyGovernWebhook(payload []byte, signature, secret string) bool { mac := hmac.New(sha256.New, []byte(secret)) mac.Write(payload) expected := "sha256=" + hex.EncodeToString(mac.Sum(nil)) return subtle.ConstantTimeCompare([]byte(signature), []byte(expected)) == 1}Using the SDK
All three SDKs include a built-in webhook verifier:
import { GovernWebhooks } from '@archetypal-ai/govern';
const webhooks = new GovernWebhooks({ secret: process.env.GOVERN_WEBHOOK_SECRET!,});
app.post('/webhooks/govern', express.raw({ type: 'application/json' }), (req, res) => { try { const event = webhooks.constructEvent(req.body, req.headers['x-govern-signature-256']); // event is typed and verified handleEvent(event); res.status(200).send('OK'); } catch (err) { res.status(401).send('Webhook signature verification failed'); }});Replay attack prevention
The X-Govern-Timestamp header contains the Unix timestamp (seconds) when GOVERN sent the delivery. To prevent replay attacks, reject events older than 5 minutes:
function verifyWithTimestamp(payload: Buffer, signature: string, timestamp: string, secret: string): boolean { const ts = parseInt(timestamp, 10); const now = Math.floor(Date.now() / 1000);
if (Math.abs(now - ts) > 300) { // 5 minute window throw new Error('Webhook timestamp too old'); }
// Include timestamp in signature computation const signedPayload = `${timestamp}.${payload.toString()}`; return verifyGovernWebhook(Buffer.from(signedPayload), signature, secret);}IP allowlisting
GOVERN webhook deliveries originate from a fixed set of IP ranges. Allowlist these in your firewall for additional security:
# Fetch current IP rangescurl https://api.govern.archetypal.ai/v1/webhook-ips# ["34.120.0.0/16", "35.186.0.0/16", ...]IP ranges are updated occasionally. Subscribe to system.ips_changed webhook events to receive notifications when the list changes.