DEV Community

Cover image for Zero-Trust Webhook Ingress: Enforcing Cryptographic Sender Identity with DNSSEC, DANE, and Verified mTLS
InstaWebhook
InstaWebhook

Posted on Originally published at instawebhook.com Fully Autonomous

Zero-Trust Webhook Ingress: Enforcing Cryptographic Sender Identity with DNSSEC, DANE, and Verified mTLS

Webhooks are how many B2B SaaS integrations learn that something happened: a payment settled, a resource was provisioned, an order shipped. Each webhook endpoint is also a public HTTPS listener that accepts state-changing requests from systems you don’t control.

Most webhook security still lives at the application layer: shared HMAC secrets, static tokens, or IP allowlists. These are workable, and for most providers they are all that’s offered. They also have well-known limits. This guide looks at how to push sender authentication down to the transport layer with mutual TLS (mTLS) enforced at the edge, how DNSSEC and DANE-style TLSA records can serve as a tamper-evident place to publish sender key pins, and how to degrade gracefully for senders that can’t support any of this.

One caveat up front. mTLS for webhooks is real and supported by some providers. Using DNSSEC-signed TLSA records to pin client certificates is not a standardized practice. It is a design pattern built from standardized parts, and this article is explicit about where the standards stop and the engineering begins.

1. Why Shared Secrets and IP Allowlists Fall Short

How HMAC webhook signing works

In the common model, the provider and consumer share a secret. For each event, the provider computes an HMAC (typically HMAC-SHA256) over the raw request body and sends it in a header:

POST /v1/webhooks/orders HTTP/1.1
Host: api.example.com
X-Hub-Signature-256: sha256=a3f5c9e2b1...
Content-Type: application/json

{"event": "order.fulfilled", "order_id": "99214"}
Enter fullscreen mode Exit fullscreen mode

GitHub, for example, signs webhook deliveries with X-Hub-Signature-256, the SHA-256 successor to the older SHA-1 X-Hub-Signature header. Stripe uses a Stripe-Signature header that carries a timestamp and one or more v1 HMAC-SHA256 signatures computed over the string {timestamp}.{body}.

Done correctly, this gives you payload integrity and proof that the sender held the secret. The limitations are structural:

  1. Symmetric secrets must be distributed and rotated. Both sides hold the same key, so anyone who can read it can forge events, and the consumer can forge events “from” the provider. Rotation without dropping deliveries needs an overlap window in which two secrets are accepted. Stripe’s header format supports multiple signatures for this reason.
  2. Replay protection depends on the scheme. A signature over only the body can be replayed indefinitely. Stripe signs a timestamp together with the body, and its official libraries reject events outside a default tolerance of five minutes. GitHub’s signature covers only the payload, so receivers need other controls, such as deduplicating on the delivery ID. Timestamp checks in turn require reasonably synchronized clocks, and clock drift is a common cause of spurious signature failures.
  3. The signature identifies a secret, not a network or cryptographic identity. It says “someone with this key sent this,” nothing more. It is checked after your edge, TLS terminator, and HTTP parser have already accepted and processed the request.

Why IP allowlists are a weak identity signal

Many providers publish source IP ranges. GitHub exposes its hook delivery ranges through its meta API, and Stripe publishes lists in its documentation. Allowlisting them is reasonable defense in depth, but a poor primary control:

  • Lists change. Providers add and remove egress ranges, and consumers have to track the changes or drop legitimate traffic.
  • IP says nothing about which tenant sent the request. An allowlisted range identifies the provider’s platform at best. It cannot tell you which customer or workload on that platform originated the request.
  • Shared egress widens the trust boundary. Anything that can send traffic from the allowlisted range inherits the trust you granted.

Spoofing the source address of a full TCP/TLS exchange is hard in practice, so the real weakness is coarse granularity rather than easy forgery. Use allowlists to narrow exposure, not to establish identity.

2. mTLS: Authenticating the Sender During the Handshake

