Back to Checks
aws

S3 bucket has cross-region replication configured to a bucket in a different region

s3_bucket_cross_region_replication
Service: s3Severity: low
OfficialBuilt-inVerified
Categories:resilience
Resource Type:AwsS3Bucket

Amazon S3 buckets use cross-Region replication with versioning and an enabled rule that targets a destination bucket in a different AWS Region.

Buckets with same-Region targets, missing destinations, or disabled versioning don't meet this replication posture.

Risk

Single-Region storage creates an availability gap: a Regional outage, control-plane isolation, or denial of service can make data unreachable.

Lack of replication raises RPO/RTO, delaying recovery and disrupting multi-Region workloads. Missing replicas also weaken data integrity during restore from corruption or deletion.

Run this check with Prowler

Run in Prowler Cloud
Run
prowler aws --checks s3_bucket_cross_region_replication

Recommendation

Enable CRR to a different Region with versioning and least-privilege roles.

  • Replicate needed prefixes and metadata
  • Consider S3 Replication Time Control for tighter RPO
  • Protect deletes via delete marker strategy and Object Lock
  • Monitor replication metrics and test DR regularly

Align with defense in depth and availability by design.

Remediation

Terraform

# Enable versioning and minimal CRR to a bucket in another region
resource "aws_s3_bucket" "source" {
  bucket = "<example_resource_name>"

  versioning {                      
    enabled = true  # critical: required for replication
  }

  replication_configuration {        
    role = aws_iam_role.replication.arn  # critical: role used by S3 to replicate

    rules {
      status = "Enabled"  # critical: replication rule must be enabled
      destination {
        bucket = "arn:aws:s3:::<example_destination_bucket_name>"  # critical: destination bucket ARN in a different region
      }
    }
  }
}

resource "aws_iam_role" "replication" {
  name = "<example_resource_name>-s3-replication-role"
  assume_role_policy = jsonencode({
    Version = "2012-10-17"
    Statement = [{
      Effect    = "Allow"
      Principal = { Service = "s3.amazonaws.com" }
      Action    = "sts:AssumeRole"
    }]
  })
}

resource "aws_iam_role_policy" "replication" {
  name = "<example_resource_name>-s3-replication-policy"
  role = aws_iam_role.replication.id
  policy = jsonencode({
    Version = "2012-10-17",
    Statement = [
      {
        Effect = "Allow",
        Action = ["s3:ListBucket", "s3:GetReplicationConfiguration"],
        Resource = "arn:aws:s3:::${aws_s3_bucket.source.bucket}"
      },
      {
        Effect = "Allow",
        Action = [
          "s3:GetObjectVersionForReplication",
          "s3:GetObjectVersionAcl",
          "s3:GetObjectVersionTagging"
        ],
        Resource = "arn:aws:s3:::${aws_s3_bucket.source.bucket}/*"
      },
      {
        Effect = "Allow",
        Action = ["s3:ReplicateObject", "s3:ReplicateDelete", "s3:ReplicateTags"],
        Resource = "arn:aws:s3:::<example_destination_bucket_name>/*"
      }
    ]
  })
}

NativeIaC

# CloudFormation: Enable versioning and CRR to a bucket in another region
Resources:
  ExampleBucket:
    Type: AWS::S3::Bucket
    Properties:
      VersioningConfiguration:
        Status: Enabled  # critical: enables versioning (required for replication)
      ReplicationConfiguration:  # critical: enables cross-Region replication
        Role: !GetAtt ReplicationRole.Arn
        Rules:
          - Status: Enabled  # critical: replication rule must be enabled
            Destination:
              Bucket: arn:aws:s3:::<example_destination_bucket_name>  # critical: destination bucket ARN in a different region

  ReplicationRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Effect: Allow
            Principal:
              Service: s3.amazonaws.com
            Action: sts:AssumeRole
      Policies:
        - PolicyName: s3-replication-minimal
          PolicyDocument:
            Version: '2012-10-17'
            Statement:
              - Effect: Allow
                Action:
                  - s3:ListBucket
                  - s3:GetReplicationConfiguration
                Resource: !Sub arn:aws:s3:::${ExampleBucket}
              - Effect: Allow
                Action:
                  - s3:GetObjectVersionForReplication
                  - s3:GetObjectVersionAcl
                  - s3:GetObjectVersionTagging
                Resource: !Sub arn:aws:s3:::${ExampleBucket}/*
              - Effect: Allow
                Action:
                  - s3:ReplicateObject
                  - s3:ReplicateDelete
                  - s3:ReplicateTags
                Resource: arn:aws:s3:::<example_destination_bucket_name>/*

Other

1In the S3 console, open the source bucket
2Go to Properties > Bucket Versioning and click Enable
3Go to Management > Replication rules > Create replication rule
4Scope: Apply to all objects in the bucket
5Destination: Select a bucket in a different AWS Region
6Status: Ensure Enabled is selected
7IAM role: Choose Create new role (recommended)
8Save the rule

Check Code

from prowler.lib.check.models import Check, Check_Report_AWS
from prowler.providers.aws.services.s3.s3_client import s3_client


class s3_bucket_cross_region_replication(Check):
    """Ensure S3 buckets replicate to a bucket in a different region.

    - PASS: At least one enabled replication rule targets a bucket in another region.
    - FAIL: Versioning is disabled, no enabled rule exists, or every resolvable
      destination is in the same region.
    - MANUAL: The versioning or replication configuration could not be retrieved
      (missing permissions), or a destination bucket is outside the audited
      account/scope so its region cannot be determined.
    """

    def execute(self) -> list[Check_Report_AWS]:
        findings = []
        for bucket in s3_client.buckets.values():
            report = Check_Report_AWS(metadata=self.metadata(), resource=bucket)

            if not bucket.versioning_retrieved or not bucket.replication_retrieved:
                report.status = "MANUAL"
                report.status_extended = f"Cannot evaluate cross region replication for S3 Bucket {bucket.name}: the versioning or replication configuration could not be retrieved. Verify that the scanning credentials are allowed to call s3:GetBucketVersioning and s3:GetReplicationConfiguration."
                findings.append(report)
                continue

            report.status = "FAIL"
            report.status_extended = f"S3 Bucket {bucket.name} does not have correct cross region replication configuration."
            unresolvable_report = None
            same_region_report = None
            if bucket.replication_rules:
                for rule in bucket.replication_rules:
                    if (
                        bucket.versioning
                        and rule.status == "Enabled"
                        and rule.destination
                    ):
                        if rule.destination not in s3_client.buckets:
                            unresolvable_report = f"S3 Bucket {bucket.name} has cross region replication rule {rule.id} in bucket {rule.destination.split(':')[-1]} which is out of Prowler's scope; verify manually that the destination bucket is in a different region."
                        else:
                            destination_bucket = s3_client.buckets[rule.destination]
                            if destination_bucket.region != bucket.region:
                                report.status = "PASS"
                                report.status_extended = f"S3 Bucket {bucket.name} has cross region replication rule {rule.id} in bucket {destination_bucket.name} located in region {destination_bucket.region}."
                                break
                            else:
                                same_region_report = f"S3 Bucket {bucket.name} has cross region replication rule {rule.id} in bucket {destination_bucket.name} located in the same region."
            # Precedence: PASS > MANUAL (unresolvable destination) > FAIL
            if report.status != "PASS":
                if unresolvable_report:
                    report.status = "MANUAL"
                    report.status_extended = unresolvable_report
                elif same_region_report:
                    report.status_extended = same_region_report
            findings.append(report)

        return findings