Docs / Storage and retention

Storage and retention

SafeGrd writes ciphertext into storage you own, or, where the plan includes it, into SafeGrd's own locked bucket (hosted storage). Either way the backup is encrypted on your host first, and who can decrypt it depends on who holds the key (Security and key custody). A bucket of your own has no per-gigabyte surcharge, and it stays in your account.

Sinks

SinkNotes
hostedSafeGrd's own bucket under compliance-mode Object Lock, with your plan's included storage. No bucket to set up. See Hosted storage.
localA directory on the host, with a filesystem-level lock standing in for Object Lock. Right for trying the product and for air-gapped hosts; it does not survive the machine.
AWS S3The reference target. A compliance-mode lock on AWS cannot be shortened or removed by anyone, and SafeGrd's retention depends on that.
MinIOSelf-hosted and S3-compatible, with real Object Lock. Set endpoint.
Cloudflare R2S3-compatible; set endpoint. Check Object Lock support for your bucket before relying on it.
Backblaze B2, WasabiS3-compatible endpoints with object lock features of their own; verify with safegrd doctor.
DigitalOcean SpacesS3-compatible but has no Object Lock at all. It requires the explicit opt-out below.

Configure a sink once in config.yaml, or override it per run with --s3-bucket, --s3-endpoint, --s3-region and --s3-prefix. A project can also carry a sink centrally, so a fleet of hosts inherits one destination; the per-run flags win over the project sink, which wins over the file.

Hosted storage

With storage.type: "hosted" the host needs no bucket and never holds a storage credential. For every part it uploads, and every object it reads, it asks the remote server for a URL signed for exactly that request: this part, of this many bytes, or this version of this snapshot. The bytes go straight to SafeGrd's bucket; a URL cannot delete, cannot be used for another object, and expires in minutes. Snapshots there are locked in compliance mode by the server, like in a bucket of your own, and still encrypted on your host.

safegrd init --storage hosted
safegrd enroll --token <your access token>
safegrd backup

Setting up an AWS S3 bucket

  1. Create the bucket with Object Lock enabled. It can only be enabled when the bucket is created; a bucket made without it has to be replaced, not converted.
    aws s3api create-bucket --bucket acme-safegrd --object-lock-enabled-for-bucket
    Outside us-east-1, add --create-bucket-configuration LocationConstraint=eu-west-1 (your region).
  2. Create an access key for each backup host with the policy below, and one read-only key for recovery.
  3. Run safegrd doctor on the host. It checks that the bucket answers and that Object Lock is on. The first safegrd backup proves the key can write.

What a backup host needs. These are exactly the S3 calls the CLI makes:

{"Version": "2012-10-17", "Statement": [
  {"Effect": "Allow", "Action": ["s3:ListBucket", "s3:ListBucketVersions", "s3:GetBucketObjectLockConfiguration"],
   "Resource": "arn:aws:s3:::acme-safegrd"},
  {"Effect": "Allow", "Action": ["s3:PutObject", "s3:PutObjectRetention", "s3:AbortMultipartUpload", "s3:GetObject", "s3:GetObjectVersion"],
   "Resource": "arn:aws:s3:::acme-safegrd/*"}
]}

That policy cannot delete anything. s3:AbortMultipartUpload only abandons an upload that failed halfway, so its parts are not billed; it cannot touch a finished object. A compromised host cannot even hide a backup behind a delete marker. The only SafeGrd command that deletes is prune, which you opt into (below).

The console's setup wizard writes this policy for your bucket's name, for AWS, Wasabi and MinIO, and the equivalent B2 key command. For AWS it also gives a CloudFormation template and a Terraform file that create the bucket with Object Lock and versioning on, with public access blocked. The same text is at GET /api/v1/storage/bucket-setup?provider=aws&bucket=acme-safegrd.

A recovery machine needs to read and list, nothing more:

{"Version": "2012-10-17", "Statement": [
  {"Effect": "Allow", "Action": ["s3:ListBucket", "s3:ListBucketVersions"], "Resource": "arn:aws:s3:::acme-safegrd"},
  {"Effect": "Allow", "Action": ["s3:GetObject", "s3:GetObjectVersion"], "Resource": "arn:aws:s3:::acme-safegrd/*"}
]}

The recovery runbooks use exactly this key.

Cloudflare R2