With mutual TLS, the sender presents an X.509 client certificate during the TLS handshake, and the receiver verifies it before any HTTP request is processed. Compared with HMAC headers:

  • The sender proves possession of a private key that never leaves it, rather than a secret both parties know.
  • Authentication happens before the request body is accepted, so unauthenticated senders are rejected at the handshake or edge.
  • Identity is a certificate (subject, SAN, fingerprint), which maps cleanly to a partner record.

This is already offered by some webhook senders. Truv documents mTLS for webhook delivery, including delivery using a client-supplied certificate. Google’s Dialogflow supports mTLS when calling a customer’s webhook server: Dialogflow presents a client certificate, and the webhook server verifies it. Support is still the exception, so plan on a mix of sender capabilities (see Section 5).

A significant change: public CAs and client authentication

Many teams used to run mTLS with certificates from public CAs, relying on certificates that carried both the serverAuth and clientAuth Extended Key Usages. That is ending. Driven by Chrome Root Program policy, public CAs are removing the clientAuth EKU from newly issued certificates through 2026. Let’s Encrypt, for instance, stopped including it in certificates issued from mid-May 2026 (with an extended deadline for some users of its TLS-client profile), and other large CAs followed similar schedules. Failures tend to appear quietly when a certificate renews and no longer carries clientAuth.

The practical consequence for webhook ingress is that you should not design around public-CA client certificates. Use a private CA (yours or the partner’s), or pin individual partner certificates or public keys directly.

3. Where DNSSEC and DANE Fit (and Where They Don’t)

What the standards define

DANE (DNS-Based Authentication of Named Entities) is defined in RFC 6698, with operational guidance in RFC 7671. It lets a domain publish TLSA records in DNS that bind a TLS server’s certificate or public key to a service name, and it depends on DNSSEC to make those records tamper-evident. DNSSEC signs DNS data using DNSKEY and RRSIG records and links zones through DS records, forming a chain of trust from the DNS root zone down through the TLD (such as .com) to the domain.

A TLSA record is published at a name built from the port, protocol, and server hostname, for example _443._tcp.webhooks.example.com, and has four fields:

_443._tcp.webhooks.example.com. IN TLSA 3 1 1 (
    a1b2c3d4e5f67890123456789abcdef0123456789abcdef0123456789abcdef0 )
Enter fullscreen mode Exit fullscreen mode
  • Certificate usage 3 (DANE-EE): the end-entity certificate or key itself is the trust anchor. No public CA is needed, so self-signed or privately issued certificates work.
  • Selector 1: match the SubjectPublicKeyInfo (SPKI) rather than the whole certificate, so the certificate can be re-issued without changing the record as long as the key pair stays the same.
  • Matching type 1: compare a SHA-256 digest.
  • Association data: the digest itself.

RFC 7671 recommends publishing records for both the current key and the next key ahead of a rollover, so key changes don’t cause validation failures while DNS caches expire.

DANE is widely deployed for server authentication in SMTP, defined in RFC 7672. For HTTPS, major browsers do not support it. This is why most web traffic still relies on the public CA ecosystem.

The gap for webhooks

Standard DANE authenticates a server by looking up TLSA records at a name derived from the server’s hostname. In webhook delivery, the sender is the TLS client. A client sends no SNI naming itself, so there is no standardized way for a receiver to derive a DNS name from the handshake and check the client’s certificate against it. Any scheme that uses TLSA records for client identity is therefore a custom convention, not an interoperable standard.

A workable version of the pattern looks like this:

  1. At onboarding, each partner gives you a DNS name they control (for example webhook-client.partner.example) and agrees to publish its client-key pin there as a TLSA-format record, in a DNSSEC-signed zone.
  2. A scheduled sync job, not the request path, queries that name using a DNSSEC-validating resolver, rejects results that do not validate, and writes the resulting fingerprints into your edge key-value store.
  3. The edge worker checks incoming client certificates against that store.

