-
Notifications
You must be signed in to change notification settings - Fork 0
Backup and Recovery
Your vault holds the only copy of things that are, by design, absent from your project repos: env hierarchies, guarded secrets, swap overrides. This page covers how that data becomes durable, and how to get an earlier version back.
┌─ VAULT (~/.rsenv) ────────────────────────────────────────────────┐
│ │
│ vaults/myproject-abc123/ │
│ envs/local.env ← plaintext, GITIGNORED (*.env) │
│ envs/local.env.a1b2c3d4.enc ← committed ● │
│ guarded/secrets.yaml.b9c0d1e2.enc ● │
│ swap/docker-compose.yml ● │
│ dot.envrc.f3a4b5c6.enc ● │
│ │
│ .git/ ← ONE repo at ~/.rsenv, covering every vault │
└───────────────────────────────────────────────────────────────────┘
│ rsenv vault commit --push
▼
remote (your backup)
Only the ● files reach git. Plaintext *.env and *.envrc are gitignored — the patterns are
derived from sops.file_extensions_enc, so they track your config automatically. Nothing
recovers a .env you never encrypted.
| Artifact | Durable? |
|---|---|
envs/*.env |
Only via its .enc — and only once encrypt has run |
guarded/* |
Only via its .enc
|
swap/* overrides |
Yes, committed as-is (not secrets by definition) |
dot.envrc |
Only via its .enc
|
*.bkp.env sweeps from rsenv env init
|
Yes — the marker sits before the extension, so backups stay covered by gitignore and SOPS |
| Files currently swapped in | No — the live bytes are in the project; the vault holds a frozen sentinel |
Encrypted files are named {name}.{hash8}.enc, where hash8 is the first 4 bytes of the
SHA-256 of the plaintext. This is the single design decision the whole backup story rests
on, so it is worth explaining properly.
Given local.env and local.env.….enc side by side: is the ciphertext up to date?
Every obvious way to answer that fails:
| Approach | Why it fails |
|---|---|
| Re-encrypt and compare bytes | SOPS output is non-deterministic — see below |
| Decrypt and compare | Needs your private key for a routine status check |
| Compare mtimes |
git checkout, rsync and editors all rewrite mtimes |
| Sidecar manifest of hashes | A second file that can drift out of sync, and merges badly |
SOPS non-determinism is not a subtlety — encrypting the identical file twice produces a fresh data key, a fresh IV per value, and a fresh MAC:
$ sops -e --age $KEY --output a.enc t.env
$ sops -e --age $KEY --output b.enc t.env
$ diff a.enc b.enc
< export A=ENC[AES256_GCM,data:Jg==,iv:VovErgrYwe2...,tag:4MGnjm9J...]
> export A=ENC[AES256_GCM,data:GQ==,iv:2zlOCCtXZbG...,tag:obyyI6cv...]
< sops_mac=ENC[AES256_GCM,data:FpyKTXLNbck...]
> sops_mac=ENC[AES256_GCM,data:EuNLqpuj7J9...]
So ciphertext can never be compared to ciphertext. Something outside the encryption has to carry the identity of the plaintext.
Put the plaintext's hash in the ciphertext's name. The filename is the manifest — and
unlike a sidecar file it cannot drift, because it travels with the file through git, rsync,
branch merges and machine syncs. Comparing state is then a string comparison against a cheap
local hash.
This one choice buys five behaviours:
| Capability | How the hash provides it |
|---|---|
| Status without keys |
rsenv sops status hashes the plaintext and parses the filename. It never invokes sops -d, so it works with no GPG agent and no YubiKey plugged in. |
| Pre-commit enforcement | Same reason: rsenv sops status --check is a keyless exit code, so the hook can block a commit on any machine. |
| Idempotent encryption | If {name}.{hash}.enc already exists, encrypt returns early without calling SOPS. Unchanged content therefore produces no new git blob — without this, non-determinism would churn a fresh blob on every single commit. |
| Safe cleanup |
rsenv sops clean deletes plaintext only when a hash-matching .enc exists. It is structurally incapable of destroying an unsaved edit. |
| Exactly one ciphertext | Before writing a new hash, encrypt deletes prior-hash .enc files for that name, so a vault never accumulates ambiguous duplicates. |
Being explicit about the costs, because they shape how recovery works:
- The filename is not encrypted, and neither is the hash. Anyone holding the repo can test a guessed plaintext offline against that 32-bit fingerprint. It does not reveal content, but it is metadata: it can confirm "this vault still holds the stock config". Treat a pushed vault remote as private regardless.
- There is no version history on disk. Prior hashes are deleted on re-encryption. Every earlier version exists only as a git object.
-
Git cannot follow a file across versions. Because the name changes with the content, git
sees a delete plus an add, and rename detection cannot link them (the ciphertexts share no
bytes).
git log --followon a.encpath will not work. Recovery uses the recipes below.
rsenv does not create this repo for you. rsenv vault init sets up the vault directory;
the git repository is yours to create, once, at base_dir (default ~/.rsenv) — it covers
every vault:
cd ~/.rsenv
git init
rsenv sops encrypt --global # make sure nothing plaintext is left to stage
git add -A .
git commit -m "Initial vault"Add a remote if you want off-machine durability:
git remote add origin git@github.com:you/rsenv-vault.git
git push -u origin mainOptionally install the keyless pre-commit guard:
rsenv hook installIf the repo is missing, rsenv vault commit fails with git's own "not a git repository" error
and rsenv hook install reports Not a git repository: ~/.rsenv.
rsenv swap out # required: commit refuses unless the vault holds the live bytes
rsenv vault commit -a # encrypts, stages, commits this project's vault dir alone
rsenv vault commit -a --pushvault commit re-encrypts before staging (sops.encrypt_on_commit, default true), so a
.env you edited is captured as current ciphertext rather than stale. See
Command Reference for the full step list and safety checks.
The swap out requirement is the one that bites. While files are swapped in, the live bytes
live in your project and the vault holds only a frozen sentinel — that work is in no git repo
at all until you swap out.
Because each version is a differently-named blob, list additions by glob rather than following one path:
cd ~/.rsenv
git log --diff-filter=A --name-only --date=short \
--pretty='%C(yellow)%h %ad %s' \
-- 'vaults/myproject-*/envs/local.env.*.enc'Each hit is one saved version, newest first.
cd ~/.rsenv
git show <sha>:vaults/myproject-abc123/envs/local.env.a1b2c3d4.enc > /tmp/old.enc
sops -d --input-type dotenv --output-type dotenv /tmp/old.encThe --input-type dotenv flags are required for .env files — rsenv passes them on every
encrypt and decrypt of an .env, and SOPS will misparse the file without them. Other file
types need no flags.
To adopt the old version, write it into place and re-encrypt so the filename hash matches again:
sops -d --input-type dotenv --output-type dotenv /tmp/old.enc \
> ~/.rsenv/vaults/myproject-abc123/envs/local.env
rsenv sops encrypt
rsenv sops status # should report local.env as currentgit clone git@github.com:you/rsenv-vault.git ~/.rsenv
rsenv sops decrypt --globalDecryption needs the GPG or Age private key the vault was encrypted to — the repo alone is not enough. Back that key up separately, by whatever means you already trust; rsenv does not manage it.
| Thing | Why | What to do |
|---|---|---|
| Plaintext never encrypted |
*.env / *.envrc are gitignored |
rsenv sops encrypt before committing |
More than one generation of env init backups |
A sweep skips existing backups, so local.bkp.env is overwritten by the next env init rather than cascading |
Use the vault's git history for anything older |
Legacy *.env.bkp files from rsenv 6.0.0 |
That extension is outside both the gitignore and file_extensions_enc, so encrypt skips them and vault commit refuses the whole commit |
Salvage what you need, then delete them (details) |
| Files currently swapped in | Vault holds a frozen sentinel, not the live bytes |
rsenv swap out first |
| Your GPG / Age private key | Deliberately outside rsenv's scope | Back it up yourself |
- SOPS Encryption — encryption setup, status categories, hash specification
-
Command Reference —
vault commitflags and safety checks - Vault Management — vault layout and lifecycle
-
File Swapping — why
swap outgates committing
rsenv Documentation