R2 is Cloudflare's S3-compatible store with no egress fees.

  1. Create an R2 bucket in the Cloudflare dashboard.
  2. Under Manage R2 API Tokens, create an API token with Object Read & Write access for the bucket.
  3. Set AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY in your environment.
  4. Point the endpoint at your account URL:
storage:
  type: "s3"
  bucket: "acme-safegrd"
  region: "auto"
  endpoint: "https://<ACCOUNT_ID>.r2.cloudflarestorage.com"

Check connectivity with safegrd doctor before running the first backup.

MinIO (Self-hosted WORM)

MinIO supports real S3 Object Lock when running in multi-drive or distributed erasure-code mode.

  1. Create a bucket with object lock enabled using mc:
    mc mb --with-lock myminio/safegrd-backups
  2. Set default retention:
    mc retention set --default COMPLIANCE 30d myminio/safegrd-backups
  3. Add the sink to config.yaml:
storage:
  type: "s3"
  bucket: "safegrd-backups"
  region: "us-east-1"
  endpoint: "http://minio.internal:9000"
  worm_mode: "COMPLIANCE"

safegrd doctor calls GetBucketObjectLockConfiguration to verify the lock mode.

Backblaze B2

B2 supports Object Lock, but File Lock must be enabled when creating the bucket. It cannot be added later.

  1. Create a B2 bucket with File Lock (Object Lock) enabled.
  2. In App Keys, generate an Application Key restricted to that bucket with Read and Write permissions.
  3. Find your region endpoint (for example, s3.us-east-005.backblazeb2.com).
  4. Configure the sink:
storage:
  type: "s3"
  bucket: "acme-b2-safegrd"
  region: "us-east-005"
  endpoint: "https://s3.us-east-005.backblazeb2.com"
  worm_mode: "COMPLIANCE"

Wasabi

Wasabi supports S3 Object Lock in compliance mode with no egress fees.

  1. Create a Wasabi bucket with Enable Object Lock turned on in Compliance Mode.
  2. Create an Access Key and Secret Key with read and write permissions on that bucket.
  3. Set the endpoint for your region:
storage:
  type: "s3"
  bucket: "acme-wasabi-safegrd"
  region: "eu-central-1"
  endpoint: "https://s3.eu-central-1.wasabisys.com"
  worm_mode: "COMPLIANCE"

DigitalOcean Spaces, and any bucket without Object Lock

Spaces has no Object Lock. SafeGrd works there, and the snapshots are still encrypted to your key and still proven by Fire Drills, but nothing stops them being deleted. Anyone holding a key with delete permission on the Space can remove them. Say so in the config, deliberately:

storage:
  type: "s3"
  bucket: "acme-safegrd"
  region: "nyc3"
  endpoint: "https://nyc3.digitaloceanspaces.com"
  worm_mode: "NONE"

safegrd init writes the same from flags:

safegrd init --storage s3 --s3-bucket acme-safegrd --s3-region nyc3 --s3-endpoint https://nyc3.digitaloceanspaces.com --worm-mode NONE

The Space's key comes from AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY in the host's environment, or from access_key_id and secret_access_key under storage.

Without worm_mode: "NONE" a backup to Spaces fails at startup rather than pretending to lock. With it, the console marks each snapshot Not locked: deletable, and the CLI never prints an immutability date. Keep the Space's keys few and write-only where you can, and consider copying to a bucket that does lock.

What WORM retention actually means

--retention-days sets an S3 Object Lock retention period on each object as it is written. Under the default, compliance mode, nobody can delete or overwrite the object until retention expires: not the account's root user, and not SafeGrd. Stolen administrator credentials cannot remove a locked backup.

Retention locks cannot be shortened or undone.

A compliance lock cannot be shortened or cancelled once it is set. A long retention on hourly snapshots builds up storage you pay for and cannot delete until the date passes. Try a short retention_days on a small dataset first.

worm_mode: "GOVERNANCE" is the softer variant, where a principal holding the bypass permission can shorten a lock. It protects against accidents, but an attacker with that permission can remove the backup.

Keeping daily, weekly and monthly backups longer

The daemon can keep some backups longer than others, grandfather-father-son style, set per surface or under defaults:

retention_days: 2 # every backup
keep_daily: 14 # the first backup of each UTC day, for 14 days
keep_weekly: 4 # the first backup of each ISO week, for 4 weeks
keep_monthly: 12 # the first backup of each month, for 12 months