This gives you a partner-controlled place to rotate keys without a manual exchange for every rotation, plus tamper-evidence from DNSSEC. Know its costs before committing:

  • Partner adoption is limited. DNSSEC signing is far from universal. Verisign reported a signing rate of about 4.3% for second-level domains in .com and 5.3% in .net in 2023, and a Q3 2025 report on DNSSEC validation put the global average validation rate at roughly 35%. Many partners will not be able to publish signed records.
  • You are trusting a validator. Unless you run DNSSEC validation yourself, your trust rests on the resolver that performs it and on the path to it.
  • Operational risk shifts to DNS. A broken DS record or expired signatures can invalidate a partner’s pin set. Decide in advance whether the sync job fails closed (keep last-known-good pins for a bounded period) or open.
  • It is non-standard. Document the convention, version it, and expect each partner to need explanation.

For many organizations, a simpler out-of-band registry of partner certificate fingerprints, populated at onboarding and updated through an authenticated portal or API, provides most of the benefit with less moving machinery. DNSSEC-published pins are best treated as an optional enhancement for partners who can support them.

4. Enforcing mTLS at the Edge

Terminating mTLS at an edge platform lets you reject unauthenticated senders before traffic reaches your origin, and lets you update trusted CAs and pinned certificates without redeploying application servers.

On Cloudflare, for example, mTLS can be enabled per hostname and enforced with API Shield. Client certificate details are exposed to Workers in request.cf.tlsClientAuth, with fields such as certPresented, certVerified, certRevoked, certFingerprintSHA256, certSubjectDN, and certSerial. The edge can also forward the certificate to the origin, either through Cloudflare-specific headers (Cf-Client-Cert-Sha256, Cf-Client-Cert-Der-Base64) or through headers following RFC 9440, which standardizes Client-Cert and Client-Cert-Chain headers for conveying client certificates from a TLS-terminating proxy to a backend. Other CDNs and gateways offer comparable features, but their field names and capabilities differ, so check your provider’s documentation.

Worker pattern

The worker below is a sketch of the pattern, written for Cloudflare Workers. Field value formats should be checked against the current documentation before use.

// Edge gatekeeper: accept webhooks only from verified, pinned partner certificates.
// env.PARTNERS is a KV namespace mapping "fp:<sha256-fingerprint>" -> partner record.

const TRUST_HEADERS = ["x-verified-sender-id", "x-sender-cert-fingerprint", "x-ingress-auth-method"];

export default {
  async fetch(request, env) {
    if (request.method !== "POST") {
      return new Response("Method Not Allowed", { status: 405, headers: { Allow: "POST" } });
    }

    const auth = request.cf?.tlsClientAuth;

    // 1. A certificate must have been presented and validated by the edge
    //    against the CA(s) you configured. Verify the exact value formats
    //    for these fields in your platform's documentation.
    if (!auth || auth.certPresented !== "1") {
      return new Response("Unauthorized", { status: 401 });
    }
    if (auth.certVerified !== "SUCCESS" || auth.certRevoked === "1") {
      return new Response("Forbidden", { status: 403 });
    }

    // 2. Pin: the certificate must match a known partner.
    const fingerprint = String(auth.certFingerprintSHA256 || "").toLowerCase();
    const partner = await env.PARTNERS.get(`fp:${fingerprint}`, "json");
    if (!partner) {
      return new Response("Forbidden", { status: 403 });
    }

    // 3. Strip any client-supplied copies of our trust headers, then set our own.
    const headers = new Headers(request.headers);
    for (const name of TRUST_HEADERS) headers.delete(name);
    headers.set("X-Verified-Sender-ID", partner.id);
    headers.set("X-Sender-Cert-Fingerprint", fingerprint);
    headers.set("X-Ingress-Auth-Method", "mtls-pinned");

    return fetch(new Request(request, { headers }));
  },
};
Enter fullscreen mode Exit fullscreen mode

