Skip to content

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

  1. Initialize. Once, the very first time the vault is used. Creates the database file and stores key-derivation parameters.
  2. 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.
  3. Read/write. Any user with the vault function permission can list, fetch, store, update, and delete entries while the vault is unlocked.
  4. 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_group is 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/sesame on 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_group to 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.