VPS Snaps

How to set up an Amazon S3 bucket for server backups

A backup bucket on Amazon S3 should be private, encrypted, versioned and able to delete old copies by itself. Create it in a Region away from your servers with the default Block Public Access and SSE-S3 encryption, give each server an IAM identity limited to one prefix, and add a lifecycle rule that expires old backups, old versions and unfinished uploads. Then upload with aws s3 cp or aws s3 sync and prove a download matches.

10 min readUpdated Checked against official documentation

Create the bucket

Plan for one bucket with one prefix per server, such as web-01/. A bucket's name and Region are permanent.

  1. Open the S3 console and choose the Region in the navigation bar, ideally one where your servers don't run.
  2. Choose General purpose buckets, then Create bucket.
  3. Enter a Bucket name: 3 to 63 lowercase letters, numbers and hyphens, starting and ending with a letter or number. Periods are allowed, but AWS recommends avoiding them. The name shows in object URLs, so keep it free of anything sensitive.
  4. Under Object Ownership, keep Bucket owner enforced, so ACLs stay off and policies alone control access.
  5. Under Block Public Access settings for this bucket, keep all four settings on. They are on by default.
  6. Under Bucket Versioning, choose Enable (see below).
  7. Under Default encryption, keep SSE-S3. It costs nothing. SSE-KMS adds AWS KMS charges and request quotas.
  8. Leave Object Lock under Advanced settings off unless you have read the Object Lock section below. Once on, it can't be turned off.
  9. Choose Create bucket.

The same bucket from the CLI, run with admin credentials:

Terminal
aws s3api create-bucket --bucket acme-server-backups --region eu-central-1 --create-bucket-configuration LocationConstraint=eu-central-1
  • --region and LocationConstraint must name the same Region. For us-east-1, drop --create-bucket-configuration; every other Region needs it.
  • New buckets get Block Public Access, disabled ACLs and SSE-S3 without extra flags.
Terminal
aws s3api put-bucket-versioning --bucket acme-server-backups --versioning-configuration Status=Enabled
Terminal
aws s3api get-public-access-block --bucket acme-server-backups

The second command should show BlockPublicAcls, IgnorePublicAcls, BlockPublicPolicy and RestrictPublicBuckets all true. AWS recommends waiting 15 minutes after first enabling versioning before writing objects.

Give the server a least-privilege credential

Never put root or admin keys on a server. On EC2, attach an IAM role to the instance through an instance profile, and programs on it get temporary credentials. Elsewhere, use an IAM user with an access key, or IAM Roles Anywhere, which AWS suggests for machines outside AWS. Either way, attach this policy:

