Skip to main content
SecurityAugust 25, 20266 min read

How to Make an S3 Bucket That Cannot Be Deleted by Accident

Protecting a bucket against a mistake and protecting it against a stolen credential are two different jobs, and the settings that do one do not do the other. This walks through versioning, MFA delete and Object Lock in both of its modes, what each one can and cannot be undone by, and where an attacker with the right permission walks straight through your protection. Several of these settings cannot be reversed once enabled, including one where AWS says the only remaining way to delete the data is to close the account, so the warnings sit next to the commands.

Two different problems get treated as one. The first is somebody running the wrong command against the wrong bucket at the end of a long day. The second is a leaked key used deliberately to destroy your data before anyone notices. The controls that solve the first do very little about the second, and knowing which you are buying is most of the work.

Several of the settings below cannot be undone. That is the point of them, and also the reason to read the whole thing before typing any of it.

Versioning first, and it is a one-way door

Nothing else works without it. Object Lock requires it, replication requires it, and without it an overwrite is gone.

aws s3api put-bucket-versioning \
  --bucket amzn-s3-demo-bucket1 \
  --versioning-configuration Status=Enabled

Read this before running it. Once a bucket is version-enabled it can never return to an unversioned state. Suspending versioning stops new versions accruing, but the bucket stays versioned forever, and existing objects keep their null version ID until the next write.

The storage consequence surprises people. Every version is the entire object rather than a diff, so three versions of a file are three files. Pair versioning with a lifecycle rule that expires noncurrent versions, or the bucket grows without limit. If the bucket already had an object expiration rule, add a noncurrent version expiration rule alongside it to keep the old permanent-delete behaviour.

A delete is not always a delete

This is the mechanic everything else builds on. On a versioned bucket a DELETE without a version ID removes nothing. S3 inserts a delete marker, the marker becomes the current version, and the object comes back when the marker goes.

A DELETE naming a version ID is the real one, and it needs s3:DeleteObjectVersion rather than s3:DeleteObject. Splitting those two permissions is the cheapest protection here, and most applications only ever need the first.

MFA delete, and why almost nobody has it

MFA delete demands a second factor to permanently delete an object version or change the versioning state.

aws s3api put-bucket-versioning \
  --bucket amzn-s3-demo-bucket1 \
  --versioning-configuration Status=Enabled,MFADelete=Enabled \
  --mfa "arn:aws:iam::111122223333:mfa/root-account-mfa-device 123456"

Three constraints explain why you rarely see it. Only the account root user can enable it, it cannot be enabled through the console at all, and it cannot be combined with lifecycle configurations, which rules it out anywhere you also want automatic expiry. It fits a small number of genuinely critical buckets, and it does put real friction in front of a destructive action.

Object Lock, and the mode you cannot take back

Object Lock stores objects under a write-once-read-many model, for a fixed period or indefinitely. It works only on versioned buckets, and can be turned on at bucket creation or on a bucket you already have.

# on a new bucket
aws s3api create-bucket \
  --bucket amzn-s3-demo-bucket1 \
  --object-lock-enabled-for-bucket \
  --region eu-central-1 \
  --create-bucket-configuration LocationConstraint=eu-central-1

# on an existing versioned bucket, with a default retention rule
aws s3api put-object-lock-configuration \
  --bucket amzn-s3-demo-bucket1 \
  --object-lock-configuration '{"ObjectLockEnabled":"Enabled","Rule":{"DefaultRetention":{"Mode":"GOVERNANCE","Days":30}}}'

Warnings belong right next to those commands. Once Object Lock is enabled you cannot disable it, and you cannot suspend versioning on that bucket either. Such a bucket also cannot be a destination for server access logs.

The choice of mode is where the irreversibility gets serious.

Governance mode protects objects from most people while leaving a way out. Someone holding s3:BypassGovernanceRetention can override or remove the retention settings if the request carries the x-amz-bypass-governance-retention:true header, and the S3 console includes that header by default.

Compliance mode has no way out. A protected object version cannot be overwritten or deleted by any user, including the account root user, the mode cannot be changed, and the retention period cannot be shortened. AWS states that the only way to delete an object under compliance mode before its retention date expires is to delete the AWS account.

Read that once more before using compliance mode on production data. A mistyped retention of ten years is a bill you cannot cancel and data you cannot remove, and a request to erase personal data will arrive at some point and cannot be honoured. Test the settings in governance mode first, which is what AWS suggests it for. Retention can always be extended afterwards, since a later Retain Until Date replaces the current one.

Legal hold is the third form and the most flexible. No expiry, stays until removed, independent of any retention period, and anyone with s3:PutObjectLegalHold can place or lift it. That freedom is the trade.

One thing Object Lock does not do. It protects the version it was placed on, and does not prevent new versions or delete markers on top. A simple DELETE against a locked object still returns 200 OK and inserts a delete marker. The data is safe, but a naive listing looks empty, which is its own kind of incident.

The bucket itself

There is a floor under all of this. DeleteBucket requires every object, every object version and every delete marker to be gone first, so a bucket holding one locked object cannot be deleted at all. You can also deny the operation outright.

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "DenyBucketDeletion",
      "Effect": "Deny",
      "Principal": "*",
      "Action": ["s3:DeleteBucket", "s3:DeleteBucketPolicy"],
      "Resource": "arn:aws:s3:::amzn-s3-demo-bucket1"
    }
  ]
}

Which brings us to the honest limitation of all of it.

Where a stolen credential walks straight through

A bucket policy is data in the bucket's own configuration, so an identity with s3:PutBucketPolicy can replace it, deny statement and all. Versioning can be suspended by anyone with s3:PutBucketVersioning, and governance retention by anyone holding the bypass permission. Each guards well against a tired engineer and poorly against an attacker holding the credentials of someone who administers the bucket.

Three controls survive that, because they sit where the compromised credential does not reach. An organisation-level SCP denying s3:DeleteBucket, s3:PutBucketVersioning and s3:PutBucketPolicy cannot be lifted from inside the member account, whatever permissions the stolen identity has there. Compliance mode Object Lock holds even against the account root user, which is exactly what its irreversibility buys for backup and audit data. And replication into a separate account leaves a copy outside the blast radius, needing versioning on both sides and a destination whose access does not overlap with the source.

The order to roll this out

  1. Enable versioning, with a noncurrent version expiration lifecycle rule in the same change so storage does not grow forever.
  2. Split s3:DeleteObject from s3:DeleteObjectVersion in application policies, and grant applications only the first.
  3. Add a bucket policy denying s3:DeleteBucket and s3:DeleteBucketPolicy.
  4. Put the same denies in an SCP, which is the part a compromised credential in the account cannot undo.
  5. For backups and audit data only, enable Object Lock. Live with your retention settings in governance mode first, and move to compliance mode only once you are certain about the period.
  6. Replicate anything irreplaceable into a separate account.

Steps one to four are reversible and worth doing this week. Step five is not, which is why it comes last and after a trial.

If you would rather have this reviewed against your actual buckets and access patterns before anything irreversible gets switched on, that is a normal piece of our security and compliance work, and it pairs with our disaster recovery and backup service. Our guide to building a backup you have actually restored covers the other half of the same problem.

Talk to the engineer who will own your stack.

No account managers, no offshore handoff. Senior DevOps, direct. Tell us what you are dealing with and you get a straight answer.