Sinks with no Object Lock

worm_mode: "NONE" is for storage with no Object Lock, such as DigitalOcean Spaces. SafeGrd never falls back to unlocked storage on its own: a bucket without Object Lock and without this setting fails at startup. With NONE, snapshots are written unlocked. The value is case-sensitive: COMPLIANCE, GOVERNANCE or NONE.

Listing and expiry

# what is in the bucket, and until when it is locked
safegrd list
# sink reachability, lock configuration, key presence
safegrd status

In your own bucket, SafeGrd deletes nothing unless you turn pruning on. (Hosted storage prunes a snapshot once its lock ends.) An expired lock only allows deletion. The snapshot stays until something prunes it.

To let expired snapshots go, set storage.expire_after_lock: true and the daemon prunes once a day, or run safegrd prune (with --dry-run first). It deletes a snapshot only after its Object Lock ended more than a day ago, judged by the bucket's own clock and the lock the bucket itself reports, rather than the host's clock. It keeps the newest three snapshots of each surface whatever their locks say (--min-keep), so a short retention never empties a surface, and retains the last known-good snapshot while Threat Shield reports an open anomaly. If the remote server is unreachable, nothing is pruned. Pruning requires s3:DeleteObjectVersion, s3:GetObjectRetention and s3:GetObjectLegalHold on the host key (the wizard adds these when expiry is enabled). With them the key can delete expired snapshots, and still cannot touch a locked one. Use this rather than a bucket lifecycle rule, which cannot tell the daily, weekly and monthly tiers apart. Local sinks are never pruned.

Keep one snapshot longer

A lock can be extended, which is the one change Object Lock allows. Keep a snapshot longer before a migration, or because an auditor asked for it, up to 13 months from today. Nobody can shorten the lock afterwards, SafeGrd included.

The backup file, its metadata and its recovery document are extended together. An incremental snapshot shares its chunks with the rest of its month, so it is kept as long as that month and not on its own.

When a project changes buckets

Moving a project to another bucket, or to or from hosted storage, or deleting an organization, leaves the backups already written where they are. SafeGrd writes down what stays: the key access log gets a sink_released entry naming every snapshot it recorded in the place being left, and the console says the same when you make the change. In your own bucket, your lifecycle rule or safegrd prune removes them once their locks end. Hosted storage removes each one when its lock ends.

A recovery document beside every snapshot

Each snapshot is written with two files beside it: SNAPSHOT.meta.json, the manifest, and SNAPSHOT.RECOVERY.md, a plain-text page that says where the snapshot is (bucket, endpoint, prefix), what a restore needs (the server version, the extensions and the roles to have ready), and the commands that restore it, with safegrd restore and, for PostgreSQL, with psql alone. It is written for a reader who has the bucket and the key and nothing else, so it is not encrypted and it holds no secret: no credential, no key, no connection string.

A full backup's two files sit next to its archive. An incremental snapshot's sit in the snapshots/ folder of its month's repository, repo/<surface>/<epoch>/snapshots/. On hosted storage, download them from the snapshot's details in the console.

recovery_doc_sealed: true on a surface seals it with the surface's recipient instead, as SNAPSHOT.RECOVERY.md.age. Both are locked like the manifest. A recovery document that could not be written is said on stderr; the backup still succeeds.

Someone deleted my backups

In a versioned bucket with Object Lock on, an ordinary delete cannot remove a locked object. S3 adds a delete marker on top of it instead, which hides the snapshot from normal listings and leaves its data where it was.

# make one snapshot visible again
safegrd undelete --snapshot snap-1b094c0af8
# clear every delete marker in the sink
safegrd undelete --all

safegrd undelete --all rolls a versioned bucket back to before the deletion: every snapshot and sidecar is visible again once its delete marker is gone, and nothing is re-uploaded, so the rollback takes seconds whatever the bucket holds.

The command only ever removes delete markers, addressed by version id. It cannot remove a version holding data, which is why it is safe to run twice. If it finds nothing to remove it says which of two reasons applies: the snapshot is there and was never hidden, or the id is not in that bucket at all. In the second case it names the bucket, prefix and node it searched and exits non-zero. Removing a delete marker needs s3:DeleteObjectVersion, which a backup host's key does not have, so run it with an administrator's key. Afterwards, deny s3:DeleteObject to the credential that did it.