Sign in
Menu
Sign up
Docs
Adopt in an existing estateMigration kit

S3 bucket

For an engineer moving an existing S3 bucket from terraform-aws-modules to compliance.tf. The controls cover encryption, access logging, versioning, public access, replication and object lock. When you finish, terraform plan passes with the compliance.tf source.

Minor fixes · 15-30 minutes per instance · No state moves · Upstream v5.x

Before you start

  • A registry token, set up as in Registry tokens. The registry host for each framework is on Registry endpoints.
  • An upstream version inside the supported range (v5.x). Older versions: upgrade upstream first, see Version compatibility.
  • State: No terraform state mv. Resource addresses stay the same, because compliance.tf modules keep the upstream resource structure. If a control adds a resource, the plan shows it as an addition.
  • Not sure if this module should go first? The Migration kit tells you where to start. The Assessment checklist finds risks before you plan.

Change the source

The diff changes the source and adds the settings this module's controls need, so the plan passes. Replace the referenced resources (aws_...) with your own.

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

   bucket = "my-app-logs"
+
+  logging = {
+    target_bucket = "my-access-log-bucket"
+    target_prefix = "s3-access-logs/my-app-logs/"
+  }
+
+  server_side_encryption_configuration = {
+    rule = {
+      apply_server_side_encryption_by_default = {
+        sse_algorithm     = "aws:kms"
+        kms_master_key_id = aws_kms_key.s3.arn
+      }
+    }
+  }
+
+  versioning = {
+    enabled = true
+  }
+
+  lifecycle_rule = [{
+    id                                     = "abort-incomplete-multipart-upload"
+    status                                 = "Enabled"
+    abort_incomplete_multipart_upload_days = 7
+  }]
+
+  replication_configuration = {
+    role = aws_iam_role.s3_replication.arn
+
+    rules = [{
+      id       = "replicate-all"
+      status   = "Enabled"
+      priority = 1
+
+      delete_marker_replication = true
+
+      source_selection_criteria = {
+        sse_kms_encrypted_objects = {
+          enabled = true
+        }
+      }
+
+      destination = {
+        bucket             = aws_s3_bucket.replica.arn
+        storage_class      = "STANDARD"
+        replica_kms_key_id = aws_kms_key.replica.arn
+      }
+    }]
+  }

   tags = {
     Environment = "production"
   }
 }
 module "s3_bucket" {
-  source  = "terraform-aws-modules/s3-bucket/aws"
-  version = "~> 5.0"
+  # Exception, reviewed: MFA delete cannot coexist with the lifecycle rule below
+  source = "https://pcidss.compliance.tf/terraform-aws-modules/s3-bucket/aws?version=5.0.0&disable=s3_bucket_mfa_delete_enabled"

   bucket = "my-app-logs"
+
+  logging = {
+    target_bucket = "my-access-log-bucket"
+    target_prefix = "s3-access-logs/my-app-logs/"
+  }
+
+  server_side_encryption_configuration = {
+    rule = {
+      apply_server_side_encryption_by_default = {
+        sse_algorithm     = "aws:kms"
+        kms_master_key_id = aws_kms_key.s3.arn
+      }
+    }
+  }
+
+  versioning = {
+    enabled = true
+  }
+
+  lifecycle_rule = [{
+    id                                     = "abort-incomplete-multipart-upload"
+    status                                 = "Enabled"
+    abort_incomplete_multipart_upload_days = 7
+  }]
+
+  replication_configuration = {
+    role = aws_iam_role.s3_replication.arn
+
+    rules = [{
+      id       = "replicate-all"
+      status   = "Enabled"
+      priority = 1
+
+      delete_marker_replication = true
+
+      source_selection_criteria = {
+        sse_kms_encrypted_objects = {
+          enabled = true
+        }
+      }
+
+      destination = {
+        bucket             = aws_s3_bucket.replica.arn
+        storage_class      = "STANDARD"
+        replica_kms_key_id = aws_kms_key.replica.arn
+      }
+    }]
+  }

   tags = {
     Environment = "production"
   }
 }

Then download the new source and plan. terraform init installs a module whose source changed; -upgrade is only for fetching the same source and version again:

terraform init
terraform plan

After init, .terraform/modules/<module name>/compliancetf-manifest.json names the framework the module was built for.

Fix the plan errors

Each error names the control and links its page, which lists the frameworks that map it. These are the ones this module raises most often.

Access logging is not configured

The control requires a logging block with a target bucket. The module has no default for it, because only you know which bucket receives the logs. Control: s3_bucket_logging_enabled.

