# Backing up a secrets vault without backing up the secrets

> A vault backup often holds every value, or the key that opens them. Which part of each design's backup is secret, where to keep it, and how to test it.

URL: https://crunu.com/blog/backing-up-a-vault-without-plaintext
Language: en
Updated: 2026-10-30

A backup of a secrets store can be a copy of every secret, or one key away from it. A backup of a folder of `.env` files is the secrets. A database dump of a server-side vault is ciphertext, but the root key in the server's configuration opens it, and the two can end up in the same snapshot. The rule that holds across designs: split the backup into the data and whatever opens it, keep them in different places with different people able to reach them, and test a restore before you need one. In a design where the server never holds a decryption key, the data part contains no plaintext values and can be stored like any other archive. You still keep the server's settings file separately, and you still plan for what a restore rolls back.

The guide to [self-hosting a secrets vault](https://crunu.com/guides/self-hosting-a-secrets-vault) compares the designs themselves. This post is about their backups.

## What's in the backup, by design

| Design                                               | The data                                                            | What opens it                                                                        |
| ---------------------------------------------------- | ------------------------------------------------------------------- | ------------------------------------------------------------------------------------ |
| `.env` files on a share, in a bucket or a repository | plaintext values                                                    | nothing needed                                                                       |
| SOPS, git-crypt, Ansible Vault                       | encrypted files                                                     | the age, PGP or KMS keys, or the vault password                                      |
| HashiCorp Vault, OpenBao                             | a storage snapshot                                                  | unseal key shares, or the auto-unseal KMS key                                        |
| Infisical, self-hosted                               | a Postgres dump                                                     | the root encryption key in the server's environment, or in an HSM                    |
| Crunu, self-hosted on the Enterprise plan            | an export archive with ciphertext, public keys and signed documents | no server-side key opens values; environment keys live only with people and machines |

The middle rows share a pattern. Infisical's security documentation describes "a 256-bit AES key provided by the operator as an environment variable" that encrypts the internal KMS root key, which in turn encrypts per-project data keys stored in the database, a form of [envelope encryption](https://crunu.com/learn/envelope-encryption). Decryption "requires both server configuration (the root encryption key) and data from the storage backend", so "compromising the database alone is insufficient" ([Infisical: security](https://infisical.com/docs/internals/security)). That's a sound design, and it makes the backup question concrete: if the dump and the environment file sit in the same server image, the image is a full copy of every secret.

Vault's version has a sharp edge in the other direction: "If the seal mechanism or its keys are permanently deleted, then the Vault cluster cannot be recovered, even from backups" ([Vault: seal/unseal](https://developer.hashicorp.com/vault/docs/concepts/seal)). Losing what opens the backup is as final as losing the backup.

## Four mistakes that turn a backup into a leak

- **Whole-machine snapshots.** A VM or disk snapshot of the vault server contains the database and the configuration file with its keys. Anyone who can read the snapshot bucket can read everything.
- **One bucket for everything.** Database dumps and the `.env` copied "for completeness" into the same place. Same result.
- **Backup access outlives people.** Removing someone from the vault doesn't remove them from the storage account where backups go. For a plaintext design, backup access is read access to every secret.
- **Deleted values live on.** Removing a value from the store doesn't remove it from last month's backups. Retention decides how long it survives.

None of these need an attacker to be clever. They need someone to have kept access they shouldn't have.

*Figure: How data moves in Crunu, in five numbered steps. 1: Priya’s browser encrypts version 15 of payments-api production and signs it. 2: the server stores only ciphertext, public keys and signed documents, and sees names and sizes but no values. 3–5: environment keys are sealed separately to Jonas for development and staging, to the machine api-prod-1, and to the CI token for web-app staging.*

## What a blind-server backup looks like

Crunu's server stores ciphertext, public keys, token HMACs, signed documents and metadata. Its backup is an archive of exactly that, made with `crunu-server export`. Nothing in it opens a variable: environment keys belong to people and machines, and passwords and Secret Keys never reach the server ([Crunu docs: backups and restore](https://crunu.com/docs/self-hosting/backups-and-restore)).

The backup still has two parts, with very different secrecy:

| Part                | What's inside                                                                                                        | Where it goes                                           |
| ------------------- | -------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------- |
| the export archive  | ciphertext and metadata: names of organizations, projects, environments and machines, email addresses, the audit log | any reliable storage where that metadata may be seen    |
| the server's `.env` | `SERVER_KEY`, `BOOTSTRAP_TOKEN`, the database address with its password, email keys                                  | a password manager or a safe, never next to the archive |

The reason for the split is specific. Without `SERVER_KEY`, a restored server recognizes no session, share link, invitation or authenticator-app secret; environment data survives, but everyone signs in again. Together with an archive, `SERVER_KEY` reveals the authenticator-app secrets and the alert webhook addresses. So the `.env` is a backup-critical secret, and the archive and the `.env` never share a location.

A daily export and a 30-day retention look like this on a self-hosted Docker server:

```sh
node scripts/crunu-server.mjs export --env-file .env --from sqlite:/data/crunu.db --out backups/crunu-$(date +%F).ndjson.gz
```

The archive is written to a temporary file and appears under its name only when complete, so an interrupted export never looks like a finished backup. On Postgres platforms and Cloudflare, run the same command from any machine that can reach the database; the server has no built-in backup scheduler.

## The honest limits of "no plaintext"

- **Metadata is real data.** Environment names, email addresses and the audit log say a lot about a company. Treat the archive as confidential, just not as secret.
- **History is in the archive.** The archive holds the ciphertext of each environment's last 100 versions. When someone is removed, their environments get new keys, so their old key opens no version made after the removal, even from a backup. What they could read before they left, they could read anyway, which is why removal comes with a reissue checklist.
- **The `.env` is still a secret.** "No plaintext in the backup" applies to the archive only.

## Testing a backup

An untested backup is a hope. Crunu's archive has a check built in:

```sh
node scripts/crunu-server.mjs verify --in backups/crunu-2026-10-24.ndjson.gz
```

It checks the checksum and record counts and prints the file's SHA-256. The stronger test is a trial restore into an empty SQLite file on a laptop, then signing in on top of it. Do that monthly, and before any server move or upgrade.

For Vault and Infisical, the equivalent is a restore into a scratch cluster or database using the keys from where you keep them, not from the production server. If the test only works with the production server's configuration at hand, your backup depends on a machine you're trying to survive losing.

*Figure: The history of the payments-api production environment at Kiteline, a fictional company: versions 15 to 11, each with its author, time and device, and what changed by variable name, never by value. Version 15 is current. Version 13 is open, and a dialog asks to restore it: Crunu will save a new version 16 with the files of version 13, and version 15 stays in the history.*

## What a restore rolls back

A restore returns the database to the moment of the backup. Everything after it is gone: new versions, invited people, enrolled machines, granted access. In a design with rollback protection, there's a second effect.

Crunu's clients and agents remember the newest state number of each environment and reject lower ones, which protects against a server that serves old data. A restore looks exactly like that to them. After a restore you made yourself, machines and terminals need their memory of the server reset (`state.json` for the agent and the command line), and browsers need the site's data cleared before signing in again. The docs give the exact steps, with a warning worth repeating: if a rollback warning appears *without* a restore, don't reset anything. The server may be serving old data on purpose.

That's a property to weigh, not a flaw. A vault that accepts an older state silently after a restore would also accept it silently from a compromised server.

## Backups when the platform is managed

On platforms where someone else runs the database, there are two layers of backup, and both count.

- **The platform's own.** Cloudflare D1 Time Travel, Neon point-in-time restore, Cloud SQL automatic backups and RDS snapshots restore the database in place, inside the same provider and usually the same account. Turn them on.
- **Your export.** An archive you take out of the platform is the one that survives losing the account, moves to another platform and can be verified on its own. Crunu's docs treat platform backups as an extra layer, not a replacement, and recommend copying the latest archive to another provider weekly.

The split rule applies here too. The platform's backup of the database and the platform's secret store holding the server's settings are in the same account, so one compromised account reaches both. Keep a copy of the `.env` outside the platform.

## A backup plan for any design

- **Split** the data from what opens it, and store them with different providers or different accounts.
- **Restrict** who can read the backup storage, and review it when people leave.
- **Retain** for as long as you need to recover, and no longer, because old backups keep deleted values alive in plaintext designs.
- **Test** a restore on a schedule, using only what's in the backup locations.
- **Write down** what a restore undoes and who has to do what afterwards.

The glossary entry on [encrypted backups](https://crunu.com/learn/encrypted-backup) has the short definition.

## Sources

- [Security internals, Infisical Docs](https://infisical.com/docs/internals/security), checked October 30, 2026.
- [Seal/Unseal, HashiCorp Vault docs](https://developer.hashicorp.com/vault/docs/concepts/seal), checked October 30, 2026.
