This check flags S3 buckets without object versioning, which means an overwrite or delete is permanent and unrecoverable. Turn versioning on with aws s3api put-bucket-versioning, and pair it with lifecycle rules to control cost.
S3 versioning is one of those settings that costs nothing to enable and saves you on the worst day of your year. Without it, every PUT that reuses a key silently replaces the old object, and every DELETE wipes the data for good. There is no undo button. The Lensix check s3_versioning_disabled looks for buckets where this safety net is missing.
This post breaks down what the check catches, why an unversioned bucket is riskier than it looks, and how to fix and prevent it across the console, the CLI, and infrastructure as code.
What this check detects
The check inspects each S3 bucket in your account and reports any bucket whose versioning status is not Enabled. A bucket can be in one of three states:
- Disabled (the default for new buckets) — no version history is kept.
- Enabled — every object write creates a new version, and deletes insert a delete marker rather than destroying data.
- Suspended — versioning was once on but has been turned off, leaving existing versions in place while new writes overwrite the current version.
The check flags both Disabled and Suspended buckets, since neither protects you from accidental or malicious overwrites going forward.
Note: When versioning has never been enabled, the S3 API returns an empty versioning configuration rather than the string Disabled. That is normal and still means versioning is off.
Why it matters
An unversioned bucket has exactly one copy of each object at any given key. That single copy is exposed to a few failure modes that show up regularly in real incidents.
Accidental overwrites and deletes
A deploy script that uploads to the wrong key, a botched aws s3 sync --delete, or an engineer cleaning up the wrong prefix can erase data in seconds. With versioning off, that data is gone. With it on, the previous version is still sitting there and you restore it in a minute.
Ransomware and malicious deletion
S3 is a common target once an attacker gets hold of credentials with write access. A typical pattern is to overwrite objects with encrypted copies or delete them outright, then demand payment. Versioning blunts this attack because the original versions remain recoverable. Combined with MFA Delete and a bucket policy that denies DeleteObjectVersion, it makes the bucket much harder to wipe.
Warning: Versioning protects you from overwrites and deletes, but a determined attacker with broad permissions can still delete specific versions. Treat versioning as one layer, not a complete backup strategy.
Compliance and data retention
Frameworks like SOC 2, HIPAA, and PCI DSS expect you to be able to recover data and demonstrate integrity controls. Versioning, especially when paired with Object Lock, is often the simplest way to satisfy a "protect against destruction" control. It is also a prerequisite for several other S3 features, including cross-region replication, which many auditors expect for critical data.
How to fix it
Enabling versioning is a single API call and takes effect immediately for all future object operations. It does not retroactively create versions of objects that already exist, but it protects everything from that point forward.
Option 1: AWS CLI
aws s3api put-bucket-versioning \
--bucket my-critical-bucket \
--versioning-configuration Status=Enabled
Confirm the change:
aws s3api get-bucket-versioning --bucket my-critical-bucket
Expected output:
{
"Status": "Enabled"
}
Option 2: AWS Console
- Open the S3 console and select the bucket.
- Go to the Properties tab.
- Find Bucket Versioning and choose Edit.
- Select Enable, then Save changes.
Option 3: Terraform
resource "aws_s3_bucket" "data" {
bucket = "my-critical-bucket"
}
resource "aws_s3_bucket_versioning" "data" {
bucket = aws_s3_bucket.data.id
versioning_configuration {
status = "Enabled"
}
}
Option 4: CloudFormation
Resources:
DataBucket:
Type: AWS::S3::Bucket
Properties:
BucketName: my-critical-bucket
VersioningConfiguration:
Status: Enabled
Warning: Versioning keeps every version of every object, so storage costs grow with each overwrite. A bucket with frequently rewritten objects can balloon if you never expire old versions. Pair versioning with a lifecycle rule, covered below.
Control cost with a lifecycle rule
Set noncurrent versions to expire after a retention window that matches your recovery needs. This keeps you protected without paying to store every version forever.
{
"Rules": [
{
"ID": "expire-old-versions",
"Status": "Enabled",
"Filter": {},
"NoncurrentVersionExpiration": {
"NoncurrentDays": 90
},
"AbortIncompleteMultipartUpload": {
"DaysAfterInitiation": 7
}
}
]
}
aws s3api put-bucket-lifecycle-configuration \
--bucket my-critical-bucket \
--lifecycle-configuration file://lifecycle.json
Tip: For buckets holding compliance or backup data, consider transitioning noncurrent versions to a cheaper storage class like S3 Glacier Instant Retrieval before expiry, rather than deleting them. You keep the recovery option at a fraction of the cost.
How to prevent it from happening again
Fixing one bucket by hand does not stop the next unversioned bucket from being created. Bake the requirement into the places where buckets are born.
Enforce in CI/CD with policy-as-code
If you use Terraform, a Checkov or OPA policy can fail the pipeline when a bucket lacks a versioning block. Checkov ships with this rule out of the box (CKV_AWS_21):
checkov -d . --check CKV_AWS_21
Add it to your pipeline so a pull request that introduces an unversioned bucket never merges:
# .github/workflows/iac-scan.yml
name: IaC Security Scan
on: [pull_request]
jobs:
checkov:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Run Checkov
uses: bridgecrewio/checkov-action@v12
with:
directory: .
check: CKV_AWS_21
Guardrails with SCPs and Config rules
For runtime enforcement, AWS Config provides the managed rule s3-bucket-versioning-enabled. Pair it with an automatic remediation action so any noncompliant bucket gets versioning turned on without human intervention.
aws configservice put-config-rule --config-rule '{
"ConfigRuleName": "s3-bucket-versioning-enabled",
"Source": {
"Owner": "AWS",
"SourceIdentifier": "S3_BUCKET_VERSIONING_ENABLED"
}
}'
Tip: Lensix runs the s3_versioning_disabled check continuously across all your accounts and regions, so you catch buckets created outside of Terraform, like ones spun up by a console click or a third-party tool, that a pipeline scan would miss entirely.
Reusable Terraform module
Wrap your bucket definition in an internal module that always sets versioning, encryption, and public access blocking. Teams consume the module instead of writing raw aws_s3_bucket resources, which makes the secure path the easy path.
Best practices
- Enable versioning on every bucket holding data you cannot easily regenerate. Logs, backups, application uploads, and Terraform state are obvious candidates.
- Always pair versioning with a lifecycle policy. Unbounded version retention is the number one reason versioning gets blamed for surprise bills.
- Add MFA Delete for high-value buckets. It requires an MFA token to permanently delete a version or suspend versioning, which stops a single compromised credential from wiping history.
- Use Object Lock for true immutability. When you need write-once-read-many guarantees for compliance, Object Lock in compliance mode prevents deletion even by the root account for the retention period. Note that Object Lock must be enabled at bucket creation.
- Combine versioning with replication for critical data. Cross-region replication requires versioning and gives you a second copy that survives a regional outage or an account-level mistake.
- Do not treat versioning as a backup. It lives in the same bucket and account as the original. For real backups, replicate to a separate account with restricted access.
Danger: Suspending versioning does not delete existing versions, but it does mean new overwrites and deletes stop being recoverable. Never suspend versioning on a production data bucket to save costs. Use a lifecycle rule to expire old versions instead.
Versioning is cheap insurance. The fix is one command, the cost is controllable with a lifecycle rule, and the upside is the difference between a five-minute restore and an unrecoverable data loss. Turn it on everywhere it matters, then let policy-as-code and continuous checks keep it that way.

