Secrets vault
Governed by ADR 0003. Spec:
docs/v0.5-implementation-spec.md.
Rivet’s default posture is to exclude secrets: .ssh, .gnupg, keyrings,
and files that match key/token signatures are refused before they can be
captured. That keeps a plaintext Git snapshot safe, but it also means a restored
machine is missing exactly the things that make it yours. The vault is the
sanctioned way to include those files — encrypted end to end, with the private
key never leaving your machine.
Two layers, not one: the plaintext scanner stays on to block accidental secrets in ordinary tracked paths; the vault is the deliberate, encrypted channel for the ones you actually want to carry.
Model at a glance
Section titled “Model at a glance”- Identity — one
ageX25519 keypair generated byrivet vault init. The private half lives only at~/.config/rivet/identity.age(mode600), optionally passphrase-wrapped. It is never written into a snapshot, a backend, orrivet.toml. - Recipients — the public half (
age1…), recorded inrivet.tomlunder[vault]. Safe to commit. You can add more (a backup key, a second machine) so one sealed secret restores in several places. - Declared secrets —
[[secret]]entries inrivet.tomlnaming a file to seal. Declaring~/.ssh/configis the explicit opt-in that lets an otherwise excluded path be captured — always as ciphertext. - Sealed blobs —
secrets/<name>.ageinside the snapshot. Only ciphertext is ever written; plaintext exists on disk only transiently, on the local machine, during aseal/unsealcall.
Commands
Section titled “Commands”# Generate the vault identity; --passphrase wraps it at rest (recommended).rivet vault init --repo ~/rivet-snapshots --passphrase
# STRONGLY recommended: add a backup/offline recipient so a lost identity is# not a lost vault. Can also be passed to `init` as --recipient <age1...>.rivet vault add-recipient --repo ~/rivet-snapshots age1<backup-public-key>
# Declare a file to seal (repeat per file). ~ / $HOME are expanded.rivet vault add --repo ~/rivet-snapshots ~/.ssh/configrivet vault add --repo ~/rivet-snapshots ~/.netrc --store age
rivet vault list --repo ~/rivet-snapshots # recipients + declared secretsrivet vault rm --repo ~/rivet-snapshots ssh-configSealing then happens automatically as part of rivet snapshot, and unsealing as
part of rivet restore --apply --yes (which prompts for the identity passphrase
if the identity is wrapped, or reads RIVET_VAULT_PASSPHRASE for non-interactive
use). rivet vault seal seals into the repo without a full snapshot, as a
preview.
Sealing needs only the public recipients, so you can snapshot a machine that holds no private key at all. Unsealing needs the private identity.
Stores
Section titled “Stores”age(default, built-in) — public-key encryption to the vault recipients via the sameageprimitive Rivet uses everywhere else. No external binary, no keyring, no configuration beyondvault init.sops(optional) — for users already invested in Mozilla SOPS (age/GPG/KMS). Declare a secret with--store sops. Rivet orchestrates;sopsowns the crypto and the keys (your.sops.yaml/SOPS_AGE_*/ KMS setup).sopsmust be installed and configured with a recipient.
pass and Bitwarden are not yet implemented as stores; they map awkwardly onto
per-file seal/unseal and are deferred to a later, dedicated design.
Threat model
Section titled “Threat model”What the vault protects against
- A snapshot repository (local, GitHub, NAS, S3/B2/Drive via rclone, restic)
being read by anyone who does not hold a listed identity. Every declared
secret is
ageciphertext at rest; names and modes are visible, contents are not. - The private identity leaking through a backend. It is written only to
~/.config/rivet/identity.ageand is asserted-absent from every backend artifact by tests that walk the on-disk output. - Accidental plaintext capture. The scanner still blocks key/token signatures in ordinary tracked paths; a declared secret is the only path that bypasses it, and even then only ciphertext is written.
- With a passphrase-wrapped identity: an attacker who obtains the identity
file still needs the passphrase (the file is itself an
agescrypt blob).
What it does NOT protect against
- A compromised local machine at restore time. Unsealing writes plaintext to
its real home location (e.g.
~/.ssh/config) with its recorded mode. Anything that can read your home directory after a restore can read those files — that is the point of restoring them. The vault protects secrets in the snapshot, not your live machine. - Transient plaintext in memory. During a single
seal/unseal, the plaintext exists in a normal buffer in Rivet’s process. The identity and any passphrase are held in zeroized-on-dropSecretStrings; the per-secret plaintext buffer is not separately zeroized (it is short-lived and never written anywhere but its ciphertext form / final destination). Hardening this is a possible future change. - Metadata. A sealed secret’s
name, home-relative destination path, and mode are recorded in the manifest in cleartext (a backend needs them to place the file on restore). Do not encode a secret in a filename. - A weak passphrase. Passphrase wrapping is only as strong as the passphrase.
- Backend-native encryption gaps.
resticand an encryptedfs/rclonebackend add a second at-rest layer, but the vault ciphertext already stands on its own; do not rely on backend encryption as the only protection.
Losing your identity
Section titled “Losing your identity”A lost identity means every secret sealed only to it is lost forever. There is no recovery path and no backdoor — that is a property of the encryption, not a missing feature.
Mitigate it before it happens:
- At
vault init, add at least one backup recipient — an offline/hardware key, or the identity of another machine you control:Every secret sealed afterward can then be unsealed by either key. (Adding a recipient does not re-encrypt already-sealed secrets — re-runTerminal window rivet vault init --repo <repo> --passphrase --recipient age1<backup-public-key>rivet snapshotto re-seal existing ones to the new recipient too.) - Back up
~/.config/rivet/identity.ageitself to safe offline storage, separate from your snapshots. Passphrase-wrap it so the backup copy is not plaintext key material. - Keep the passphrase somewhere you will not lose it either — a wrapped identity with a forgotten passphrase is as lost as a deleted one.
What Rivet will not do
Section titled “What Rivet will not do”- Write your identity or passphrase into any snapshot or backend.
- Unseal without an available identity. On a machine that does not hold one, restore reports a clear error and writes nothing — it never prompts you to copy a key into a shared or repository location.
- Rotate keys automatically. Re-encrypting existing snapshots to new recipients (true key rotation) is deliberately out of scope for v0.5 and is flagged for a reviewed design before it is built.