Add a logging block that names an existing bucket you own.

logging = {
  target_bucket = "my-access-log-bucket"
  target_prefix = "s3-access-logs/my-app-logs/"
}

Encryption uses AES-256 or has no KMS key

The control requires sse_algorithm set to aws:kms (or aws:kms:dsse) together with a customer managed key in kms_master_key_id. AES-256, or aws:kms without a key ID, fails. Control: s3_bucket_default_encryption_enabled_kms.

Point default encryption at a KMS key you manage.

server_side_encryption_configuration = {
  rule = {
    apply_server_side_encryption_by_default = {
      sse_algorithm     = "aws:kms"
      kms_master_key_id = aws_kms_key.s3.arn
    }
  }
}

Cross-region replication is not configured

The control requires a replication rule with status Enabled, which needs an IAM role and a destination bucket in another region. Versioning must be on for both buckets. Control: s3_bucket_cross_region_replication_enabled.

Create the role and the destination bucket first, then add a replication rule. The bucket is KMS-encrypted, so the rule also opts in to replicating KMS-encrypted objects and names the replica key; the role needs kms:Decrypt on the source key and kms:Encrypt on the replica key.

replication_configuration = {
  role = aws_iam_role.s3_replication.arn

  rules = [{
    id       = "replicate-all"
    status   = "Enabled"
    priority = 1

    delete_marker_replication = true

    source_selection_criteria = {
      sse_kms_encrypted_objects = {
        enabled = true
      }
    }

    destination = {
      bucket             = aws_s3_bucket.replica.arn
      storage_class      = "STANDARD"
      replica_kms_key_id = aws_kms_key.replica.arn
    }
  }]
}

No enabled lifecycle rule on a versioned bucket

When versioning is Enabled, the control requires at least one lifecycle rule with status Enabled. The module's default is no lifecycle rules. Control: s3_bucket_versioning_and_lifecycle_policy_enabled.

Add an enabled lifecycle rule. Aborting incomplete multipart uploads is a rule almost every bucket wants anyway.

lifecycle_rule = [{
  id                                     = "abort-incomplete-multipart-upload"
  status                                 = "Enabled"
  abort_incomplete_multipart_upload_days = 7
}]

Object lock is off on an existing bucket

AWS lets you turn Object Lock on for an existing bucket once versioning is enabled, and AWS never turns it off again. New objects get a retention period only if you also configure a default retention rule; without one, objects can still be deleted. Objects already in the bucket get no retention until you apply it with S3 Batch Operations. Control: s3_bucket_object_lock_enabled.

Enable versioning and Object Lock, then use S3 Batch Operations for the existing objects. If the bucket does not need object lock, turn the control off on the source URL and record why.

versioning = {
  enabled = true
}

object_lock_enabled = true

MFA delete conflicts with the lifecycle rule (PCI DSS v4.0 only)

PCI DSS v4.0 maps s3_bucket_mfa_delete_enabled and s3_bucket_versioning_and_lifecycle_policy_enabled; SOC 2 maps only the second. AWS does not allow lifecycle configurations on a bucket with MFA delete, so one bucket cannot meet both controls. Only the root user can enable MFA delete, with the MFA device serial and a current code, through the AWS CLI or the API. Control: s3_bucket_mfa_delete_enabled.

Pick one control per bucket and turn the other off on the source, with the reason in the pull request. The After block above keeps the lifecycle rule. To keep MFA delete instead, turn off the lifecycle control, remove lifecycle_rule, set mfa_delete with the root user's MFA serial and code in mfa, and have the root user make that one change.

source = "https://pcidss.compliance.tf/terraform-aws-modules/s3-bucket/aws?version=5.0.0&disable=s3_bucket_versioning_and_lifecycle_policy_enabled"

versioning = {
  enabled    = true
  mfa_delete = true
  mfa        = var.root_mfa # "<MFA device serial> <current code>", root user only
}

A control that does not apply to this instance can be turned off on the source URL; see Turn controls on and off.

Controls this enforces

SOC 2

PCI DSS v4.0

Each framework also maps controls that this module does not enforce. Those stay with your team; the module page lists enforced and mapped-only controls apart.

Roll back

  1. Point the source back at terraform-aws-modules/s3-bucket/aws, keeping the same version constraint.
  2. Run terraform init.
  3. Run terraform plan. Expect no resource changes: the values you added stay in place, but compliance.tf no longer checks them.

Next steps

On this page

Ask AI about this

Help improve this page