Skip to content

S3 buckets should have lifecycle policies configured

Without lifecycle policies, objects accumulate indefinitely. Storage costs grow month over month with no automatic cleanup, and stale data increases the blast radius of a breach. A single forgotten bucket can quietly rack up thousands of dollars in Standard-tier charges for objects nobody reads.

Lifecycle rules also enforce data retention hygiene. Regulators expect organizations to delete data after its useful life, and auditors look for automated proof. Relying on manual cleanup scripts or ad-hoc deletion requests introduces human error and leaves compliance gaps that are hard to explain during an audit.

Retrofit consideration

Existing buckets may contain objects across mixed prefixes with different retention needs. Applying a single blanket lifecycle rule can inadvertently delete data still in use. Inventory each bucket's prefix structure and access patterns before rolling out rules.

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  = "nistcsf.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  = "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 this control, each aws_s3_bucket must have an associated aws_s3_bucket_lifecycle_configuration resource with at least one rule block set to status = "Enabled". A rule with status = "Disabled" does not satisfy the control. The bucket argument must reference the source bucket's id. It fails when no aws_s3_bucket_lifecycle_configuration resource exists, or when every defined rule is disabled.

Common pitfalls

  • Deprecated inline lifecycle_rule block

    The legacy lifecycle_rule block inside aws_s3_bucket was deprecated in AWS provider v4.x. Terraform may still accept it in some versions, but it can conflict with a separate aws_s3_bucket_lifecycle_configuration resource, causing perpetual diffs or silent overwrites. Use the standalone resource consistently.

  • Rules with status Disabled still count as no policy

    A rule block with status = "Disabled" is still visible in the S3 API response but does nothing. This control checks for at least one status = "Enabled" rule; disabled rules don't count toward passing.

  • Abort incomplete multipart upload often forgotten

    Incomplete multipart uploads accumulate storage charges but don't appear in standard object listings, so they're easy to miss. Add abort_incomplete_multipart_upload { days_after_initiation = 7 } to at least one rule to prevent orphaned parts from inflating your bill.

  • Versioned buckets need noncurrent version rules

    On versioning-enabled buckets, expiration only places a delete marker on the current version. Previous versions keep accumulating and keep costing money until you add noncurrent_version_expiration { noncurrent_days = N } to actually reclaim that storage.

  • Filter conflicts between prefix and tags

    A filter block that specifies both prefix and tag must wrap them in an and block. Omitting it causes a provider error. An empty filter {} applies the rule to the entire bucket.

Audit evidence

Auditors expect AWS Config evaluation results for s3-bucket-lifecycle-policy-check showing all in-scope buckets as COMPLIANT. Supporting evidence includes output of aws s3api get-bucket-lifecycle-configuration --bucket <name> for each bucket, showing at least one enabled rule with defined transitions or expirations. Screenshots from the S3 console Management tab showing active lifecycle rules are also acceptable.

For continuous compliance, a Config or Security Hub dashboard showing historical compliance status across the audit period adds continuity to the record. CloudTrail entries for PutBucketLifecycleConfiguration can establish when policies were first applied.

Framework-specific interpretation

SOC 2: SOC 2 confidentiality and privacy criteria expect organizations to dispose of data when its retention period ends. Lifecycle configuration shows auditors the organization has automated that process rather than relying on ad-hoc deletion requests.

PCI DSS v4.0: Requirements 3.1 and 3.2 prohibit retaining stored cardholder data and sensitive authentication data beyond business need. Objects stored in S3 won't self-delete, so lifecycle policies are the practical control for enforcing that limit.

NIST Cybersecurity Framework v2.0: PR.DS under NIST CSF v2 covers data management through its full lifecycle, including automated disposition. Lifecycle rules on S3 buckets satisfy the technical enforcement side of that expectation, replacing manual deletion processes with defined, auditable schedules.

Tool mappings

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

  • Compliance.tf Control: s3_bucket_lifecycle_policy_enabled

  • AWS Config Managed Rule: S3_LIFECYCLE_POLICY_CHECK

  • Checkov Check: CKV2_AWS_61

  • Powerpipe Control: aws_compliance.control.s3_bucket_lifecycle_policy_enabled

  • Prowler Check: s3_bucket_lifecycle_enabled

  • AWS Security Hub Controls: S3.10, S3.13, S3.25

Last reviewed: 2026-03-09