Skip to content

S3 buckets with versioning enabled should have lifecycle policies configured

Versioning protects against accidental deletion and overwrites, but every write creates a new object version that S3 stores forever unless a lifecycle rule removes it. A bucket receiving frequent writes can accumulate millions of noncurrent versions within weeks, with no upper bound on cost.

Lifecycle rules let you transition noncurrent versions to cheaper storage classes like S3 Glacier or expire them after a defined retention period. Without them, you pay S3 Standard pricing for data you will almost certainly never read again, and you have no mechanism to demonstrate a defensible retention posture when auditors ask.

Retrofit consideration

Existing buckets may be sitting on large volumes of noncurrent versions. Adding expiration rules will trigger bulk deletions that can spike S3 request costs and generate a significant volume of CloudTrail events. Test on a non-production bucket first, and consider starting NoncurrentVersionExpiration with a conservative NoncurrentDays value rather than an aggressive cutoff.

Implementation

Choose the approach that matches how you manage Terraform.

Use the compliance.tf module to enforce this control by default. See get started with compliance.tf.

module "s3_bucket" {
  source  = "soc2.compliance.tf/terraform-aws-modules/s3-bucket/aws"
  version = ">=5.0.0"

  bucket = "abc123"
}

module "s3_bucket" {
  source  = "pcidss.compliance.tf/terraform-aws-modules/s3-bucket/aws"
  version = ">=5.0.0"

  bucket = "abc123"
}

module "s3_bucket" {
  source  = "nist80053.compliance.tf/terraform-aws-modules/s3-bucket/aws"
  version = ">=5.0.0"

  bucket = "abc123"
}

module "s3_bucket" {
  source  = "nistcsf.compliance.tf/terraform-aws-modules/s3-bucket/aws"
  version = ">=5.0.0"

  bucket = "abc123"
}

module "s3_bucket" {
  source  = "fedrampmoderate.compliance.tf/terraform-aws-modules/s3-bucket/aws"
  version = ">=5.0.0"

  bucket = "abc123"
}

module "s3_bucket" {
  source  = "cisv80ig1.compliance.tf/terraform-aws-modules/s3-bucket/aws"
  version = ">=5.0.0"

  bucket = "abc123"
}

module "s3_bucket" {
  source  = "awswellarchitected.compliance.tf/terraform-aws-modules/s3-bucket/aws"
  version = ">=5.0.0"

  bucket = "abc123"
}

module "s3_bucket" {
  source  = "cccsmedium.compliance.tf/terraform-aws-modules/s3-bucket/aws"
  version = ">=5.0.0"

  bucket = "abc123"
}

module "s3_bucket" {
  source  = "eugmpannex11.compliance.tf/terraform-aws-modules/s3-bucket/aws"
  version = ">=5.0.0"

  bucket = "abc123"
}

module "s3_bucket" {
  source  = "cfrpart11.compliance.tf/terraform-aws-modules/s3-bucket/aws"
  version = ">=5.0.0"

  bucket = "abc123"
}

module "s3_bucket" {
  source  = "hipaasecurity2003.compliance.tf/terraform-aws-modules/s3-bucket/aws"
  version = ">=5.0.0"

  bucket = "abc123"
}

module "s3_bucket" {
  source  = "nistcsfv11.compliance.tf/terraform-aws-modules/s3-bucket/aws"
  version = ">=5.0.0"

  bucket = "abc123"
}

module "s3_bucket" {
  source  = "pcidssv321.compliance.tf/terraform-aws-modules/s3-bucket/aws"
  version = ">=5.0.0"

  bucket = "abc123"
}

If you use terraform-aws-modules/s3-bucket/aws, set the right module inputs for this control. You can later migrate to the compliance.tf module with minimal changes because it is compatible by design.

module "s3_bucket" {
  source  = "terraform-aws-modules/s3-bucket/aws"
  version = ">=5.0.0"

  bucket = "abc123"

  lifecycle_rule = {
    [0] = {
      status = "Enabled"
    }
  }
}

Use AWS provider resources directly. See docs for the resources involved: aws_s3_bucket_lifecycle_configuration.

resource "aws_s3_bucket" "this" {
  bucket        = "pofix-abc123"
  force_destroy = true
}

resource "aws_s3_bucket_lifecycle_configuration" "this" {
  bucket = "example-bucket-abc123"
  rule {
    expiration {
      days = 365
    }

    filter {
    }

    id     = "lifecycle-rule"
    status = "Enabled"
  }
}

