WAF-COST-040 โ€“ Storage & Retention Lifecycle Defined

Description

All storage resources (S3 buckets, Azure Storage accounts, GCS buckets) MUST have explicit lifecycle rules configured. All log groups (CloudWatch, Azure Log Analytics, GCP Cloud Logging) MUST have an explicit retention_in_days value that is not 0 (infinite) and not null. No storage or logging resource may be provisioned without an explicit, finite retention or lifecycle policy. Infinite retention is classified as Architectural Cost Debt.

Rationale

Risk Description

Risk

Unlimited storage retention lets data grow unnecessarily and increases cost and deletion risk.

Risk

Missing lifecycle rules prevent automatic archiving or deletion by data classification.

Requirements

  • All storage resources (S3 buckets, Azure Storage accounts, GCS buckets) MUST have explicit lifecycle rules configured.

  • All log groups (CloudWatch, Azure Log Analytics, GCP Cloud Logging) MUST have an explicit retention_in_days value that is not 0 (infinite) and not null.

Implementation Guidance

  1. Implement the required configuration as code in Terraform or equivalent IaC.

  2. Add an automated check to CI/CD so deviations are caught before deployment.

Maturity Levels

Level Name Criteria

L1

Ad hoc โ€“ control not consistently applied

* Manual evidence exists for some workloads, but coverage is unknown.

L2

Standardised โ€“ policy defined and communicated

* A version-controlled policy, ADR, or standard describes the requirement.

L3

Integrated โ€“ automated validation in CI/CD

* Terraform checks run automatically on pull requests and report results.

L4

Auditable โ€“ metrics and exceptions tracked

* Control coverage, failures, and approved exceptions are reported quarterly.

L5

Optimised โ€“ continuous improvement based on data

* Control effectiveness is measured and iteratively improved.

Checks

waf-cost-040.tf.aws.s3-lifecycle-rule-defined

Engine: terraform | Provider: aws | Automated: Yes

S3 buckets must have explicit lifecycle configuration

Every aws_s3_bucket resource must have an associated aws_s3_bucket_lifecycle_configuration resource, or a lifecycle_rule block. S3 buckets without lifecycle rules accumulate data indefinitely, driving unbounded storage costs.

Scope: resource โ€” aws_s3_bucket_lifecycle_configuration

Assertions: * rule not empty โ€” S3 lifecycle configuration must contain at least one rule.

Remediation: Add an aws_s3_bucket_lifecycle_configuration resource for every S3 bucket. Include at minimum: transition to STANDARD_IA after 30 days, GLACIER after 90 days, and an abort_incomplete_multipart_upload rule with days_after_initiation = 3.

Compliant
resource "aws_s3_bucket_lifecycle_configuration" "data" {
  bucket = aws_s3_bucket.data.id

  rule {
    id     = "tiering"
    status = "Enabled"

    transition {
      days          = 30
      storage_class = "STANDARD_IA"
    }
    transition {
      days          = 90
      storage_class = "GLACIER_IR"
    }
    expiration {
      days = 365
    }
  }

  rule {
    id     = "abort-incomplete"
    status = "Enabled"
    abort_incomplete_multipart_upload {
      days_after_initiation = 3
    }
  }
}
Non-Compliant
resource "aws_s3_bucket" "data" {
  bucket = "acme-application-data"
  # No lifecycle configuration โ€“ WAF-COST-040 violation
  # Data will accumulate indefinitely
}

waf-cost-040.tf.aws.cloudwatch-log-retention-not-zero

Engine: terraform | Provider: aws | Automated: Yes

CloudWatch Log Groups must not have infinite retention

All aws_cloudwatch_log_group resources must have retention_in_days set to a value greater than 0. The Terraform default (not setting retention_in_days) results in infinite retention, which is classified as Architectural Cost Debt in Wombat.

Scope: resource โ€” aws_cloudwatch_log_group

Assertions: * retention_in_days attribute exists โ€” CloudWatch Log Group is missing retention_in_days. Default is infinite retention (cost debt). * retention_in_days greater than or equal 1 โ€” retention_in_days must be >= 1. Setting 0 means infinite retention (WAF-COST-040 violation).

Remediation: Set retention_in_days on all aws_cloudwatch_log_group resources. Use the 4-tier model: operational=30, security-audit=365, compliance=2555 (7yr). Never use 0 (infinite) as this represents unconstrained cost debt.

Compliant
resource "aws_cloudwatch_log_group" "application" {
  name              = "/app/production/payment-service"
  retention_in_days = 30  # Hot-tier: operational logs
  kms_key_id        = aws_kms_key.logging.arn
  tags              = module.mandatory_tags.tags
}

resource "aws_cloudwatch_log_group" "audit" {
  name              = "/app/production/audit"
  retention_in_days = 365  # Cold-tier: audit logs
  kms_key_id        = aws_kms_key.logging.arn
  tags              = module.mandatory_tags.tags
}
Non-Compliant
resource "aws_cloudwatch_log_group" "application" {
  name = "/app/production/payment-service"
  # retention_in_days not set = infinite retention = WAF-COST-040 violation
}

resource "aws_cloudwatch_log_group" "debug" {
  name              = "/app/production/debug"
  retention_in_days = 0  # Explicit infinite โ€“ also a violation
}

waf-cost-040.tf.azurerm.storage-lifecycle-rule

Engine: terraform | Provider: azurerm | Automated: Yes

Azure Storage accounts must have explicit lifecycle management policies

Every azurerm_storage_account used for blob storage must have an azurerm_storage_management_policy resource attached with at least one rule defining tiering or deletion. Log Analytics workspaces must have an explicit retention_in_days value.

Scope: resource โ€” azurerm_storage_management_policy, azurerm_log_analytics_workspace

