Vault¶
The vault is ProxiPort's encrypted key-value store for documents and
per-client secrets. It is a separate SQLite database
({data_dir}/vault.sqlite.db by default) sealed with a passphrase
that lives only in the server process's memory — never on disk.
A server restart always re-locks the vault, regardless of what is stored inside. An operator with the passphrase has to unlock it again through the SPA or the API before the values become readable.
Lifecycle¶
- Initialize. Once, the very first time the vault is used. Creates the database file and stores key-derivation parameters.
- Unlock. Every time the server starts. The administrator provides the passphrase; the server derives a key, validates it against the stored marker, and keeps the derived key in memory.
- Read/write. Any user with the
vaultfunction permission can list, fetch, store, update, and delete entries while the vault is unlocked. - Lock. Optional manual seal. Wipes the key from memory. The stored data stays on disk.
The administrator who runs unlock does not gain any per-entry visibility beyond what the user-group permissions would otherwise allow — locking is a global control, not a per-entry one.
Admin API¶
The endpoints under /api/v1/vault-admin require Administrators group
membership.
Initialize¶
TOKEN=$(curl -s -u admin:password \
https://proxiport.example.com/api/v1/login | jq -r .data.token)
curl -s -X POST \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
--data-raw '{"password":"a-vault-passphrase"}' \
https://proxiport.example.com/api/v1/vault-admin/init
The passphrase must be between 4 and 32 bytes. Anything outside that range is rejected.
Status¶
curl -s -H "Authorization: Bearer $TOKEN" \
https://proxiport.example.com/api/v1/vault-admin | jq
{
"data": {
"init": "setup-completed",
"status": "unlocked"
}
}
init is setup-completed or uninitialized. status is unlocked
or locked.
Unlock and lock¶
# Unlock
curl -s -X POST \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
--data-raw '{"password":"a-vault-passphrase"}' \
https://proxiport.example.com/api/v1/vault-admin/sesame
# Lock (DELETE on the same path)
curl -s -X DELETE -H "Authorization: Bearer $TOKEN" \
https://proxiport.example.com/api/v1/vault-admin/sesame
If the wrong passphrase is supplied to unlock, the request is rejected and the vault stays locked. If the passphrase is lost, the data is unrecoverable — there is no master key, no backup escrow, and no support recovery path.
User API¶
Once the vault is unlocked, any user with the vault function
permission (see client groups and permissions)
can interact with /api/v1/vault.
List¶
GET /api/v1/vault returns every entry's metadata — id, client_id,
created_by, created_at, key — but not the decrypted value:
curl -s -H "Authorization: Bearer $TOKEN" \
https://proxiport.example.com/api/v1/vault | jq
Supports sort= (prefix with - for descending) and
filter[<field>]= on id, client_id, created_by, created_at,
and key. Pass -g to curl if you use the bracketed filter syntax
on the URL.
Read¶
GET /api/v1/vault/<id> returns the full record including the
decrypted value:
{
"data": {
"id": 1,
"client_id": "alpha-prod",
"required_group": "",
"key": "deploy-key",
"value": "ssh-ed25519 AAAA…",
"type": "secret",
"created_at": "2026-05-17T09:46:07+00:00",
"updated_at": "2026-05-17T09:46:07+00:00",
"created_by": "admin",
"updated_by": "admin"
}
}
Create¶
curl -s -X POST \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
--data-raw '{
"client_id": "alpha-prod",
"required_group": "",
"key": "deploy-key",
"value": "ssh-ed25519 AAAA…",
"type": "secret"
}' \
https://proxiport.example.com/api/v1/vault
Update and delete¶
# Full replacement; partial PATCH is not supported
curl -s -X PUT \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
--data-raw '{ "client_id":"alpha-prod","required_group":"","key":"deploy-key","value":"...","type":"secret" }' \
https://proxiport.example.com/api/v1/vault/1
# Delete
curl -s -X DELETE -H "Authorization: Bearer $TOKEN" \
https://proxiport.example.com/api/v1/vault/1
Field reference¶
client_id
: Optional. Ties a document to a specific agent. Empty (or 0)
means the entry is global and shows up on every client's
Documentation tab.
required_group
: Optional. If set, only users in this user-group can read, update,
or delete the entry. Use it for compartmentalised secrets in a
multi-team server.
key
: Required short identifier. Unique within a client_id scope.
value
: Required. The encrypted body of the document. Every other column
is stored in clear.
type
: Required. One of text, secret, markdown, string. The SPA
uses the type to decide how to render the body — secret is
masked behind a reveal toggle, markdown is rendered, etc.
Backups¶
vault.sqlite.db belongs in the same backup schedule as the other
SQLite files (see
operator runbook — backups). The
encrypted file is safe to copy — the cipher text is useless without
the passphrase.
Restoring a vault backup requires re-entering the passphrase on the restored server. The marker the new server checks against is part of the database, so the same passphrase used at init time will unlock the restored copy.
Clear-text backups¶
Because losing the passphrase loses the data, consider also keeping clear-text dumps of the vault outside the server. The dumps belong in a safe (encrypted disk, password manager, hardware-backed store) — never alongside the unencrypted database file.
A minimal export script:
USER=admin
TOKEN=e83d40e4-e237-43d6-bb99-35972ded631b
URL=https://proxiport.example.com/api/v1/vault
FOLDER=./vault-backup
mkdir -p "${FOLDER}"
IDS=$(curl -s -u "${USER}:${TOKEN}" "${URL}" | jq .data[].id)
for ID in $IDS; do
curl -s -u "${USER}:${TOKEN}" "${URL}/${ID}" -o "${FOLDER}/${ID}.json"
done
tar czf vault-backup.tar.gz "${FOLDER}"
# Securely delete the per-entry files; keep only the tarball.
find "${FOLDER}" -type f -exec shred {} \;
rm -rf "${FOLDER}"
Encrypt vault-backup.tar.gz before storing it anywhere external.
Operational notes¶
- The vault is locked after every restart. This is the most common operator surprise. Bookmark the unlock URL in the SPA, or script the unlock with a credential pulled from your password manager.
- A wrong passphrase only fails — it does not damage the data. Brute-forcing is the only attack path against the data on disk. Use a strong passphrase.
required_groupis checked at every read. Removing a user from the required group immediately revokes their access to the entries that name it.- The vault does not auto-lock on idle. If you want a periodic
lock, run a cron that POSTs
DELETE /api/v1/vault-admin/sesameon the schedule you want.
Hardening checklist¶
- Pick a passphrase that survives a brute-force attempt against the on-disk file — 32 random ASCII characters is a reasonable floor.
- Store the passphrase in a password manager, not in a shell history or a script committed to version control.
- Schedule regular clear-text backups and encrypt them at rest.
- Use
required_groupto restrict per-team secrets even from other vault users.
See also: operator runbook — backups,
architecture — datastore,
client groups and permissions for the
user-group model the vault function permission and required_group
fields rely on.