What this control checks

To pass, any S3 bucket with versioning enabled via aws_s3_bucket_versioning (with versioning_configuration.status = "Enabled") must also have an associated aws_s3_bucket_lifecycle_configuration. That resource must reference the bucket via its bucket argument and contain at least one rule block with status = "Enabled". Valid rule actions include noncurrent_version_expiration with a positive noncurrent_days, noncurrent_version_transition, expiration, or transition. It fails when no aws_s3_bucket_lifecycle_configuration targets the bucket, or when every defined rule has status = "Disabled".

Common pitfalls

  • Lifecycle rule exists but is disabled

    Setting status = "Disabled" on every rule in aws_s3_bucket_lifecycle_configuration is effectively the same as having no policy at all. The control checks for at least one rule with status = "Enabled", so a configuration block full of disabled rules still fails.

  • Using deprecated inline lifecycle_rule in aws_s3_bucket

    Use aws_s3_bucket_lifecycle_configuration as a standalone resource. The deprecated lifecycle_rule block inside aws_s3_bucket is still valid HCL, but managing both for the same bucket is unsupported and typically causes perpetual diffs or silent policy overwrites on apply. If you have older configurations using the inline block, migrate them to the standalone resource.

  • Filter scope mismatch leaves versions unmanaged

    A lifecycle rule with prefix = "logs/" only expires versions under that path. Everything outside that prefix accumulates indefinitely. Make sure your rules collectively cover every prefix in use, or use an empty filter {} block to apply a rule bucket-wide.

  • Versioning suspended does not delete existing versions

    Suspending versioning via versioning_configuration.status = "Suspended" stops new versions from being created but leaves every existing noncurrent version in place. If versioning was previously active, you still need a lifecycle rule with noncurrent_version_expiration to clean up what accumulated while it was on.

Audit evidence

Auditors expect Config rule evaluation results showing compliant status for all versioned S3 buckets, typically from the managed rule S3_LIFECYCLE_POLICY_CHECK or an equivalent custom rule. Supporting that, the output of aws s3api get-bucket-lifecycle-configuration --bucket <name> for each versioned bucket should show active rules with noncurrent version expiration or transition actions.

Security Hub dashboards correlating versioning status with lifecycle policy presence give a consolidated view across accounts. CloudTrail logs showing PutBucketLifecycleConfiguration API calls establish when policies were applied and by whom.

Framework-specific interpretation

SOC 2: CC8.1 and related information lifecycle criteria expect data to be retained only as long as needed and that changes to storage systems stay controlled. Lifecycle policies on versioned buckets address both, capping retention and preventing storage from growing outside any defined boundary.

PCI DSS v4.0: Requirement 3.2 mandates that stored account data isn't retained beyond defined periods. For S3 buckets holding cardholder data with versioning on, lifecycle expiration is how you enforce that at the object level. Without it, prior versions of cardholder data persist indefinitely regardless of what your retention policy says on paper.

NIST SP 800-53 Rev 5: Both SI-12 (Information Management and Retention) and CM-2 (Baseline Configuration) apply. SI-12 requires that information follows defined retention and disposal schedules; CM-2 expects that storage configurations remain within an authorized baseline. Lifecycle rules are the operational mechanism that makes both enforceable on versioned objects.

NIST Cybersecurity Framework v2.0: PR.DS data security calls for systematic management of data throughout its lifecycle. Without lifecycle rules on versioned buckets, retention commitments are unenforceable and storage grows without bound, both of which are visible gaps during a CSF assessment.

FedRAMP Moderate Baseline Rev 4: SI-12 requires federal systems to follow defined retention and disposal procedures. Versioned buckets without expiration rules make it impossible to demonstrate that stored data doesn't outlive its authorized retention period, which is exactly what assessors check.

Tool mappings

Use these identifiers to cross-reference this control across tools, reports, and evidence.

  • Compliance.tf Control: s3_bucket_versioning_and_lifecycle_policy_enabled

  • AWS Config Managed Rule: S3_VERSION_LIFECYCLE_POLICY_CHECK

  • Powerpipe Control: aws_compliance.control.s3_bucket_versioning_and_lifecycle_policy_enabled

  • Prowler Check: s3_bucket_lifecycle_enabled

  • AWS Security Hub Control: S3.10

Last reviewed: 2026-03-09