---
title: "Encryption"
description: "Configured per backup, performed before upload, with a key you supply."
url: "https://saved.sh/docs/backups/encryption"
---

You supply a PGP public key. We encrypt to it and never hold the private half.

**Where the encryption happens depends on the kind**, and that difference is the whole
security story of the product, so it is the first thing on this page rather than a footnote.

| Kind     | Encrypted by                   | Plaintext exists on |
| -------- | ------------------------------ | ------------------- |
| `local`  | **Your worker**, before upload | Your host only      |
| `cloud`  | Us, after the dump             | Our infrastructure  |
| `manual` | Us, after your upload          | Our infrastructure  |

For a local backup, ciphertext is all we ever receive. For the other two, we necessarily
handle plaintext: we produced it, or you sent it to us. Encryption there protects the archive
at rest, not the pipeline that made it.

## Supplying the key [#supplying-the-key]

Generate a keypair and give us the public half:

```bash
gpg --quick-generate-key "backups@example.com" default default never
gpg --armor --export backups@example.com > ./keys/prod.asc
```

**On the backup definition**, for cloud and manual:

```yaml title="saved.yaml"
encryption:
  public_key: ./keys/prod.asc
```

```bash
sctl backup configure <backup-id> --encryption-key ./keys/prod.asc
```

**On the worker**, for local:

```yaml title="config.yaml"
encryption:
  public_key: |
  -----BEGIN PGP PUBLIC KEY BLOCK-----
  ...
  -----END PGP PUBLIC KEY BLOCK-----
```

The key is validated when you save it. A malformed or unreadable key is refused at configure
time rather than at 02:00.

<Callout type="error">
  **Keep the private half somewhere you will still have it after the incident that makes you
  need it.** We hold only the public key. We cannot decrypt your backups, and we cannot recover
  them if you lose the private key. That is not a policy we could waive; there is no key on our
  side to waive it with.
</Callout>

Store the private key somewhere that does not share fate with the systems being backed up. A
password manager, an HSM, an offline copy in a safe. Not on the database server.

## Local backups: put the key on the worker [#local-backups-put-the-key-on-the-worker]

<Callout type="warn">
  For a `local` backup, set the key in the **worker's** `config.yaml`. Setting
  `encryption.public_key` on the definition as well makes us encrypt again, on top of the
  ciphertext your worker already produced. The result is readable, but you decrypt twice and
  the second pass compresses nothing.
</Callout>

If you want belt and braces, that is your call and it works. If you want one encryption, put
the key on the worker and leave the definition's key unset.

## What "encrypted" means on an artifact [#what-encrypted-means-on-an-artifact]

Every artifact records what was actually done to it:

| Field        | Meaning                                               |
| ------------ | ----------------------------------------------------- |
| `encrypted`  | Whether a PGP layer was applied                       |
| `key_id`     | The fingerprint of the public key it was encrypted to |
| `compressed` | Whether a gzip layer was applied, before the PGP one  |

`key_id` is what tells you **which** key opens a given artifact, which is the field that
matters once you have rotated a key. Listings show it, so you never have to guess.

## Plain artifacts [#plain-artifacts]

Encryption is optional, and an artifact with no key configured is stored as it came out of
the source.

| Kind     | What "no key" means                                                 |
| -------- | ------------------------------------------------------------------- |
| `local`  | The worker uploads the dump as-is. Your plaintext is in our storage |
| `cloud`  | We store the dump as-is                                             |
| `manual` | We store what you uploaded, as-is                                   |

<Callout type="warn">
  Nothing refuses to run without a key, and nothing warns you at run time. A backup configured
  without one quietly produces plain artifacts forever. Check `encrypted` on an artifact you
  care about rather than assuming.
</Callout>

Server-side encryption at the storage layer is not a substitute and we do not offer it as
one. It protects against a stolen disk and nothing else, because whoever can read the bucket
can read through it.

## Changing the key [#changing-the-key]

Set a new public key on the backup, or on the worker for a local backup.

* **Future artifacts** are encrypted to the new key.
* **Existing artifacts are untouched**, and still need the old private key.
* Each artifact's `key_id` says which one it needs.

**Never destroy an old private key while artifacts encrypted to it still exist.** Retention
tells you exactly when that is safe: once the last artifact carrying that `key_id` has
expired, the key is dead weight.

```bash
sctl artifact list --backup $BACKUP_ID
```

A rotation plan that works: add the new key, let the retention window pass, confirm no
artifact still lists the old `key_id`, then retire the old private key.

## Verifying you can actually decrypt [#verifying-you-can-actually-decrypt]

A backup you have never restored is a hypothesis, and an encryption key you have never used
is a worse one.

```bash
sctl restore file <artifact-id> --path ./restore-test.dump
```

Restore is client-side: the artifact is downloaded and decrypted **on your machine**, with
your private key. We are not in that path and could not be. If it works on your laptop with
your key, it will work during the incident.

Do this the day you configure the key, not the day you need it. See
[Rehearsal](/docs/recover/rehearsal).

## Next [#next]

<Cards>
  <Card href="/docs/security/encryption" title="Encryption design" description="The scheme itself, and what it does and does not defend." />

  <Card href="/docs/security/key-management" title="Key management" description="Where to keep the private half." />

  <Card href="/docs/recover/artifact-format" title="Artifact format" description="Opening one with plain gpg and no CLI." />
</Cards>
