Versioning protects your S3 objects, but without MFA Delete enabled, anyone with sufficient permissions (or a leaked key) can permanently delete versions and turn off versioning entirely. Enable MFA Delete on critical buckets using the root account and a hardware or virtual MFA device.
S3 versioning is one of those features that gives teams a false sense of safety. You flip it on, assume your data is now protected against accidental or malicious deletion, and move on. The problem is that versioning alone does not stop someone from deleting object versions or suspending versioning altogether. That second layer of protection is MFA Delete, and it is the part most teams skip.
This check, s3_nomfadelete in the s3_checks module, flags any S3 bucket where versioning is configured but MFA Delete has not been enabled.
What this check detects
The check inspects the versioning configuration of each S3 bucket in your account. A bucket can be in one of three versioning states:
- Disabled (the default) — no version history is kept
- Enabled — every object write creates a new version
- Suspended — versioning was on but is currently paused
When versioning is enabled or suspended, the configuration can also include an MFADelete field. The check fails when versioning is present but MFADelete is set to Disabled. In other words, you have version history, but no requirement for multi-factor authentication when someone tries to permanently delete a version or change the versioning state.
Note: MFA Delete adds a requirement that the request include a valid MFA code for two specific operations: permanently deleting a version of an object, and changing the versioning state of the bucket. Normal reads, writes, and "soft" deletes (which just add a delete marker) are unaffected.
Why it matters
Versioning without MFA Delete protects you against accidents, not against attackers. If someone gains access to credentials with s3:DeleteObjectVersion permissions, they can wipe your version history clean. The delete markers that versioning creates are easy to remove, and the underlying versions can be permanently deleted in bulk.
Consider a realistic ransomware-style scenario. An attacker compromises an IAM access key through a leaked .env file in a public repo. They enumerate your buckets, find one holding database backups with versioning enabled, and run a batch delete across every version. Without MFA Delete, nothing stops them. With MFA Delete, the delete calls fail because the request carries no MFA token, and the attacker cannot generate one without physical or virtual possession of your MFA device.
Warning: Versioning increases storage costs because every version of every object is retained and billed. Combine MFA Delete with lifecycle rules that expire non-current versions after a set period, otherwise costs on high-churn buckets can climb quickly.
The business impact lands hardest on buckets that hold backups, audit logs, compliance records, or terraform state. These are exactly the assets an attacker wants to destroy to cover their tracks or hold you to ransom, and they are exactly the buckets where unrecoverable deletion is most painful.
How to fix it
MFA Delete has an unusual requirement: it can only be enabled by the bucket owner using the AWS account root user credentials, and the request must include a valid MFA serial number and code. IAM users, even administrators, cannot enable it. This is by design, and it is also why MFA Delete is underused.
Danger: The following steps require root account credentials and a root MFA device. Using the root account programmatically is something you should do rarely and carefully. Create a root access key only for this operation, then delete it immediately afterward.
Step 1: Confirm you have a root MFA device
Sign in to the AWS console as the root user, open Security credentials, and confirm a multi-factor authentication device is registered. Note its ARN (for virtual devices) or serial number (for hardware devices). It looks like:
arn:aws:iam::123456789012:mfa/root-account-mfa-device
Step 2: Enable MFA Delete with the CLI
Using root credentials, run the following. The --mfa value combines the device ARN (or serial) and the current code from your MFA device, separated by a space:
aws s3api put-bucket-versioning \
--bucket my-critical-backups \
--versioning-configuration Status=Enabled,MFADelete=Enabled \
--mfa "arn:aws:iam::123456789012:mfa/root-account-mfa-device 123456"
Step 3: Verify the change
aws s3api get-bucket-versioning --bucket my-critical-backups
You should see both fields enabled:
{
"Status": "Enabled",
"MFADelete": "Enabled"
}
Tip: Once MFA Delete is on, generate the root access key, perform the operation, and immediately deactivate or delete that key under Security credentials. Long-lived root keys are a far bigger risk than the one you just mitigated.
A note on Terraform and IaC
This is the part that trips people up. Terraform's aws_s3_bucket_versioning resource exposes an mfa_delete argument, but the provider cannot actually enable it because the underlying API call must be signed with root credentials and an MFA token. You can declare the desired state for documentation and drift detection, but the enabling step remains a manual root operation:
resource "aws_s3_bucket_versioning" "backups" {
bucket = aws_s3_bucket.backups.id
versioning_configuration {
status = "Enabled"
mfa_delete = "Enabled"
}
}
If you apply this without first enabling MFA Delete manually, the apply will fail or report drift. Treat the IaC declaration as the source of truth and the root CLI command as the one-time bootstrap.
How to prevent it from happening again
Because MFA Delete cannot be enabled by automation, prevention is about detection rather than enforcement. You want to catch buckets that should have MFA Delete but do not, and surface them before they hold sensitive data.
- Run the Lensix check on a schedule so any new versioned bucket without MFA Delete is flagged within hours of creation.
- Tag buckets by sensitivity (for example
data-class=backup) and write a policy that requires MFA Delete on anything tagged as critical. - Gate Terraform plans in CI with a policy-as-code tool. The example below uses Open Policy Agent to fail any plan that creates a versioned bucket without declaring
mfa_delete = "Enabled".
package terraform.s3
deny[msg] {
resource := input.resource_changes[_]
resource.type == "aws_s3_bucket_versioning"
config := resource.change.after.versioning_configuration[_]
config.status == "Enabled"
config.mfa_delete != "Enabled"
msg := sprintf("Bucket versioning '%s' must declare mfa_delete = Enabled", [resource.address])
}
Tip: Pair this OPA rule with a CI step that posts a comment on the pull request explaining the manual root step. Engineers often do not know MFA Delete needs root credentials, and the reminder prevents a failed deploy.
Best practices
MFA Delete is a strong control, but it is one piece of a layered approach to protecting S3 data. Use it alongside the following:
- Reserve MFA Delete for genuinely critical buckets. Enabling it everywhere creates operational friction because every legitimate version cleanup now needs root and an MFA code. Apply it to backups, logs, and state files.
- Consider S3 Object Lock as an alternative or complement. Object Lock with a retention period or legal hold enforces write-once-read-many semantics and can be managed without root credentials. For new buckets, it is often the more practical choice for immutability.
- Lock down delete permissions. Even with MFA Delete, scope
s3:DeleteObjectands3:DeleteObjectVersiontightly in IAM and bucket policies. Defense in depth means an attacker should not reach the delete call in the first place. - Apply lifecycle policies to non-current versions. This controls the storage cost that versioning introduces while keeping a reasonable recovery window.
- Replicate critical buckets cross-account. For your most important data, S3 Replication into a separate, locked-down account gives you a copy an attacker in the primary account cannot touch.
Note: Object Lock and MFA Delete are not mutually exclusive, but Object Lock must be enabled at bucket creation time, while MFA Delete can be added later. If you are designing a new compliance bucket, evaluate Object Lock first.
The takeaway is simple: versioning gives you history, MFA Delete makes that history hard to destroy. If a bucket matters enough to version, decide deliberately whether it matters enough to protect with MFA Delete or Object Lock, then enforce that decision with continuous checks rather than hoping it stays correct.