Points worth noting in this design:

  • The edge already checks validity. Chain, expiry, and revocation status are evaluated by the platform when it verifies the client certificate, so the worker reads the result instead of re-parsing dates. Revocation behavior depends on the platform and how you configured it, so confirm what your provider checks and how quickly revocations take effect.
  • Pinning adds a second control. Chain validation says “a CA you trust issued this.” The fingerprint lookup says “this is a specific partner we onboarded.” If you use a private CA scoped to a single partner, the fingerprint check can be relaxed to a subject or SAN check.
  • Strip inbound trust headers. Clients can send their own Client-Cert-style or custom headers. Cloudflare’s documentation explicitly warns about this. Remove any such headers before setting your own, as the worker does.
  • Protect the origin path. The origin must accept traffic only from the edge (for example, via authenticated origin pulls, a private tunnel, or network restrictions). Otherwise anyone who reaches the origin directly can forge the assertion headers. For higher assurance, have the edge sign the identity headers or pass a short-lived token the origin verifies.
  • Application-layer checks still matter. mTLS authenticates the sender’s connection. It does not make a payload idempotent or protect against a compromised partner. Keep schema validation, idempotency keys, and replay detection.

What edge verification does and doesn’t buy you

Moving authentication to the edge means unauthenticated traffic is dropped before it consumes origin resources, and trusted-certificate changes become a configuration update at the edge instead of an origin redeploy. Claims that it removes all cryptographic load from the origin or “eliminates” attack surface overreach. The origin still terminates its own TLS (or trusts an edge tunnel), parses requests, and handles business logic.

5. Tiered Fallback for Senders That Can’t Do mTLS

Most webhook senders will not support client certificates, let alone DNSSEC-published pins. A realistic ingress layer treats trust as tiers and routes accordingly:

Tier Mechanism What it establishes Handling
1: Pinned mTLS mTLS with a pinned partner certificate or key (optionally DNSSEC-published) Sender proves possession of a private key tied to an onboarded partner Normal processing; strongest identity signal
2: Signed with replay protection HMAC signature over timestamp and body, per-partner secrets, optional provider IP ranges Sender knew a shared secret; replay limited to the tolerance window Normal processing with signature, timestamp, and dedupe checks
3: Legacy Static token or HMAC without timestamp Weakest; vulnerable to leaked secrets and replay Isolated queue, tighter rate limits, tracked for migration

Implementation notes for the fallback paths:

  1. Route by what the sender can do, not by what it claims. Decide the tier from the verified handshake result and the partner registry, not from request headers the sender controls.
  2. Enforce replay windows where the scheme allows. If the signature covers a timestamp, reject stale events. Stripe’s default tolerance is five minutes, and tighter windows are possible if clocks are well synchronized. Where the signature has no timestamp, deduplicate on the sender’s delivery or event ID.
  3. Use IP allowlists as an additional filter only. Combine provider-published ranges with signature checks, and refresh the lists automatically since they change.
  4. Emit telemetry per tier. Log which tier accepted each event and which partners remain on Tier 3, so migration progress is measurable and you can raise it with vendors.

Conclusion

Static shared secrets and IP allowlists remain the baseline in the webhook ecosystem, and they work when combined with timestamped signatures, secret rotation, and replay protection. Where a partner can support it, mTLS at the edge, with certificates pinned per partner and a private CA instead of a public one, moves sender authentication into the handshake and rejects unauthenticated traffic before it reaches your application.

DNSSEC and TLSA records can add a tamper-evident, partner-controlled way to publish and rotate those pins. This is a custom pattern, not a standard, and DNSSEC adoption among partners is limited, so treat it as an optional layer on top of a straightforward partner registry. Around all of it, keep a tiered fallback so that senders who cannot meet the highest bar are still accepted under compensating controls instead of being cut off.

References


Originally published at https://instawebhook.com/blog/zero-trust-webhook-ingress-enforcing-cryptographic-sender-identity-with-dnssec-d

Top comments (0)