ADR-0003 — Secrets and encryption
- Status: Accepted (2026-07-24, Opus review)
- Related: ROADMAP v0.5, CODE_REVIEW M3/L5
- Acceptance note: Confirmed —
age(Rust crate) as the built-in primitive, optional pass/SOPS/Bitwarden plugins, keys never leave the machine. No code in v0.2 beyond an emptyrivet-cryptocrate stub; full vault is v0.5 and must land with an external security review.
Context
Section titled “Context”v0.1.1 keeps users safe by excluding secrets: policy::is_excluded_path
blocks .ssh, .gnupg, keyrings, etc., and policy::secret_findings blocks
files containing key/token signatures before Git staging. That is correct for a
plaintext Git repo, but the v1.0 goal is to include secrets so a restored
machine is actually usable — encrypted properly, keys never committed.
This is a first-class subsystem, not a flag. The decision is which encryption model balances “done properly” against Rivet’s simplicity and delegate-to-tools principle. The user asked us to recommend, so this ADR recommends and justifies.
Decision
Section titled “Decision”Built-in encryption uses age (via the age Rust crate), with optional
plugin encryptors for users already invested in other tools.
Why age:
- Modern, small, audited; single X25519+ChaCha20-Poly1305 construction, no cipher/opt soup. Fewer ways to hold it wrong than GPG.
- No GPG keyring/agent friction; identities are plain files.
- Native Rust crate — no external binary required for the default path (keeps
Rivet self-contained), while remaining compatible with the
age/rageCLIs. - Multi-recipient by design: a snapshot can be encrypted to several public keys (this machine, a laptop, an offline backup key) so it restores on more than one host.
Design:
- Identity key generated by
rivet vault init, stored at~/.config/rivet/identity.age(mode600), never written to any backend. Public recipients are recorded inrivet.toml. - Secrets live as
secrets/<name>.ageinside the snapshot tree; plaintext exists only transiently on the local machine during seal/unseal. - Commands:
rivet vault init,vault add <path>,vault list,vault rm,vault seal(encrypt tracked secrets into the snapshot),vault unseal(decrypt on restore, into the correct home locations with correct modes). - Optional plugin encryptors implement a
SecretStoretrait so users can keep their existing workflow instead of the built-in age path:pass(GPG-backed password-store),- Mozilla SOPS (age/GPG/KMS) for teams and cloud KMS,
Bitwarden/rbwfor those centralising in a password manager. Rivet orchestrates; the store owns the crypto.
- The scanner stays on as a safety net. With a vault present, the sanctioned channel for secrets is the vault; the scanner keeps blocking accidental plaintext secrets from the ordinary tracked paths, and its ruleset grows toward gitleaks coverage (CODE_REVIEW L5). Two layers, not one.
Key handling rules (non-negotiable, enforced + tested):
- Private identities and passphrases are never written to a backend.
.gitignore/backend ignore rules exclude identity files by construction.- Restore refuses to unseal without an available identity; it never prompts to paste a key into a shared location.
Alternatives considered
Section titled “Alternatives considered”- Standardise on SOPS. Excellent for teams/KMS but heavier and GPG-adjacent; overkill as the default for a personal dotfile tool. Offered as a plugin, not the base.
- GPG directly. Keyring/agent UX is the pain
agewas built to remove. Available throughpass/SOPS plugins for those who want it. - Roll our own crypto. Never. Rejected on principle.
- Keep excluding secrets forever. Fails the stated v1.0 requirement.
Consequences
Section titled “Consequences”- Positive: secrets become part of a reproducible environment without ever exposing keys; multi-recipient means multi-machine restore; the default path needs no external binary.
- Positive: power users keep pass/SOPS/Bitwarden via plugins.
- Cost: key lifecycle is real UX surface (generation, backup, rotation, loss). Must be documented with a threat model and covered by an external security review at v0.9 (ROADMAP).
- Risk: a lost identity means lost secrets — mitigated by encouraging a
second backup recipient at
vault initand warning loudly.