Skip to content

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 empty rivet-crypto crate stub; full vault is v0.5 and must land with an external security review.

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.

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/rage CLIs.
  • 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 (mode 600), never written to any backend. Public recipients are recorded in rivet.toml.
  • Secrets live as secrets/<name>.age inside 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 SecretStore trait 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/rbw for 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):

  1. Private identities and passphrases are never written to a backend.
  2. .gitignore/backend ignore rules exclude identity files by construction.
  3. Restore refuses to unseal without an available identity; it never prompts to paste a key into a shared location.
  • 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 age was built to remove. Available through pass/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.
  • 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 init and warning loudly.