Skip to the content.

Deploying apd

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.

Trust & TLS

AAuth identities are HTTPS URLs and every token is verified by fetching {issuer}/.well-known/aauth-agent.jsonjwks_uri over TLS. Therefore:

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".

Single instance

{
  "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.json

Multi-instance (horizontal scale)

Because 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:

  1. 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.
  2. 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_uses counts 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:

Key management

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 old

Rotation 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.

systemd

[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.)

Container

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.

Operational checklist