Install & deploy
for operators
psd is one static binary, one keys file and one SQLite database. This page takes you from nothing to a Person Server that agents can find and people can log in to, then covers day-two operations. Every configuration field is in the configuration reference; every command in the CLI reference.
Before you start: three decisions that are hard to undo
The issuer is permanent. issuer (for example https://ps.example.com)
goes into every pairwise sub psd derives and into iss of every token it
signs. Change it and every service sees a stranger and every agent must be
approved again. It must be an https:// origin — lowercase, no port, no
path — and this exact origin must serve /.well-known/aauth-person.json.
It must be a hostname, not an address. People log in with passkeys, and
browsers refuse to create passkeys on IP-address origins. In development
that means http://localhost:8430, never http://127.0.0.1:8430.
The keys file is the identity, and the pairwise secret inside it must
never change. psd keygen writes the Ed25519 signing keys and the
pairwise_secret used to derive per-service identifiers to one file. Losing
the file means every service sees new strangers; leaking it means someone
else can sign as your server. Back it up like a private key, because it is
one.
Get the binary
Pick one.
From source (Rust 1.85+; SQLite is compiled in, so a C compiler is the only system dependency):
git clone https://github.com/PersonServer/source-code psd && cd psd
cargo build --release
./target/release/psd version
Container image, multi-arch (amd64, arm64), distroless, non-root:
docker pull ghcr.io/personserver/psd:latest # or :edge for the latest main
Helm chart (OCI), single replica with a persistent volume:
helm install psd oci://ghcr.io/personserver/charts/psd \
--set issuer=https://ps.example.com --set keys.existingSecret=psd-keys
The chart’s README covers its values; the rest of this page applies to it as much as to a bare binary.
1 · Create the keys
psd keygen --keys /var/lib/psd/psd-keys.json
# created /var/lib/psd/psd-keys.json with new active key 'ps-…' and a pairwise secret
The file is written 0600. Its shape is a JSON object with the active
kid, the private keys, and the pairwise secret; there is nothing in it you
ever paste anywhere else. Rotation is covered below.
2 · Write the config
psd example-config > /etc/psd/psd.json
Edit at least these:
{
"issuer": "https://ps.example.com",
"listen": "127.0.0.1:8430",
"keys_file": "/var/lib/psd/psd-keys.json",
"storage": { "backend": "sqlite", "path": "/var/lib/psd/psd.db" },
"metadata": {
"name": "Example Person Server",
"description": "Manage which agents act for you and review what they do.",
"documentation_uri": "https://personserver.dev/docs/"
}
}
Unknown fields are hard errors and every value is validated at load, with a
message that says what to change — a typo will not become a silently
ignored setting. metadata.name is what people see on every screen; set it.
Everything else has a sensible default; see the
reference when you want to change one.
Secrets and hostnames can be injected by the environment instead of edited
into the file: PSD_ISSUER, PSD_LISTEN, PSD_KEYS_FILE, PSD_DB_PATH
win over the file.
3 · Put TLS in front
psd speaks plain HTTP on listen; terminate TLS in a reverse proxy or
ingress. Three rules, each with a reason:
- Preserve the
Hostheader. Agents sign@authorityon every request and psd checks it against the issuer host before doing anything else. A proxy that rewritesHostto127.0.0.1:8430breaks every signed request. If you truly cannot preserve it, setexpected_authorityto what the proxy sends — but preserving it is the fix. - No path rewriting.
/.well-known/aauth-person.json,/person,/tokenand the rest must be reachable at exactly those paths on the issuer origin. Serve psd at the root of its own hostname. - Pass bodies through unchanged. Signed requests cover a
Content-Digestof the body; a proxy that re-encodes JSON will break the signature. Normal proxies do not.
A minimal nginx site:
server {
listen 443 ssl http2;
server_name ps.example.com;
# ssl_certificate …; ssl_certificate_key …;
location / {
proxy_pass http://127.0.0.1:8430;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Proto https;
proxy_http_version 1.1;
}
}
Caddy does the right thing with reverse_proxy 127.0.0.1:8430. On
Kubernetes, the chart’s Ingress preserves Host with every mainstream
controller’s defaults.
4 · Serve
psd serve --config /etc/psd/psd.json
Startup prints what it is doing — issuer, listen address, storage, whether passkeys are available (they are not on an IP-address issuer), whether templates are overridden, which drafts it tracks — and then serves. Check from outside:
curl -s https://ps.example.com/healthz
curl -s https://ps.example.com/.well-known/aauth-person.json
curl -s https://ps.example.com/.well-known/jwks.json
The metadata document is what agents (and Agent Providers, and services) discover; if it does not load at the issuer origin, nothing else will work.
A systemd unit that matches the layout above:
[Unit]
Description=psd — AAuth Person Server
After=network-online.target
Wants=network-online.target
[Service]
User=psd
Group=psd
ExecStart=/usr/local/bin/psd serve --config /etc/psd/psd.json
WorkingDirectory=/var/lib/psd
Restart=on-failure
NoNewPrivileges=true
ProtectSystem=strict
ProtectHome=true
ReadWritePaths=/var/lib/psd
PrivateTmp=true
[Install]
WantedBy=multi-user.target
CLI commands take --config; relative paths in the config resolve against
the current directory, so run them from WorkingDirectory (or use absolute
paths, as the examples here do).
5 · Add the first person
psd person add --name "Alice" --config /etc/psd/psd.json
# https://ps.example.com/enrol/…
# created person … (Alice); open the link above within 900s to register a passkey (single use)
The link goes to stdout and the explanation to stderr, so it scripts
cleanly. --ttl changes the 15-minute default.
Send the link to Alice (or open it yourself). It shows “Welcome, Alice”
and a Create passkey button; after that she is enrolled and on her
dashboard. psd invite --person ID mints another link later — for a person
who lost every passkey, for instance. psd person list shows who exists.
That is a working Person Server. Point an agent at it — the quickest real
one is an agent enrolled at
agentprovider.dev’s sandbox;
apd’s tools/aauthcheck --target https://ps.example.com --poll walks the
whole deferred flow — approve the request on Alice’s dashboard, and the
agent receives its person token.
Day two
Headless approvals
When nobody is watching a browser, decisions can be made from the shell:
psd pending list --config /etc/psd/psd.json
psd pending approve <id> --config /etc/psd/psd.json # --person ID if unbound
psd pending deny <id> --config /etc/psd/psd.json
They are recorded in the audit log with "via": "cli", so the record shows
the operator decided, not the person’s browser. psd agents list and
psd agents revoke ISS SUB do the same for bindings; a CLI revocation
revokes the agent’s consents and sweeps its auth tokens at their services
just as the dashboard button does.
Notifications
By default a pending request simply waits on the dashboard. Add
"webhook" to notify.channels and set an https:// notify.webhook_url
to receive a JSON POST when a decision is pending — enough to page someone
or forward to chat. The webhook never carries the interaction code.
The record
Every issuance, denial, revocation, binding and mission event is one JSON
line on stderr, and the same event as a row the person sees under
Activity. Set audit_log_file to also append the lines to a file. Two
events deserve an alert if you have one: resource_token_mismatch (a
service presented a resource token that names a person token psd issued —
but for a different agent, person or mission; the service is confused or
hostile) and repeated person_token_denied with reason: too_many_resources
(an agent probing many services). A third, discovery_unavailable, means
psd could not fetch an issuer’s metadata or JWKS and answered the agent
503: usually your egress (DNS, firewall, a proxy that rewrites responses),
sometimes their outage — either way the agent is not at fault, and the
event names the issuer and the reason.
Backups
Two files: the keys file and the SQLite database (psd.db, plus its -wal
and -shm while running). The database holds person records, passkey
public keys, bindings, consent history and the directed identifiers — it is
personal data; treat backups accordingly. psd runs SQLite in WAL mode, so
copying the file while serving is safe as long as you copy psd.db and
psd.db-wal together (or use sqlite3 psd.db ".backup out.db").
Key rotation
psd keygen --keys /var/lib/psd/psd-keys.json --rotate # new active key
psd keygen --keys /var/lib/psd/psd-keys.json --prune-days 30 # drop old ones
Rotation adds a new active key; old public keys stay in the JWKS until pruned, so tokens signed before rotation (they live at most an hour) keep verifying. psd re-reads the file at start — restart after rotating. The pairwise secret is untouched by rotation and must stay so.
Upgrades
Stop, replace the binary (or image tag), start. The schema is created and extended at startup. Take a database backup before upgrading; AAuth is a draft, so pin a version, read the release notes, and expect wire changes between draft revisions.
Development mode
For local work set "insecure_dev_mode": true and an http://host:port
issuer such as http://localhost:8430. That relaxes the identifier rules,
admits loopback and plain-HTTP egress so a mock Agent Provider on
127.0.0.1 works, and permits a non-Secure session cookie. serve prints
a warning while it is on. Never enable it where anyone else can reach the
server.
Optional surfaces
missions.enabled advertises mission_endpoint and turns on the mission
flow; federation.enabled turns on four-party federation to an Access
Server and call chaining. Both default to off. Federation is exercised
against a mock Access Server only — no live one exists yet — so enable it
knowingly.
What psd expects from you
- A stable hostname with TLS in front,
Hostpreserved. - The keys file kept secret and backed up; the database backed up.
- Someone who reads the audit line about
resource_token_mismatch. - A version pin and a glance at release notes: the protocol is a draft.