Whisper · Docs
Verify & authorize

Access control

An inbound agent connection is just an address until you decide what that address is allowed to do — check who it really is, then authorize by name, not by a bearer token someone could copy.

Every gateway that accepts agent traffic ends up bolting on the same brittle thing: a shared secret in a header, an API key in a query string, an allowlist of IPs that changes every time infrastructure moves. Headers get logged, keys get pasted into the wrong Slack channel, IP allowlists rot the day a NAT gateway rotates. None of that is identity — it's a bearer credential wearing identity's clothes, and anyone who steals the bytes gets the access. Whisper agents don't need any of it, because the source address on the wire already resolves to a name your own DNS resolver can independently confirm — no header to forge, no token to leak, nothing to steal but a network path the agent's operator actually controls. This page is the two-step mechanism — confirm, then authorize — and how to wire it into the reverse proxy, the app layer, or mTLS you already run.

The two-step model

  1. Confirm. Take the source address of the inbound connection and resolve it to a name you can trust — forward-confirmed reverse DNS (FCrDNS), optionally DNSSEC-validated, or a single whisper verify call. This is the full mechanism, covered in depth in Reverse DNS & FCrDNS and Verify an agent; this page assumes you have a confirmed name in hand and picks up from there.
  2. Authorize. Match the confirmed name against a policy at whichever granularity your service needs — one agent, one tenant's whole fleet, or any verified Whisper agent on the internet — and allow or deny.

The property that makes step 2 trustworthy is entirely earned in step 1: because the name came from a chain the caller doesn't control (reverse zone delegated to AS219419, forward zone delegated to the agent's own tenant, both DNSSEC-signed), matching on it is equivalent to matching on cryptographically-attested network control — not on a string the client sent you.

Step 1, recap: getting a confirmed name

With stock tools

SRC="2a04:2a01:eb5a:ca74:cef2:2a:323d:40d4"

name=$(dig +short -x "$SRC")                     # reverse: PTR lookup
dig +short AAAA "$name" | grep -qx "$SRC" \
  && echo "FCrDNS OK: $name" || echo "FCrDNS FAIL"
# FCrDNS OK: acef2002a323d40d4.<tenant>.agents.whisper.online.

For a spoof-proof version, require the AD (Authentic Data) bit on both legs — proof that DNSSEC (RFC 4033–4035) validated the whole chain up to the root, not just that an answer came back:

kdig +dnssec -x "$SRC"       | grep -q 'flags:.* ad' && \
kdig +dnssec AAAA "$name"    | grep -q 'flags:.* ad' && \
echo "spoof-proof match"

With Whisper

whisper verify "$SRC"
# PASS  acef2002a323d40d4.<tenant>.agents.whisper.online.  (AD=1 both legs)

curl -s https://rdap.whisper.online/verify-identity/"$SRC" \
  | jq -r 'select(.is_whisper_agent) | .fqdn'
# acef2002a323d40d4.<tenant>.agents.whisper.online.

Both endpoints are keyless — no whisper_live_… key required to verify a caller, per Whisper's two-tier design (verification is a public good; provisioning and egress are gated). For a version that doesn't even ask you to trust Whisper's own verdict, whisper verify --trustless walks the DNSSEC chain, DANE TLSA record, and a signed transparency-log entry itself — see Verify an agent.

Step 2: authorize on the confirmed name

Once you have a name, you can grant access at three levels of granularity, from tightest to loosest:

Match Pattern (example) Grants
Exact agent acef2002a323d40d4.<tenant>.agents.whisper.online one specific agent, and only that one
Tenant subdomain *.<tenant>.agents.whisper.online every agent belonging to one customer/organization
Wildcard *.agents.whisper.online any agent verified as a genuine Whisper identity, regardless of owner

The name is a normal DNS label under a normal delegated zone (agents.whisper.online, tenant subdomain, agent leaf), so any glob/wildcard matcher you already have works unmodified — fnmatch in Python, map in nginx, a Host-style rule in Envoy. There is no bespoke Whisper matching syntax to learn.

With stock tools — a Python allowlist

import fnmatch

ALLOW = [
    "acef2002a323d40d4.<tenant>.agents.whisper.online",  # one agent
    "*.<tenant>.agents.whisper.online",                   # or: whole tenant
]

def authorized(confirmed_name: str) -> bool:
    name = confirmed_name.rstrip(".")
    return any(fnmatch.fnmatch(name, pattern) for pattern in ALLOW)

With Whisper — same allowlist, driven by the control plane

If the allowlist itself should live where you already manage the fleet rather than in application config, list the tenant's live agents via the control verb and derive the pattern instead of hand-maintaining it:

curl -s https://graph.whisper.security/api/query \
  -H "X-API-Key: whisper_live_xxx" \
  -H 'content-type: application/json' \
  -d '{"query":"CALL whisper.agents({op:\"list\", args:{}})"}' \
  | jq -r '.rows[0].result.rows[].fqdn'
# acef2002a323d40d4.<tenant>.agents.whisper.online
# ...one line per provisioned agent in your tenant

Feed that list straight into the ALLOW array above, or into an nginx map file regenerated on a cron — either way the source of truth is the same registry that provisioned the agents, not a copy that drifts.

Step 3: wire it into the edge

nginx — resolve-and-compare at the edge with js_content (or an auth_request subrequest to a tiny sidecar running the Python above), then gate on the result:

map $ssl_client_s_dn $whisper_confirmed_name { default ""; }  # or set by auth_request

location /webhook {
    auth_request /_whisper_verify;
    if ($whisper_confirmed_name !~ "\.acmecorp\.agents\.whisper\.online$") {
        return 403;
    }
    proxy_pass http://backend;
}

Envoy — an ext_authz HTTP filter calling the same verify endpoint, denying on anything but a 200 with a matching fqdn header; the pattern is identical to any other external-authorization integration Envoy already supports.

App middleware — verify once per connection (not per request) and cache the confirmed name for the connection's lifetime; a TCP or TLS handshake is a natural place to do the reverse-DNS round trip once instead of on every HTTP call.

mTLS — pair this with DANE: the agent's TLSA record (3 1 1 b653a4ef…fcb82d1d, RFC 6698) pins the same certificate at the same name FCrDNS just confirmed, so a client certificate check and a DNS name check are proving the same identity two independent ways — defense in depth against either chain being compromised in isolation.

Failure modes to handle explicitly

Access control built this way has no secret to leak. An attacker who steals your config gets the policy — the list of names you trust — not a credential that lets them impersonate one.

Next: Verify an agent — the full trust chain behind a confirmed name · Reverse DNS & FCrDNS — how PTR and forward records are made to agree