Anyone who finds out your webhook's URL can post to it. To be sure a delivery came from DNS Check and wasn't changed along the way, check its signature before acting on it. Deliveries are signed following the Standard Webhooks specification, in every payload format.
The signature covers the request body and two headers: webhook-id (the event ID) and webhook-timestamp (when the attempt was sent). The signature itself arrives in a third header, webhook-signature.
We show your webhook's signing secret, which starts with whsec_, when you edit the webhook on the Webhooks tab. Keep it out of your source code, for example, in an environment variable.
Verify with a Standard Webhooks Library
Standard Webhooks publishes verification libraries for many languages. Pass the library the signing secret exactly as we show it, the request's headers, and the raw request body. Use the body exactly as it arrived: parsing the JSON and serializing it again changes the bytes, and the signature no longer matches.
Node.js, with Express and the standardwebhooks package:
import express from "express";
import { Webhook } from "standardwebhooks";
const app = express();
const webhook = new Webhook(process.env.DNSCHECK_SIGNING_SECRET);
// The signature covers the body exactly as sent, so read it raw
app.post("/dnscheck/webhook", express.raw({ type: "*/*" }), (req, res) => {
let event;
try {
event = webhook.verify(req.body, req.headers);
} catch (err) {
return res.status(400).send("Invalid signature");
}
console.log(event.type, event.text);
res.sendStatus(204);
});
app.listen(3000);
Python, with Flask and the standardwebhooks package:
import os
from flask import Flask, request
from standardwebhooks import Webhook, WebhookVerificationError
app = Flask(__name__)
webhook = Webhook(os.environ["DNSCHECK_SIGNING_SECRET"])
@app.post("/dnscheck/webhook")
def dnscheck_webhook():
try:
event = webhook.verify(request.get_data(), request.headers)
except WebhookVerificationError:
return "Invalid signature", 400
print(event["type"], event["text"])
return "", 204
Both libraries parse the body as JSON after verifying it. For the Plain text format, turn that off with webhook.verify(req.body, req.headers, { jsonParse: false }) in Node.js or webhook.verify(request.get_data(), request.headers, json_parse=False) in Python, then read the message from the raw body.
Verify without a Library
To verify a signature yourself:
- Build the signed content by joining the
webhook-idheader, thewebhook-timestampheader, and the raw request body with periods:<webhook-id>.<webhook-timestamp>.<body>. - Build the key: remove the
whsec_prefix from the signing secret, and base64-decode the rest. Using the whole secret as the key fails every signature. - Compute the HMAC-SHA256 of the signed content with that key, base64-encode it, and put
v1,in front. - Compare the result to each space-separated value in the
webhook-signatureheader, using a constant-time comparison. The delivery is valid if any of them match. - Reject the delivery if
webhook-timestampis more than five minutes from your server's current time, so a captured request can't be replayed later. Every retry is signed with a fresh timestamp, so retries still pass this check.
In Python, using only the standard library:
import base64
import hashlib
import hmac
import time
TOLERANCE_SECONDS = 5 * 60
def valid_signature(secret, headers, body):
"""headers: the request's headers; body: the raw request body, as bytes."""
event_id = headers.get("webhook-id")
timestamp = headers.get("webhook-timestamp")
signatures = headers.get("webhook-signature")
if not (event_id and timestamp and signatures):
return False
if abs(time.time() - int(timestamp)) > TOLERANCE_SECONDS:
return False
key = base64.b64decode(secret.removeprefix("whsec_"))
signed_content = f"{event_id}.{timestamp}.".encode() + body
digest = hmac.new(key, signed_content, hashlib.sha256).digest()
expected = "v1," + base64.b64encode(digest).decode()
return any(hmac.compare_digest(signature, expected)
for signature in signatures.split(" "))
Header names are case-insensitive, so look them up the way your framework does. Flask's and Django's request.headers both accept any capitalization.
Roll the Signing Secret
To replace a webhook's signing secret, click the webhook's drop-down menu on the Webhooks tab, select Roll secret, and confirm. We show you the new secret and keep signing with the old one for 24 hours. Until then, each delivery's webhook-signature header carries two signatures, one made with each secret. Standard Webhooks libraries, and the example above, accept a delivery when any signature matches, so your endpoint keeps verifying deliveries while you update it. Update your endpoint with the new secret before the old one expires; editing the webhook shows exactly when that is.
If a secret is exposed, roll it and update your endpoint right away. Your endpoint decides which secrets it trusts, so once it has only the new secret, a request signed with just the old one fails verification, even while we're still adding an old-secret signature to our deliveries.
Each attempt is signed when it's sent, so retries of earlier deliveries use the new secret too. We sign with up to two secrets at a time: the current one, and the previous one if it was replaced within the last 24 hours. So rolling again within that window stops signing with the older of the two right away.
Protect your DNS infrastructure with automated monitoring
Get notified immediately when DNS records change. Start monitoring your critical DNS infrastructure for free in under 5 minutes.
No credit card required • Cancel anytime