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.
What Rivet is
Section titled “What Rivet is”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.
Assets
Section titled “Assets”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 to —
git,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.
Adversaries and what stops them
Section titled “Adversaries and what stops them”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:
- 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. - 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/nvimat~/dotfiles/nvimstill works, while a~/Documentspointing 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).
E. A restore destroying your work
Section titled “E. A restore destroying your work”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.
F. Losing the vault identity
Section titled “F. Losing the vault identity”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.
Known weaknesses
Section titled “Known weaknesses”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 --passphraseis 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:
- Run
rivet snapshotagainst a scratch$HOMEfirst and readgit show --statbefore you push anything. - Keep the snapshot repository private, and use
rivet vaultfor anything that is genuinely a credential rather than relying on the scanner. - Register a backup vault recipient, and store it somewhere the machine is not.