apd is a single static binary. The two things that make it production-ready
are TLS (AAuth requires HTTPS server identifiers) and, for more than one
instance, shared state + shared signing keys.
AAuth identities are HTTPS URLs and every token is verified by fetching
{issuer}/.well-known/aauth-agent.json → jwks_uri over TLS. Therefore:
issuerMUST be the realhttps://origin relying parties will reach.apditself speaks plain HTTP; terminate TLS in front of it (reverse proxy, load balancer, or service mesh) and forward tolisten.- The proxy MUST preserve the
Hostheader and the request path/method — the HTTP Message Signature covers@authorityand@path, so a proxy that rewrites the Host or path will break verification. Do not let the proxy strip or reorder theSignature,Signature-Input, orSignature-Keyheaders. - Do not enable
insecure_dev_modein production.
Example nginx front:
server {
listen 443 ssl;
server_name ap.example.com;
ssl_certificate /etc/ssl/ap.example.com.crt;
ssl_certificate_key /etc/ssl/ap.example.com.key;
location / {
proxy_pass http://127.0.0.1:8420;
proxy_set_header Host $host; # signature covers @authority
proxy_set_header X-Forwarded-For $remote_addr;
proxy_read_timeout 75s; # allow /inbox long-poll
}
}With issuer = "https://ap.example.com" and listen = "127.0.0.1:8420".
{
"issuer": "https://ap.example.com",
"listen": "127.0.0.1:8420",
"keys_file": "/var/lib/apd/apd-keys.json",
"storage": { "backend": "file", "path": "/var/lib/apd/state.json" },
"enrollment": { "mode": "token" },
"events": { "enabled": true }
}file storage is crash-safe (atomic snapshot per mutation). Put the admin token
in the environment, not the file:
APD_ADMIN_TOKEN="$(openssl rand -hex 32)" apd serve --config /etc/apd/apd.jsonBecause verification is stateless, you scale read/verify load simply by adding instances behind the load balancer — relying parties only ever hit the well-known + JWKS endpoints, which serve pre-serialized bytes with cache headers. The instances must share two things:
- The same signing keys. Mount the identical
keys_file(or the same secret) on every instance. A token signed by instance A is verified by anyone using the JWKS all instances publish. - Shared state via Redis. Enrollments, single-use enrollment tokens,
naming-JWT replay guards, event subscriptions/counters, and inboxes live in
Redis so any instance can serve any agent. All the mutating operations are
atomic Redis primitives, so concurrent instances stay correct (single-use
tokens are consumed exactly once,
max_usescounts exactly, etc.).
{
"issuer": "https://ap.example.com",
"listen": "0.0.0.0:8420",
"keys_file": "/etc/apd/apd-keys.json",
"storage": { "backend": "redis", "redis_addr": "redis.internal:6379", "key_prefix": "apd:" },
"enrollment": { "mode": "token" },
"events": { "enabled": true }
}Notes:
- The built-in Redis client is plain TCP RESP2. Keep Redis on a private network or tunnel it (stunnel / mesh mTLS). Requires Redis ≥ 6.2.
- Clocks must be NTP-synced across instances and agents — the signature
createdwindow (default 60 s) depends on it. /inboxlong-poll holds a connection up to 50 s; size your proxy read timeout and per-instance connection limits accordingly, or have agents poll with shorterPrefer: wait.
apd keygen --keys apd-keys.json # create
apd keygen --keys apd-keys.json --rotate # add a new active key
apd keygen --keys apd-keys.json --prune-days 2 # drop retired keys >2d oldRotation is safe and online: --rotate appends a new key, marks it active, and
keeps the previous public keys in the JWKS so tokens already signed with them
still verify. Distribute the updated keys_file to all instances (rolling
restart, or a shared mount), then, after the longest token lifetime has elapsed
(≤ agent_token_ttl_secs), prune the retired keys. The keys_file is a secret —
mode 0600 (the tool sets this), backed up, never committed.
Revoking a single agent is POST /admin/agents/{local}/revoke: the AP refuses to
issue that agent new tokens; its current token ages out within its (≤1 h)
lifetime. This is the AP's revocation lever — short token lifetimes make every
refusal effective quickly without any cross-party coordination.
[Unit]
Description=apd (AAuth Agent Provider)
After=network-online.target redis.service
Wants=network-online.target
[Service]
ExecStart=/usr/local/bin/apd serve --config /etc/apd/apd.json
Environment=APD_ADMIN_TOKEN=%I
DynamicUser=yes
StateDirectory=apd
AmbientCapabilities=
NoNewPrivileges=yes
ProtectSystem=strict
ProtectHome=yes
Restart=on-failure
[Install]
WantedBy=multi-user.target(Point keys_file/storage.path at /var/lib/apd = the StateDirectory.)
apd is a static-ish binary; a distroless or scratch+glibc image works. Build
--release, copy the binary, ship the config via a mounted file + env secrets,
expose the listen port, and terminate TLS at the ingress. Health-check
GET /healthz.
-
issueris the real HTTPS origin; TLS terminates in front; Host/path preserved. -
keys_fileis identical on all instances,0600, backed up, not in VCS. -
admin_tokenand Redis address come from the environment/secret store. - Multi-instance ⇒
redisbackend; Redis is private/tunneled and ≥6.2. - NTP on all hosts.
-
insecure_dev_modeis off. - Monitoring hits
/healthz; alert on non-200. - A key-rotation runbook exists (rotate → distribute → wait ≥ token TTL → prune).