Skip to content

Threat model

What Rivet defends against, what it does not, and where the boundary sits. If you are deciding whether to point Rivet at your home directory, this is the page to read.

For reporting a problem, see SECURITY.md. For how the vault works mechanically, see SECRETS.md and ADR-0003.

A tool that reads configuration out of your home directory into a Git-versioned repository you own, and writes it back on another machine. It runs as you, with your permissions, on a machine you control.

That framing decides most of what follows.

Ranked by what their loss costs you:

Asset Where it lives Loss means
The vault identity ~/.config/rivet/identity.age Every sealed secret, past and future, is readable by whoever has it
Secret plaintext Your $HOME; sealed into secrets/*.age A credential is compromised
Configuration Your $HOME; captured into files/ Disclosure of what you run and how; sometimes embarrassing, occasionally sensitive
The snapshot repository Local, and wherever you push it All of the above, at whatever exposure the destination has
Your live $HOME Your machine A bad restore overwrites work

Trusted, and therefore not defended against

Section titled “Trusted, and therefore not defended against”

Being explicit about this is more useful than a longer list of mitigations:

  • Your user account. Rivet runs as you and can read everything you can. An attacker with code execution as your user does not need Rivet.
  • Your machine’s integrity. Rivet cannot detect a compromised kernel, a malicious shell profile, or a keylogger.
  • The tools it delegates togit, age, sops, restic, rclone, and your native package manager. Their correctness is theirs. How Rivet invokes them is Rivet’s, and is in scope.
  • Your own snapshot repository. Rivet treats a repository you point it at as authoritative.

A. Someone who reads a pushed snapshot repository

Section titled “A. Someone who reads a pushed snapshot repository”

Because you pushed it somewhere public, a collaborator was added, or a hosting account was breached.

Stopped by: the secret scanner refuses to capture content matching a high-confidence credential signature; name-based exclusions skip .ssh, .gnupg, credentials, and similar; declared secrets are sealed with age and only ciphertext is stored; the vault identity is never captured, and a test asserts it never reaches a backend artifact.

Not stopped: captured configuration is plaintext by design, and it discloses what you run. A credential in a format the scanner does not recognise is captured like any other file. Review git show --stat before pushing.

B. Someone with read access to a storage backend

Section titled “B. Someone with read access to a storage backend”

An fs backend on a NAS, an rclone cloud remote, a shared restic repository.

Stopped by: fs and rclone are Rivet-encrypted by default (age), so the backend holds ciphertext; restic provides its own. The identity file is never written to a backend path.

Not stopped: --no-encrypt disables Rivet-side encryption, deliberately and by explicit request. A backend still leaks metadata — snapshot count, timing, and approximate size.

C. A malicious or tampered snapshot being restored

Section titled “C. A malicious or tampered snapshot being restored”

Someone else’s repository, or yours after modification.

Stopped by: package names are validated before reaching a package manager, so a name cannot be misread as a command-line option; secret names and manifest paths are rejected if they traverse outside their intended root; every restore write is confined to $HOME (see below); rollback copies are made before anything is overwritten.

How containment is actually enforced. Two rules, both in rivet-core/src/paths.rs, because checking the manifest’s strings is not enough — the write that follows resolves against the filesystem:

  1. A symlink Rivet creates must point back inside $HOME. Capture only ever records a link whose target stays in $HOME, so a manifest asking for anything else was hand-edited; restore refuses it rather than creating it. This matters because a link is a doorway: a later entry in the same manifest can be written through one.
  2. A path Rivet writes through must really land inside $HOME, checked component by component against the filesystem as it exists at that moment. A symlinked ancestor is followed only as far as it stays inside $HOME — so a dotfiles farm pointing ~/.config/nvim at ~/dotfiles/nvim still works, while a ~/Documents pointing at another volume stops the write.

Rule 2 is not only an anti-tampering measure: it is what makes the guarantee hold for the ordinary user whose home directory already contains symlinks.

Not stopped: restoring from a repository is equivalent to trusting whoever authored it. Export hooks run commands. Restored shell configuration runs the next time you open a terminal. Do not restore from a repository you do not trust. Rivet does not sign snapshots, and does not verify that a repository was not edited by hand.

Containment is also not hardened against a concurrent local attacker: the component-by-component check canonicalises rather than using openat2(RESOLVE_BENEATH), so a process racing the restore could in principle swap a directory for a symlink between check and write. That is consistent with the trust boundary above — an attacker already running as you does not need Rivet — and it is the trade the code documents at the point it is made.

D. Rivet itself capturing something it should not

Section titled “D. Rivet itself capturing something it should not”

The failure mode with the widest blast radius, because it turns a private file into a pushed one.

Stopped by: every capture in Rivet funnels through one collector (rivet-core/src/collect.rs), so the exclusion rules, the content scanner, and home-containment cannot be bypassed by adding a new caller. Symlinks are never dereferenced, and one whose target resolves outside $HOME is skipped with a warning. A detected secret blocks the whole snapshot rather than being silently redacted — a broken snapshot you notice beats a leaked one you do not.

Note on the v0.8 skip: an unchanged file is not re-scanned. That is sound only because identical bytes give an identical verdict from the same scanner, which policy::SECRET_SCAN_VERSION enforces — a manifest written under different rules never authorises a skip. See PERFORMANCE.md.

Not stopped: the scanner is a safety net, not a guarantee. --allow-excluded-names relaxes name-based exclusions by explicit request (the content scanner still runs on every file).

Stopped by: restore prints a plan and does nothing until --apply --yes; --diff shows per-file changes first; rollback copies are written before any overwrite; writes are confined to $HOME by the two rules in §C, enforced against the resolved path rather than the manifest’s text.

Not stopped: rollback copies are on the same disk, so they do not survive disk loss. Rivet is not a backup tool — it captures configuration you have selected, not your data.

Stopped by: multi-recipient sealing — rivet vault init --recipient age1... registers a backup recipient, and future seals encrypt to both.

Not stopped: nothing else. There is no escrow, no recovery path, and no maintainer who can help. This is deliberate: a recovery path is a second way in. Register a backup recipient.

Stated here rather than left to be discovered:

  • The crypto has not been externally reviewed. v0.5’s definition of done called for it; the release was tagged without it and it is still owed. An internal review found and fixed one issue (path traversal via a secret name). Treat the crypto as unaudited — SECURITY.md records what the outstanding review covers and why its brief is not published yet.
  • Snapshots are neither signed nor integrity-checked. The manifest’s content hashes exist so the next snapshot can skip unchanged files; restore does not consult them, and they will not reveal hand-editing.
  • Secret detection is signature-based, covering OpenSSH and PGP private keys and GitHub and AWS token prefixes. It has no entropy heuristic and no understanding of file formats.
  • No protection against a passphrase-less identity file beyond filesystem permissions. rivet vault init --passphrase is optional and off by default.
  • Export hooks run arbitrary commands from the plugin set. Built-in plugins are reviewed; user plugins in your own config are yours to trust.

If you are evaluating Rivet for something sensitive

Section titled “If you are evaluating Rivet for something sensitive”

Three things, in order:

  1. Run rivet snapshot against a scratch $HOME first and read git show --stat before you push anything.
  2. Keep the snapshot repository private, and use rivet vault for anything that is genuinely a credential rather than relying on the scanner.
  3. Register a backup vault recipient, and store it somewhere the machine is not.