backup-policy.json
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "ListOwnPrefix",
      "Effect": "Allow",
      "Action": "s3:ListBucket",
      "Resource": "arn:aws:s3:::acme-server-backups",
      "Condition": { "StringLike": { "s3:prefix": ["web-01/*"] } }
    },
    {
      "Sid": "ReadWriteOwnPrefix",
      "Effect": "Allow",
      "Action": [
        "s3:PutObject",
        "s3:GetObject",
        "s3:DeleteObject",
        "s3:AbortMultipartUpload"
      ],
      "Resource": "arn:aws:s3:::acme-server-backups/web-01/*"
    }
  ]
}
  • s3:ListBucket is a bucket action, so its Resource is the bucket ARN without /*. The s3:prefix condition allows listings under web-01/ only, which is what aws s3 ls and aws s3 sync request for that path.
  • s3:PutObject uploads, including each part of a multipart upload. s3:GetObject downloads, for restores and checks.
  • s3:DeleteObject lets your tool prune old backups. In a versioned bucket it only adds a delete marker, because the policy doesn't grant s3:DeleteObjectVersion. Drop it if lifecycle rules do the pruning.
  • s3:AbortMultipartUpload lets the CLI clean up a failed large upload.
  • Object actions take the object ARN, ending in /*. Give each server its own user and prefix.

In the IAM console, create a policy from this JSON and attach it to a new user without console access. On the user's Security credentials tab, choose Create access key. Then, on the server:

Terminal
aws configure --profile backups

The secret access key is shown once, and the CLI stores it in plain text in ~/.aws/credentials. Keep that file readable only by the user that runs backups, with chmod 600.

Versioning: what it buys you

With versioning on, an overwrite keeps the old object as a noncurrent version, and a delete without a version ID only adds a delete marker. A buggy script or a stolen key with the policy above can hide backups but not destroy them. Each version is billed as a full object, and versioning can later be suspended but never removed, so pair it with the noncurrent-version rule below.

Expire old backups with a lifecycle rule

lifecycle.json
{
  "Rules": [
    {
      "ID": "expire-server-backups",
      "Filter": { "Prefix": "" },
      "Status": "Enabled",
      "Expiration": { "Days": 30 },
      "NoncurrentVersionExpiration": { "NoncurrentDays": 7 },
      "AbortIncompleteMultipartUpload": { "DaysAfterInitiation": 7 }
    }
  ]
}
  • "Prefix": "" applies the rule to the whole bucket. Use web-01/ to target one server.
  • Expiration Days: 30: in a versioned bucket, S3 adds a delete marker 30 days after upload, and the backup becomes noncurrent.
  • NoncurrentVersionExpiration NoncurrentDays: 7: permanently deletes a version 7 days after it became noncurrent. A backup lives about 37 days, and an accidental delete can be undone for 7. Add NewerNoncurrentVersions (1 to 100) to also keep that many newer old versions.
  • AbortIncompleteMultipartUpload: removes the parts of uploads that never finished. You pay for those parts until they go.
  • With Days set, S3 also removes leftover delete markers on its own.
Terminal
aws s3api put-bucket-lifecycle-configuration --bucket acme-server-backups --lifecycle-configuration file://lifecycle.json

This command replaces the bucket's entire lifecycle configuration, so the file must contain every rule you want. Rules also apply to objects already in the bucket: a new 30-day rule queues everything older than 30 days for removal.

Removal is asynchronous, but storage isn't billed after the expiration date. Run this with admin credentials; the backup user can't change lifecycle rules, by design.

Choose a storage class

Class (API value)Minimum durationMinimum billable sizeGetting data back
S3 Standard (STANDARD)NoneNoneImmediate, no retrieval fee
Standard-IA (STANDARD_IA)30 days128 KBImmediate, per-GB retrieval fee
One Zone-IA (ONEZONE_IA)30 days128 KBImmediate, per-GB fee, one Availability Zone
Glacier Instant Retrieval (GLACIER_IR)90 days128 KBMilliseconds, per-GB fee
Glacier Flexible Retrieval (GLACIER)90 days40 KB metadata added per objectRestore first: minutes to hours
Glacier Deep Archive (DEEP_ARCHIVE)180 days40 KB metadata added per objectRestore first: hours

Deleting an object before its minimum duration bills the rest of it, so a 7-day backup in Standard-IA is charged for 30 days. Use Standard for short retention and Standard-IA for copies kept 30 days or more and rarely restored. Glacier Flexible Retrieval and Deep Archive need a restore request before download, which slows a recovery. AWS recommends One Zone-IA only for data you can recreate.

Terminal
aws s3 cp web-01-2026-10-03.tar.gz s3://acme-server-backups/web-01/2026-10-03.tar.gz --storage-class STANDARD_IA --profile backups

Lock backups with Object Lock

Object Lock stops object versions from being deleted or overwritten until a date. It needs versioning, and you can turn it on when creating the bucket or later from the bucket's Properties tab. After that, Object Lock can't be disabled and versioning can't be suspended.

  • Governance mode: only a user with s3:BypassGovernanceRetention who sends the bypass header can delete locked versions. Enough to stop a stolen server key.
  • Compliance mode: nobody can delete a locked version or shorten its lock, root included. AWS says the only early exit is deleting the account.
  • Legal hold: a lock with no end date, removed by hand.
Terminal
aws s3api put-object-lock-configuration --bucket acme-server-backups --object-lock-configuration '{"ObjectLockEnabled": "Enabled", "Rule": {"DefaultRetention": {"Mode": "GOVERNANCE", "Days": 14}}}'

This sets a default retention, so every new version is locked for 14 days without changing your upload commands. Lifecycle rules don't remove noncurrent versions that have Object Lock applied, so keep the lock shorter than your expiration window.

Start with governance mode and a short period. A compliance-mode lock set too long can't be undone, and you pay for that storage until it ends.

Upload backups with the AWS CLI

Give every backup a unique, dated key so nothing overwrites it. Make the archive first, for example with tar, then copy it:

Terminal
aws s3 cp /var/backups/web-01-2026-10-03.tar.gz s3://acme-server-backups/web-01/2026-10-03.tar.gz --profile backups --only-show-errors

--only-show-errors keeps cron logs short. A single PUT tops out at 5 GB; the aws s3 commands switch to multipart for large files, up to 48.8 TiB per object. To stream an archive without a temporary file, use - as the source:

Terminal
tar -czf - /etc /var/www | aws s3 cp - s3://acme-server-backups/web-01/2026-10-03-files.tar.gz --profile backups

For a stream over 50 GB, add --expected-size with the size in bytes, or the upload can fail at the 10,000-part limit. To mirror a local backup directory instead:

Terminal
aws s3 sync /var/backups/web-01/ s3://acme-server-backups/web-01/ --profile backups --only-show-errors

sync copies files that are new, changed in size, or newer locally. It deletes nothing unless you add --delete; leave that off, or a wiped local directory empties the remote copy too. Schedule either command with cron.

Verify a backup

Terminal
aws s3 ls s3://acme-server-backups/web-01/ --recursive --human-readable --summarize --profile backups
Terminal
aws s3api head-object --bucket acme-server-backups --key web-01/2026-10-03.tar.gz --checksum-mode ENABLED --profile backups

head-object returns ContentLength and, with --checksum-mode ENABLED, the stored checksum. S3 adds a CRC-64/NVME checksum when an upload doesn't specify one. Don't compare the ETag with an MD5 sum: for multipart uploads it isn't one. The real proof is a download:

Terminal
aws s3 cp s3://acme-server-backups/web-01/2026-10-03.tar.gz /tmp/restore-test.tar.gz --profile backups && sha256sum /tmp/restore-test.tar.gz && tar -tzf /tmp/restore-test.tar.gz > /dev/null && echo OK

Compare the hash with one recorded before upload. To recover an overwritten or deleted backup, find it with aws s3api list-object-versions --bucket acme-server-backups --prefix web-01/ and fetch it with aws s3api get-object and --version-id. Listing versions needs s3:ListBucketVersions, so use admin credentials. More in testing a restore.

What it costs

Rates vary by Region and class; check the S3 pricing page for current numbers. As of October 2026, you pay for:

  • Storage per GB-month, by storage class and Region. Every version and every unfinished multipart part counts.
  • Requests, priced by type. DELETE and CANCEL requests are free.
  • Retrieval fees per GB for the IA and Glacier classes, plus minimum-duration charges.
  • Data in from the internet is free. Data out to the internet is billed per GB, after the first 100 GB a month, which is shared across all AWS services.

Egress is the cost people forget: a full restore to a server outside AWS pays per GB out.

Limits and gotchas

  • An account can create 10,000 general purpose buckets by default.
  • Multipart parts are 5 MiB to 5 GiB, with at most 10,000 parts per upload.
  • Lifecycle transitions skip objects under 128 KB by default.
  • An admin in the same AWS account can delete any version not under a compliance-mode lock. For a copy that survives an account takeover, keep one in a separate account or at a second provider, per the 3-2-1 rule.

A key that can upload can usually delete too. For backups an attacker cannot remove, see protecting backups from ransomware; to keep what is stored readable only by you, encrypt it before upload.

Frequently asked questions

Is a new S3 bucket private by default?
Yes. New buckets have all four Block Public Access settings on and ACLs disabled, so nothing is public unless you change those settings.
Do I need to turn on encryption for an S3 backup bucket?
No. Since January 5, 2023, S3 encrypts all new uploads with SSE-S3 at no cost. Choose SSE-KMS only if you need to control the keys, and expect KMS charges.
Which S3 storage class is best for backups?
Standard for backups kept less than 30 days. Standard-IA for copies kept 30 days or more and rarely restored, Glacier Instant Retrieval for 90 days or more. Glacier Flexible Retrieval or Deep Archive only for archives you can wait hours to restore.
Can I enable S3 Object Lock on an existing bucket?
Yes. It requires versioning, and once enabled, Object Lock can't be disabled and versioning can't be suspended.
Does S3 versioning cost extra?
Each version is billed as a full object. A NoncurrentVersionExpiration lifecycle rule deletes old versions after a set number of days.

How this was checked

Commands, limits and prices were checked against these official pages, on October 3, 2026: