Agent-to-server auth
Every agent that SSHes into a box or calls an internal API gets a badge that never expires — a key in authorized_keys, or an API key in an .env, that grants access to whoever holds the string, forever.
An agent with a real, DNSSEC-signed, revocable identity needs neither: it authenticates to your infrastructure with the same address that names it.
The shared-secret problem, for agents specifically
A human operator who leaks an SSH key or an API token notices eventually — a password manager flags it, a security review catches the stale entry. An autonomous agent that leaks one doesn't notice at all, and the credential keeps working long after the task that needed it ended. Worse, most agent tooling makes the problem worse by default: scripts that shell out to ssh routinely set StrictHostKeyChecking no because nobody wants an interactive yes/no prompt blocking a non-interactive run, which means the agent will happily hand its credentials to anything answering on that port and hostname — a textbook MITM opening. And on the API side, a bearer token has no concept of "this specific caller" — it's a string that is either present or absent, full stop.
Whisper's identity fixes both halves at once, using standards that already exist:
- SSHFP (RFC 4255) pins your server's host key in DNSSEC-signed DNS, so the agent's SSH client verifies it's landing on the right box cryptographically, with no TOFU prompt to skip.
- The agent's own address — a real, routable, DNSSEC-backed
/128under2a04:2a01::/32(AS219419) — becomes the thing you authorize against, using OpenSSH's ownfrom=restriction, so a leaked key alone isn't enough. - DANE-pinned mTLS (RFC 6698 + RFC 7671) lets your API validate the agent's own certificate against its public
TLSArecord instead of a shared allowlist.
None of this requires a vault, a CA, or Whisper's API being reachable at the moment your server makes the call — it's DNS and TLS, checked by tools you already have.
Part 1 — the agent trusts your box: SSHFP
SSHFP publishes the fingerprint of a host's SSH key at that host's own DNS name — two small integers (key algorithm, hash algorithm) plus the hash itself, three fields per RFC 4255 §3.1. A DNSSEC-validating SSH client fetches it, confirms the AD bit, and skips the fingerprint prompt entirely because it already knows the answer is authentic.
With stock tools — generate the record from your own host key, publish it, and connect without a TOFU prompt:
# On the server: derive the SSHFP record from the host's own key
ssh-keygen -r git.example.com -f /etc/ssh/ssh_host_ed25519_key.pub
# git.example.com IN SSHFP 4 2 9f86d0818... (4 = Ed25519, 2 = SHA-256)
# Publish that record in your DNSSEC-signed zone, then any client can check it:
dig +dnssec SSHFP git.example.com
# git.example.com. 300 IN SSHFP 4 2 9f86d0818...
# git.example.com. 300 IN RRSIG SSHFP 13 2 300 ... ← AD bit set = authentic
# The agent's ssh client verifies the host key against DNS instead of asking "yes/no":
ssh -o VerifyHostKeyDNS=yes git@git.example.com
VerifyHostKeyDNS=yes only trusts the record when the resolver returns AD (DNSSEC-authenticated); use ask if you want a fallback prompt rather than a hard failure, and check ssh -V — some OpenSSH builds ship without DNSSEC-aware resolver support compiled in.
With Whisper — the same host-key story, but the client side is already DNSSEC-native: an agent spawned with whisper init claude or run under whisper run resolves through Whisper's DNSSEC-validating resolver, which sets the AD bit on answers it has authenticated — so all the agent needs is VerifyHostKeyDNS=yes, with no separate unbound/bind validator to stand up just for this one check.
Part 2 — your box trusts the agent: bind the key to the address
SSHFP authenticates your server to the agent. The other direction — proving which agent is connecting — doesn't need a new mechanism, just OpenSSH's existing from= authorized-keys restriction, pinned to the one address that only that agent can source packets from:
With stock tools:
# authorized_keys entry, restricted to the agent's own /128 —
# a copied private key alone is useless from any other source address
from="2a04:2a01:eb5a:ca74:cef2:2a:323d:40d4",restrict,command="/opt/deploy/run.sh" \
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAA... shipping-bot@agents.whisper.online
# verify the agent really does source from that address before you trust the binding
dig +short -x 2a04:2a01:eb5a:ca74:cef2:2a:323d:40d4
# acef2002a323d40d4.demo.agents.whisper.online.
With Whisper — read the exact address to bind straight from the agent's own identity record, instead of copy-pasting it out of a chat log:
whisper agent shipping-bot # the agent's identity record + live counters
# FIELD VALUE
# agent shipping-bot
# address 2a04:2a01:eb5a:ca74:cef2:2a:323d:40d4
# fqdn acef2002a323d40d4.demo.agents.whisper.online
The binding is only as good as the source address being real — every packet the agent sends, whether over a routed WireGuard tunnel (Tier 1) or the SOCKS5/HTTP egress proxy (Tier 1.5), is source-bound to that exact /128 via AnyIP + IP_FREEBIND (see Connect & egress), so from= is checking a fact about the network path, not a header the agent could forge. And because CALL whisper.agents({op:'revoke', ...}) tears the /128 down along with the rest of the identity, a stale authorized_keys line left behind after an agent is decommissioned stops being reachable at the network layer — you don't have to remember to go clean it up.
Part 3 — the agent authenticates to your API: DANE-pinned mTLS
An agent's per-agent certificate is pinned by a public TLSA record at its own name — DANE-EE, usage/selector/matching 3 1 1, RFC 6698. That pin isn't only useful for validating the agent as a TLS server; RFC 6698 doesn't care which side of the handshake presents the certificate, so the identical check works when your API asks the agent to present a client certificate for mutual TLS. You get the equivalent of an API-key allowlist with no keys to store, rotate, or leak.
With stock tools — request the client cert, then verify it against the live DNS pin instead of a CA bundle:
# The agent presents its own per-agent cert as a TLS client cert:
openssl s_client -connect api.example.com:443 \
-cert agent-client.pem -key agent-client-key.pem </dev/null 2>/dev/null
# Your API's auth check — three commands, no CA, no stored allowlist:
CERT_HASH=$(echo | openssl s_client -connect api.example.com:443 \
-cert agent-client.pem -key agent-client-key.pem 2>/dev/null \
| openssl x509 -pubkey -noout | openssl pkey -pubin -outform der \
| openssl dgst -sha256 -r | cut -d' ' -f1)
TLSA_HASH=$(dig +short TLSA _443._tcp.acef2002a323d40d4.demo.agents.whisper.online \
| awk '{print $4}')
[ "$CERT_HASH" = "$TLSA_HASH" ] && echo "authenticated: acef2002a323d40d4" || echo "reject"
That comparison is the entire trust decision — swap in nginx's ssl_verify_client optional_no_ca; (or Envoy's client-cert validation context) to require the cert at the TLS layer, then run this check as the request-auth step instead of a database lookup.
With Whisper — the CLI already re-derives this exact chain, so a request-auth hook can shell out to it (or vendor the same three steps) instead of hand-rolling the pipeline:
whisper verify --trustless acef2002a323d40d4.demo.agents.whisper.online
dane pass DNSSEC-root served leaf SPKI-SHA256 == TLSA pin
| Mechanism | Direction | Binds to | RFC |
|---|---|---|---|
| SSHFP | agent verifies your server | your host's SSH key | 4255 |
authorized_keys from= |
your server verifies the agent | the agent's routable /128 |
OpenSSH (de facto standard) |
| DANE-pinned mTLS | your server verifies the agent | the agent's per-agent cert, via TLSA |
6698 / 7671 |
Next
DANE & DNSSEC — the TLSA mechanics both mTLS checks above rely on · mTLS & DPoP — how the same pin authenticates an agent to Whisper's own egress gateway