Start the vault server with Docker, choose open or authenticated mode, and operate the data volume.
docker run -d --name vault \
-p 8080:8080 \
-v vault-data:/data \
registry.agentics.dk/agentics/pks-agent-vault:latest
GET /api/health answers {"status":"ok","mode":"open"} once it is up.
The image also mirrors to ghcr.io/pksorensen/pks-agent-vault. Both registries
resolve to the same digest.
There is no database. /data holds everything, and everything in it is either
ciphertext the server cannot read or metadata it can:
owners/<owner>/owner.json
owners/<owner>/index.json rebuildable cache
owners/<owner>/audit/audit.jsonl hash-chained, append-only
owners/<owner>/vaults/<vaultId>/vault.json
owners/<owner>/vaults/<vaultId>/items/<itemId>/item.json
owners/<owner>/vaults/<vaultId>/items/<itemId>/sealed.json opaque, byte-for-byte
Back it up like any other volume. A backup contains no plaintext secrets, but it does contain item names, which are stored in the clear — an attacker who takes a backup learns what the secrets are called, not what they are. If that matters for a particular item, put a placeholder in the name and carry the real one inside the sealed blob.
Only one process may hold the volume: the server takes an exclusive flock on
start. Two servers sharing a volume would each keep their own audit chain head
and produce two divergent chains, so the second one refuses to start rather than
silently forking the log.
With OIDC_ISSUER unset the server runs open: no authentication, and the
local principal is the administrator. That is a trusted-network and development
affordance only. Do not expose an open-mode vault.
Setting OIDC_ISSUER turns on the OIDC port. One rule differs deliberately from
the rest of the Agentics family, and it will stop your first deploy if you do not
know about it:
With
OIDC_ISSUERset andVAULT_ADMIN_SUBSempty, the process refuses to start.
Agent Inbox and Agent Consent read an empty admin list as "every authenticated caller is an admin". For those services that is defensible. Here it would mean that on a self-signup realm, anyone who registers can read every tenant's topology, mint invites on your domain, and purge vaults. So the vault fails closed. List the subjects explicitly:
docker run -d --name vault \
-p 8080:8080 -v vault-data:/data \
-e OIDC_ISSUER=https://keycloak.example.com/realms/agentics \
-e OIDC_CLIENT_ID=agentics-vault \
-e OIDC_CLIENT_SECRET=... \
-e PUBLIC_BASE_URL=https://vault.example.com \
-e VAULT_ADMIN_SUBS=8f2b...,c41d... \
registry.agentics.dk/agentics/pks-agent-vault:latest
The mirror-image mistake is refused too: VAULT_ADMIN_SUBS set with no issuer
means you believe a gate is running while nothing is authenticating, so the
server rejects that combination rather than ignoring the list.
| Variable | Default | Description |
|---|---|---|
HTTP_ADDR | :8080 | Listen address for the JSON API |
USER_DATA_DIR | /data | The data volume |
OIDC_ISSUER | (empty) | Empty ⇒ open mode. Set to require authentication |
OIDC_CLIENT_ID | (empty) | Required when OIDC_ISSUER is set |
OIDC_CLIENT_SECRET | (empty) | Confidential-client secret |
OIDC_SCOPES | (empty) | Extra scopes, comma-separated |
PUBLIC_BASE_URL | (empty) | Required in auth mode; must be https:// (or http://localhost) |
VAULT_ADMIN_SUBS | (empty) | Administrator OIDC subjects. Required in auth mode |
VAULT_SELF_PROVISION | (empty) | true lets a signed-in caller claim one namespace of its own. Off is the right default for a self-hosted vault |
CONSENT_URL | (empty) | pks-agent-consent base URL. Without it, no agent policy that asks for consent can release anything |
CONSENT_OWNER | (empty) | The consent tenant to ask. Required alongside CONSENT_URL |
LOG_LEVEL | info | debug, info, warn, error |
CONSENT_URL is not optional in practice. Leaving it empty does not turn the
consent gate off — it fails closed, and every agent policy except the
consentMode: never kind (which the server only accepts for a policy expiring
within the hour) will refuse to release. That is deliberate: a gate that
disappears when it is misconfigured is not a gate. The same applies to a consent
service that is unreachable, erroring, or too old to support single-use
approvals; the last of those is detected explicitly and reported as
consent_incompatible, because an approval that can be replayed for thirty days
is not the thing the vault asked for.
PUBLIC_BASE_URL is required in auth mode because proof-of-possession
signatures are bound to the absolute request URL; a service that does not know
its own public URL cannot verify them.
Browsing to / returns JSON, not a page, and that is deliberate. The client that
seals and unwraps secrets is served from a different origin, because whoever
serves a client's code decides what that code does with the plaintext. The one
component whose claim is "it cannot read your secrets" must not also be the one
choosing the code that reads them.
If you self-host, you supply the depositing client — either the keyholder CLI or your own page on your own origin. The server will never grow one.
The admin tool ships in the same image, and its commands split in two by whether they write.
Reading is safe while the server runs. Every file is written with an atomic rename, so a reader always sees a whole file:
docker exec vault vault-admin --data /data owners
docker exec vault vault-admin --data /data audit verify acme
docker exec vault vault-admin --data /data audit head acme
Writing requires the server to be stopped. owner add and reindex take the
volume's process lock, which the running server already holds; two writers on one
volume would fork the audit chain into two histories no verifier can reconcile.
Run them in their own container against the same volume:
docker run --rm -v vault-data:/data \
registry.agentics.dk/agentics/pks-agent-vault:latest \
vault-admin --data /data owner add acme --subs <oidc-sub> --label "Acme"
That is also how you create the first owner: do it before starting the server, or stop it first. Attempting it against a live volume fails with a message saying so rather than an flock errno.
VAULT_SELF_PROVISION=true turns on POST /api/owners, which lets a signed-in
caller claim one namespace bound to its own subject — what vault init uses
to make installing the client the whole of setup. Leave it off and nothing
changes: the route answers 404, and the CLI tells the user to ask you.
Think about it before turning it on, because it is two decisions, not one:
VAULT_ADMIN_SUBS exists to prevent for administration, so it is only
reasonable when signing in already means something — a closed realm, or a
hosted service that intends to be self-service.POST /api/owners answers 409 on a
name that exists. Nothing else in the API changes: every other route still
answers the same 404 for a namespace that is absent and one that is not
yours, so knowing a name is claimed still tells you nothing about what is in
it or who holds it.One subject may claim one namespace. Further ones are owner add, on the box,
with a person deciding — which is the same rule as before, applied to the case
where nobody has decided anything yet.
Verify the audit chain on a schedule and keep the printed head somewhere off the box. Chain verification catches any edit or deletion within the log; it cannot, by itself, catch someone deleting the newest segment entirely, because nothing on the disk records how far the chain had got and an attacker with write access would edit anything that claimed to. A head you exported earlier is what catches that.