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
- Enable versioning, with a noncurrent version expiration lifecycle rule in the same change so storage does not grow forever.
- Split
s3:DeleteObjectfroms3:DeleteObjectVersionin application policies, and grant applications only the first. - Add a bucket policy denying
s3:DeleteBucketands3:DeleteBucketPolicy. - Put the same denies in an SCP, which is the part a compromised credential in the account cannot undo.
- 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.
- 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.
Related Articles
How to Detect and Respond to a Compromised Linux Server
A practical incident response guide for Linux servers: identifying signs of compromise, initial triage, evidence preservation, containment, rootkit detection, and writing an incident report.
SecurityEleven npm Packages Compromised in a 53 Minute Attack That Steals Every Credential Your Build Host Can Reach
On August 4, 2026 a worm pushed malicious versions of eleven npm caching packages inside a 53 minute window, harvesting npm tokens, GitHub PATs, AWS credentials, Kubernetes service account tokens and SSH keys. The headline was keyv and its 604 million monthly downloads, but keyv was the safest package on the list: its malicious release was a major version bump that no caret range accepts. The other ten were patch bumps, silently eligible for every dependency range in the ecosystem. That distinction, not the download count, decided who got hit. This is a practical guide to the defenses that actually change the outcome: what your semver range really grants, why npm install and npm ci are not interchangeable, when to disable install scripts and what breaks when you do, and how to check a tree you already have.
SecurityAWS WAF Configuration for Web Application Security
Deploy and configure AWS WAF with managed rule groups, custom rules, rate limiting, and bot control to protect web applications from common threats.