# 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 bolts 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 need none of it: 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 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 the 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](/docs/reverse-fcrdns) and [Verify an agent](/docs/verify); 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

```bash
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:

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

### With Whisper

```bash
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](/docs/verify).

## 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

```python
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:

```bash
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:

```nginx
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](/docs/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 `AD` bit** — without DNSSEC validation on both legs, FCrDNS defeats casual PTR forgery but not on-path spoofing (see [Reverse DNS & FCrDNS](/docs/reverse-fcrdns) for the full threat model). Decide up front whether your risk tier requires `AD=1`.
- **Overbroad wildcards** — `*.agents.whisper.online` authorizes *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](/docs/verify) — the full trust chain behind a confirmed name · [Reverse DNS & FCrDNS](/docs/reverse-fcrdns) — how PTR and forward records are made to agree
