---
title: "Cloud backups"
description: "We connect to your source, with a credential you hand us for that purpose."
url: "https://saved.sh/docs/backups/cloud"
---

A cloud backup runs on our infrastructure. You give us a credential for the source, we
connect to it on the schedule, and we produce, encrypt and store the artifact.

## The trade, stated plainly [#the-trade-stated-plainly]

There is no process for you to run. In exchange, we hold a credential to your data.

|                                   | Local              | Cloud                          |
| --------------------------------- | ------------------ | ------------------------------ |
| Who connects                      | Your worker        | We do                          |
| Source credential                 | Your `config.yaml` | Our vault                      |
| Where plaintext exists            | Your host          | Our infrastructure             |
| Where encryption happens          | Your host          | Our infrastructure             |
| Runs while your machines are down | No                 | Yes                            |
| Reachability required             | Worker to source   | **Our network to your source** |

That last row is the one that decides most cases. A database on a private network with no
public ingress cannot be a cloud backup, and should not be made into one by opening it.

If you are choosing between the two, choose local when you can run a worker and the source is
sensitive, and cloud when the source is already reachable and you would rather not operate
anything.

## Where the credential lives [#where-the-credential-lives]

A cloud source's configuration is split the moment you save it, and the two halves land in
different stores.

| Half       | Stored in    | Returned by the API | Examples                                             |
| ---------- | ------------ | ------------------- | ---------------------------------------------------- |
| **Public** | Our database | Yes, as `source`    | host, port, database, bucket, url, region            |
| **Secret** | Vault        | **Never**           | password, access keys, request headers, TLS settings |

Two rules follow:

* **The API writes secrets and never reads them back.** No endpoint returns a secret it was
  given. Reading a backup shows you *which* database it connects to, not the password.
* **A secret sent in the wrong half is still routed to Vault.** The split is judged by the
  source type itself, not by which map you put a field in.

The dividing line is connection intent rather than sensitivity alone. TLS settings travel
with the password because they describe the same connection and are worthless apart from it.

<Callout>
  This is why an operator can audit which databases a workspace backs up without being handed
  the credentials to read them.
</Callout>

## Configuring one [#configuring-one]

```yaml title="saved.yaml"
backups:
  - name: prod-db
    kind: cloud
    source_type: postgres
    schedule: "0 2 * * *"
    source:
      host: db.example.com
      port: 5432
      database: app
      user: backup
    credentials:
      password: "<password>"
    encryption:
      public_key: ./keys/prod.asc
    retention:
      keep_last: 10
      expire_after: 90d
```

The definition is **validated when it is saved**, not when it fires. A source that cannot
run is refused at configure time rather than at 02:00 on a Sunday.

Credentials are plain values. `apply` runs on your machine against a file you control, so
keep that file out of the repository or render it at apply time. `encryption.public_key` is a
path, resolved relative to the manifest.

## Source types [#source-types]

| Type       | Fields                                                                                                              |
| ---------- | ------------------------------------------------------------------------------------------------------------------- |
| `postgres` | `host`, `port`, `database`, `user`, `schema`, `exclude_tables`, `no_owner`, `no_privileges`, `ssl_mode`, `password` |
| `mysql`    | `host`, `port`, `database`, `user`, `tables`, `exclude_tables`, `no_data`, `ssl_mode`, `password`                   |
| `redis`    | `host`, `port`, `db`, `user`, `tls`, `insecure`, `password`                                                         |
| `s3`       | `bucket`, `prefix`, `region`, `endpoint`, `use_path_style`, `access_key_id`, `secret_access_key`                    |
| `web`      | `url`, `method`, `headers`                                                                                          |

Every field of a source goes to the vault together, and none of it is returned by any
endpoint. There is no half we keep back and show you: a hostname describes how to reach
your database, so it travels with the password rather than being treated as harmless.

`script`, `file` and `folder` are never available as cloud backups. We do not execute
customer commands in our cloud, and your filesystem does not exist there. Submitting one is
refused, naming the type.

## Reachability [#reachability]

Our side has to be able to open a connection to your source. In practice that means:

* A publicly resolvable hostname, or one reachable from our network.
* A firewall or security group that accepts the connection.
* TLS you are willing to terminate against a client that is not on your network.

Prefer a **least-privileged role** scoped to reading what the backup takes, and prefer TLS
with verification (`ssl_mode: verify-full` on Postgres) over trusting the transport.

<Callout type="warn">
  Do not open a database to the internet to make a cloud backup possible. If the source is not
  already reachable, that is the signal to run a [local backup](/docs/backups/local) instead.
</Callout>

## Rotating a credential [#rotating-a-credential]

Reconfigure the backup with the new secret. The next run uses it; runs already in flight
finish on the old one.

```bash
sctl backup configure <backup-id> --credential password=<new-password>
```

Revoking the old credential at the source is your step, and worth doing only after a run has
succeeded on the new one.

Existing artifacts are unaffected: they are already encrypted and stored, and the source
credential has nothing to do with reading them back.

## Next [#next]

<Cards>
  <Card href="/docs/backups/sources" title="Sources" description="Every field, per source type." />

  <Card href="/docs/backups/local" title="Local backups" description="The alternative, and when it is the better one." />

  <Card href="/docs/security/data-flow" title="Data flow" description="Where a cloud backup's bytes actually go." />
</Cards>
