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
- 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 verifycall. 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. - 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
- No PTR, or PTR doesn't resolve forward — treat as unverified, never as "probably fine." An address with no confirmed name gets no more trust than an anonymous connection.
- Missing
ADbit — without DNSSEC validation on both legs, FCrDNS defeats casual PTR forgery but not on-path spoofing (see Reverse DNS & FCrDNS for the full threat model). Decide up front whether your risk tier requiresAD=1. - Overbroad wildcards —
*.agents.whisper.onlineauthorizes any verified Whisper agent, not agents you provisioned. Reserve it for services that genuinely want "any real agent, whoever owns it" (e.g., a public API with per-request billing); scope to your tenant subdomain otherwise. - TTL and negative caching — cache confirmed names for their record TTL, and respect negative-cache TTLs (RFC 2308) on failed lookups so a transient resolver hiccup doesn't get treated as a permanent identity.
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