Local Vault: file format, recovery, and backup
Local vault is devboy-tools's own encrypted local secret store. One file, three unlock paths, a recovery phrase for emergencies. This document explains the format, when to pick local-vault over the OS keychain or external sources, and how to back it up safely.
See ADR-023 §3.1–§3.3 and ADR-021 §4 for the full specification.
Russian translation:
ru/local-vault.md.
When to use local-vault
Main criterion: local-vault is for cases where there is no good system store. If the OS keychain or 1Password is available, prefer them. Local-vault gives you portability and file-level control at the cost of owning backups and key rotation yourself.
Unlocking from the secrets UI
devboy secrets ui resolves the backend three ways at startup, in order:
DEVBOY_VAULT_PASSPHRASEis set → the vault opens straight away, no prompt. Scriptable; right for headless / auto-agent runs.- A
.dvbfile exists (default<config>/devboy-tools/secrets/local-vault.dvb, override withDEVBOY_VAULT_PATH) but no env passphrase → the UI opens a modal unlock prompt over a dimmed inventory. Type the passphrase, hit Unlock. A wrong passphrase shows in red and the modal stays open. Use keychain instead is the escape hatch — it switches to the OS keychain for that session. - No
.dvbfile, no env passphrase → the backend defaults to the OS keychain. A Switch to encrypted vault button in the top bar opens a create-vault modal: pick a passphrase, confirm it, and the UI mints the vault and shows the recovery phrase once — behind an explicit I've saved this phrase gate.
Once unlocked, the top bar offers Lock vault (drops the passphrase, the unlock prompt returns) and Switch to keychain. The switch is session-scoped — the env var stays the durable, machine-level override.
The passphrase typed into the modal lives in a SecretString and is zeroized when the modal closes; the agent never sees it (same contract as the provision dialog).
First-run onboarding wizard
On the very first launch — no sources.toml, no .dvb file, no DEVBOY_VAULT_PASSPHRASE — devboy secrets ui shows an onboarding wizard instead of jumping to the keychain. It's a backend picker:
- OS keychain — pre-ticked, zero config.
- Encrypted local vault — ticking it reveals passphrase + confirm fields; on finish the
.dvbfile is created and its recovery phrase is shown once. - HashiCorp Cloud Vault — ticking it reveals address / namespace / mount / token fields plus a read-only / read-write access radio.
Combinations are allowed — all three can be configured at once. One is the primary backend (a radio over the ticked providers) — the one the UI works against today. Finish setup writes a sources.toml with one [[source]] per ticked backend and [default].source pointing at the primary; Skip — use keychain goes straight to the keychain, writing nothing. The wizard never re-appears once sources.toml exists.
External Vault as a read-source (HCP / HashiCorp Cloud)
A type = "vault" [[source]] points devboy at a remote HashiCorp Vault over the HTTP KV v2 API. HCP Vault (the managed offering) needs a namespace (conventionally admin); plain open-source Vault leaves it blank. Settings:
An ADR-020 path team/openai/api-key is read from the Vault reference <mount>/team/openai/api-key.
Read-only today. The
SecretSourcetrait has no write surface yet — that ships in P15. So evenaccess = "readwrite"cannot write to Vault from the UI yet; the UI refuses a save with an honest message and you write the value with the Vault CLI / UI, then devboy reads it back.access = "read"is the safe, working choice;access = "readwrite"is forward-looking config.
The access mask
access = "read" narrows whatever the source plugin declares to READ | LIST | VALIDATE — WRITE / ROTATE are masked off even when the backend supports them. Use it to mount a team's shared Vault read-only on most machines and leave readwrite only where rotation runs. doctor's "Sources" card shows both the declared and the effective (masked) capability set, so a refused write is explainable. The mask can only narrow — it never grants a capability the plugin lacks.
Multi-source routing is deferred (P11). The onboarding wizard writes every selected backend into
sources.toml, but the UI drives exactly one — the primary — until the P11 router orchestration lands. The other entries are recorded and ready.
File format
The file lives at ~/.devboy/secrets/local-vault.dvb by default. Binary layout (see crates/devboy-vault-crypto/src/format.rs):
The DVB1 magic is enough for file local-vault.dvb to tell a vault from random bytes.
Metadata is readable without unlock
description, retrieval_url, expires_at, rotation_method, and pattern_id are stored in the clear. This is intentional — secrets list, doctor, and discovery flows must work without a PIN prompt. Threat model: a local process with read access to the file already sees what ls -la exposes. Encrypting the metadata would add a prompt with no real protection.
Unlock paths (envelopes)
One vault can support several unlock methods at once. Each method is a separate envelope that independently stores an encrypted copy of the vault-key. Any envelope unlocks the same blobs.
1. Passphrase envelope (default)
Default Argon2id parameters (KdfParams::DEFAULT): m=64 MiB, t=3, p=1, salt 32 B. On 2024-class hardware one unlock takes ~250 ms. Change parameters via devboy secrets vault rekey — the old salt is preserved so other envelopes keep working.
2. Keychain envelope (optional)
The OS keychain stores a fresh random 32-byte key; the envelope's wrapped_key is XChaCha20-Poly1305(keychain_key).encrypt(vault-key). Convenient: the vault unlocks via a single fingerprint / PIN prompt from the OS, no passphrase typing.
Available on macOS / Windows / Linux with gnome-keyring; other platforms — passphrase + recovery phrase only.
3. Recovery phrase envelope (BIP39)
12-word BIP39 mnemonic, generated when the vault is created. The envelope uses HKDF(BIP39_seed) without Argon2id (the phrase is high-entropy on its own). The recovery phrase is the spare key for cases where:
- The passphrase is forgotten.
- The keychain entry is wiped (new OS, reinstall).
- The vault file moves to a machine without a keychain.
The recovery phrase is never stored automatically. The CLI prints it once when the vault is created; you decide where to put it (paper list, password manager, sealed envelope).
⚠ Without the recovery phrase: potentially permanent loss: if the passphrase is forgotten and the keychain is unavailable and the recovery phrase was never saved — the vault cannot be unlocked. There is no key escrow in the system; brute-forcing Argon2id with default parameters takes years. Back up the recovery phrase separately from the vault file.
Recovery: what to do when something breaks
Scenario A — passphrase forgotten
-
If a keychain envelope exists → unlock via keychain:
rekeyrecreates the passphrase envelope with a new phrase; the keychain envelope stays valid. -
If the keychain is also gone → recovery phrase:
-
If neither keychain nor recovery phrase is available → restore from a vault-file backup taken before the key was lost.
Scenario B — vault file is corrupt
The magic is wrong / the TOML doesn't parse / a blob fails to decrypt:
- Don't panic — do not write anything over the corrupt file.
- Copy
~/.devboy/secrets/local-vault.dvbsomewhere safe (local-vault.dvb.broken). - Restore from the latest backup.
- Run
devboy secrets validate. Green → continue. - Investigate what corrupted the file: full disk,
kill -9mid-write, manual editing. Atomic writes (tmpfile + rename) rule out interrupt-based corruption but not external tampering.
Scenario C — migrate the vault to a new machine
The vault is one file, so migration is trivial:
After unlocking on the new machine, add a fresh keychain envelope:
Backups
The vault is one file, so backup = regular copy + recovery phrase stored separately.
Minimum policy (one developer)
- Once a day, cron /
systemd --usercopies~/.devboy/secrets/local-vault.dvbto encrypted storage (Time Machine, Backblaze, a Restic repo with its own password). - Recovery phrase on paper in a physically secure place and in a personal password manager (e.g. 1Password Personal). Never with the vault file.
- Once a quarter — restore drill: deploy the backup to a clean machine/container and try unlocking through the recovery phrase.
What not to do
- ❌ Put the vault and the recovery phrase in the same backup directory. If the backup leaks, everything leaks.
- ❌ Store the recovery phrase as a plaintext file in a repo / on a Git server. BIP39 is high-entropy, but a public leak turns it into a one-step unlock key.
- ❌ Send the vault file over email / messengers. AEAD protects values, but the KDF salt in the header leaks, which speeds up offline passphrase brute-force. Use a secure channel (
scp,magic-wormhole, S3 with KMS). - ❌ Use a weak passphrase. Argon2id slows brute-force, but
password123still falls. Minimum: 4 randomdiceware/xkpasswdwords or 16+ characters from a password manager.
A good workflow
Security boundary
- AEAD choice: XChaCha20-Poly1305. The long nonce (24 bytes) lets us pick it randomly without a counter. Throughput is on par with ChaCha20.
- AAD: every ciphertext is bound to its envelope kind (
passphrase/keychain/recovery). Moving a blob between incompatible envelopes is detected at decrypt time. - Zeroize:
vault-keyandunlock-keyare wrapped insecrecy::SecretBox, which callszeroizeon drop. Hand-off through FFI to keychain backends uses minimal lifetimes. - Lock posture: after
idle_timeout(default 60 s with no requests) the daemon zeroizes the in-memory keys. Unlock is required again. - Does NOT protect against:
- A root/admin local process that can read the vault file and scrape envelope unlock-keys from the daemon's memory.
- CPU side-channel attacks (Spectre-class).
- Social engineering (phishing the recovery phrase).
See also
onboarding.md— first-run install + source setup.token-catalog.md— author per-provider procedure files (kimi.json,openai.json) the GUI binds to.catalog-url-sources.md— serve catalogs over the network with sha-pinning + audit log.agent-protocol.md— how AI agents talk to the vault through MCP.- ADR-023 §3.1 (format) and §3.2–§3.3 (envelopes) — the formal spec.
crates/devboy-vault-crypto/— source for the format, AEAD, and KDF.