Assertions: * rule not empty โ€” Azure storage management policy must contain at least one rule.

Remediation: Add an azurerm_storage_management_policy for each storage account. Define rules to tier blobs to Cool/Archive and delete old versions. Set retention_in_days on azurerm_log_analytics_workspace resources to a finite value.

Compliant
resource "azurerm_storage_management_policy" "data" {
  storage_account_id = azurerm_storage_account.data.id

  rule {
    name    = "tier-and-delete"
    enabled = true
    filters {
      prefix_match = ["container1"]
      blob_types   = ["blockBlob"]
    }
    actions {
      base_blob {
        tier_to_cool_after_days_since_modification_greater_than    = 30
        tier_to_archive_after_days_since_modification_greater_than = 90
        delete_after_days_since_modification_greater_than        = 365
      }
    }
  }
}

resource "azurerm_log_analytics_workspace" "logs" {
  name                = "log-workspace"
  location            = var.location
  resource_group_name = azurerm_resource_group.main.name
  retention_in_days   = 30
}
Non-Compliant
resource "azurerm_storage_account" "data" {
  name                     = "acmeappdata"
  resource_group_name      = azurerm_resource_group.main.name
  location                 = var.location
  account_tier             = "Standard"
  account_replication_type = "LRS"
  # No azurerm_storage_management_policy attached
}

waf-cost-040.tf.google.gcs-lifecycle-rule

Engine: terraform | Provider: google | Automated: Yes

GCS buckets must have explicit lifecycle rules

Every google_storage_bucket resource must define a lifecycle_rules block. Cloud Logging buckets must have an explicit retention_days value to prevent infinite retention and unbounded cost.

Scope: resource โ€” google_storage_bucket

Assertions: * lifecycle_rule attribute exists โ€” GCS bucket must define at least one lifecycle rule.

Remediation: Add lifecycle_rules to every google_storage_bucket. Include rules for transitions to Nearline/Coldline/Archive and deletion. Set retention_days on logging buckets to a finite value.

Compliant
resource "google_storage_bucket" "data" {
  name          = "acme-app-data"
  location      = var.region
  storage_class = "STANDARD"

  lifecycle_rule {
    action {
      type          = "SetStorageClass"
      storage_class = "NEARLINE"
    }
    condition {
      age = 30
    }
  }

  lifecycle_rule {
    action {
      type = "Delete"
    }
    condition {
      age = 365
    }
  }
}
Non-Compliant
resource "google_storage_bucket" "data" {
  name     = "acme-app-data"
  location = var.region
  # No lifecycle_rule defined โ€“ data accumulates indefinitely
}

waf-cost-040.tf.stackit.object-storage-lifecycle

Engine: terraform | Provider: stackit | Automated: Yes

STACKIT object storage buckets must have a lifecycle rule

STACKIT object storage buckets must have a lifecycle rule. This check validates the Terraform configuration uses the expected resource and attribute.

Scope: resource โ€” stackit_object_storage_bucket

Assertions: * lifecycle_rule attribute exists โ€” STACKIT object storage bucket must define lifecycle rules.

Remediation: Review the Terraform configuration for the listed resource types and ensure the required attribute or tag is present. See the control narrative for provider-specific guidance.

Compliant
# Example resource meets the requirement.
resource "stackit_object_storage_bucket" "example" {
  # required attribute/tag/label present
}
Non-Compliant
# Example resource does not meet the requirement.
resource "stackit_object_storage_bucket" "example" {
  # Missing required attribute/tag/label
}

waf-cost-040.tf.ovh.object-storage-lifecycle

Engine: terraform | Provider: ovh | Automated: Yes

OVHcloud object storage containers must have a lifecycle rule

OVHcloud object storage containers must have a lifecycle rule. This check validates the Terraform configuration uses the expected resource and attribute.

Scope: resource โ€” ovh_cloud_project_storage_object_container

Assertions: * lifecycle_rule attribute exists โ€” OVHcloud object storage container must define lifecycle rules.

Remediation: Review the Terraform configuration for the listed resource types and ensure the required attribute or tag is present. See the control narrative for provider-specific guidance.

Compliant
# Example resource meets the requirement.
resource "ovh_cloud_project_storage_object_container" "example" {
  # required attribute/tag/label present
}
Non-Compliant
# Example resource does not meet the requirement.
resource "ovh_cloud_project_storage_object_container" "example" {
  # Missing required attribute/tag/label
}

waf-cost-040.tf.hcloud.volume-backup-policy

Engine: terraform | Provider: hcloud | Automated: Yes

Hetzner Cloud volumes must have a backup or lifecycle policy

Hetzner Cloud volumes must have a backup or lifecycle policy. This check validates the Terraform configuration uses the expected resource and attribute.

Scope: resource โ€” hcloud_volume

Assertions: * labels.lifecycle-policy key exists โ€” Hetzner Cloud volume must declare a lifecycle-policy label.

Remediation: Review the Terraform configuration for the listed resource types and ensure the required attribute or tag is present. See the control narrative for provider-specific guidance.

Compliant
# Example resource meets the requirement.
resource "hcloud_volume" "example" {
  # required attribute/tag/label present
}
Non-Compliant
# Example resource does not meet the requirement.
resource "hcloud_volume" "example" {
  # Missing required attribute/tag/label
}

Evidence

Kind Required Description

iac

Required

Terraform configuration implementing the control.

governance

Required

Policy, ADR, or standard documenting the requirement.

config

Optional

Cloud console or API output confirming runtime state.

Framework Controls

FinOps Foundation

* Optimize Phase โ€“ Storage Optimization and Lifecycle Management

GDPR

* Art. 5(1)(e) โ€“ Storage limitation (data not kept longer than necessary) * Art. 17 โ€“ Right to erasure (lifecycle policies support GDPR deletion requirements)