CloudTrail Multi-Region Setup in Terraform: Account-Wide Trail

Last reviewed: 2026-05-27 · 10 min read

Rost Mironenko
Rost Mironenko·Founder, ArchGuard

5+ years AWS engineering · Open-source contributor

Last reviewed: 2026-05-27

A multi-region CloudTrail trail in Terraform means one trail that records all API activity across all AWS regions in an account — including global services like IAM and STS. Without it, activity in unmonitored regions is invisible. The AWS Well-Architected Security pillar (SEC04-BP02) requires capturing and analyzing logs from all relevant sources. CloudTrail is the mandatory baseline — without it, incident response is guesswork.

Step 1 — Create the CloudTrail S3 bucket with a delivery policy

CloudTrail requires explicit permission to write to your S3 bucket. The bucket policy must allow the cloudtrail.amazonaws.com service principal to call s3:GetBucketAcl (to verify the bucket is accessible) and s3:PutObject (to write log files). Without this, the trail creates successfully but logs are never delivered.

cloudtrail.tf✓ After
data "aws_caller_identity" "current" {}data "aws_region" "current" {}resource "aws_s3_bucket" "cloudtrail" {  bucket        = "${var.env}-cloudtrail-logs-${data.aws_caller_identity.current.account_id}"  force_destroy = false  tags = {    ManagedBy = "terraform"  }}resource "aws_s3_bucket_public_access_block" "cloudtrail" {  bucket                  = aws_s3_bucket.cloudtrail.id  block_public_acls       = true  block_public_policy     = true  ignore_public_acls      = true  restrict_public_buckets = true}data "aws_iam_policy_document" "cloudtrail_bucket" {  statement {    sid     = "AWSCloudTrailAclCheck"    effect  = "Allow"    actions = ["s3:GetBucketAcl"]    principals {      type        = "Service"      identifiers = ["cloudtrail.amazonaws.com"]    }    resources = [aws_s3_bucket.cloudtrail.arn]    condition {      test     = "StringEquals"      variable = "aws:SourceArn"      values   = ["arn:aws:cloudtrail:${data.aws_region.current.name}:${data.aws_caller_identity.current.account_id}:trail/${var.env}-trail"]    }  }  statement {    sid     = "AWSCloudTrailWrite"    effect  = "Allow"    actions = ["s3:PutObject"]    principals {      type        = "Service"      identifiers = ["cloudtrail.amazonaws.com"]    }    resources = ["${aws_s3_bucket.cloudtrail.arn}/AWSLogs/${data.aws_caller_identity.current.account_id}/*"]    condition {      test     = "StringEquals"      variable = "s3:x-amz-acl"      values   = ["bucket-owner-full-control"]    }    condition {      test     = "StringEquals"      variable = "aws:SourceArn"      values   = ["arn:aws:cloudtrail:${data.aws_region.current.name}:${data.aws_caller_identity.current.account_id}:trail/${var.env}-trail"]    }  }}resource "aws_s3_bucket_policy" "cloudtrail" {  bucket     = aws_s3_bucket.cloudtrail.id  policy     = data.aws_iam_policy_document.cloudtrail_bucket.json  depends_on = [aws_s3_bucket_public_access_block.cloudtrail]}

The aws:SourceArn condition prevents other trails or accounts from delivering logs to your bucket

Step 2 — Create a CloudWatch Logs log group for real-time visibility

S3 delivery is the durable archive. CloudWatch Logs delivery enables real-time metric filters and alarms — for example, alerting on root account console logins or IAM policy changes. The log group needs a retention period; the default is no expiry, which accumulates cost over time.

cloudtrail.tf✓ After
resource "aws_cloudwatch_log_group" "cloudtrail" {  name              = "/aws/cloudtrail/${var.env}"  retention_in_days = 90  kms_key_id        = aws_kms_key.main.arn}data "aws_iam_policy_document" "cloudtrail_cw_assume" {  statement {    effect  = "Allow"    actions = ["sts:AssumeRole"]    principals {      type        = "Service"      identifiers = ["cloudtrail.amazonaws.com"]    }  }}data "aws_iam_policy_document" "cloudtrail_cw_logs" {  statement {    effect    = "Allow"    actions   = ["logs:CreateLogStream", "logs:PutLogEvents"]    resources = ["${aws_cloudwatch_log_group.cloudtrail.arn}:*"]  }}resource "aws_iam_role" "cloudtrail_cw" {  name               = "${var.env}-cloudtrail-cw-role"  assume_role_policy = data.aws_iam_policy_document.cloudtrail_cw_assume.json}resource "aws_iam_role_policy" "cloudtrail_cw" {  name   = "cloudwatch-logs-delivery"  role   = aws_iam_role.cloudtrail_cw.id  policy = data.aws_iam_policy_document.cloudtrail_cw_logs.json}

Step 3 — Create the multi-region trail

The two required settings for account-wide coverage are is_multi_region_trail = true (records activity in all regions) and include_global_service_events = true (records IAM, STS, and Route 53 activity, which have no region). Log file validation creates a digest file that CloudTrail signs — allowing you to detect if a log file was deleted or modified after delivery.

cloudtrail.tf✗ Before
resource "aws_cloudtrail" "bad" {  name           = "my-trail"  s3_bucket_name = aws_s3_bucket.cloudtrail.id  # is_multi_region_trail defaults to false  # include_global_service_events defaults to true but not explicit  # No log file validation  # No CloudWatch Logs delivery}
cloudtrail.tf✓ After
resource "aws_cloudtrail" "main" {  name                          = "${var.env}-trail"  s3_bucket_name                = aws_s3_bucket.cloudtrail.id  include_global_service_events = true  is_multi_region_trail         = true  enable_log_file_validation    = true  kms_key_id                    = aws_kms_key.main.arn  cloud_watch_logs_group_arn = "${aws_cloudwatch_log_group.cloudtrail.arn}:*"  cloud_watch_logs_role_arn  = aws_iam_role.cloudtrail_cw.arn  event_selector {    read_write_type           = "All"    include_management_events = true  }  depends_on = [    aws_s3_bucket_policy.cloudtrail,    aws_s3_bucket_public_access_block.cloudtrail,  ]  tags = {    ManagedBy = "terraform"  }}

The depends_on prevents the trail from being created before the bucket policy is in place

Organization trail for multi-account environments

If your workload spans multiple AWS accounts in an organization, deploy an organization trail from the management account. It aggregates logs from all member accounts into the management account's S3 bucket.

cloudtrail-org.tf
resource "aws_cloudtrail" "org" {  # Deploy this only from the management account  count = var.is_management_account ? 1 : 0  name                          = "org-trail"  s3_bucket_name                = aws_s3_bucket.cloudtrail.id  is_multi_region_trail         = true  is_organization_trail         = true  include_global_service_events = true  enable_log_file_validation    = true  kms_key_id                    = aws_kms_key.main.arn}

is_organization_trail = true requires CloudTrail to be enabled as a trusted service in AWS Organizations

Log retention with S3 lifecycle rules

CloudTrail logs have high retention requirements (CIS AWS Foundations Benchmark v3.0.0 control 3.7 recommends at least 365 days) but are infrequently accessed after 90 days. S3 lifecycle rules move older logs to Glacier Instant Retrieval, which costs approximately 4× less than S3 Standard for the same storage volume.

cloudtrail.tf
resource "aws_s3_bucket_lifecycle_configuration" "cloudtrail" {  bucket = aws_s3_bucket.cloudtrail.id  rule {    id     = "cloudtrail-retention"    status = "Enabled"    transition {      days          = 90      storage_class = "GLACIER_IR"    }    expiration {      days = 365    }  }}

Related Security pillar articles

Frequently asked questions

What is the difference between a single-region and multi-region CloudTrail?

A single-region trail logs API activity only in the region where it is created. A multi-region trail (is_multi_region_trail = true) automatically logs API activity in all current and future AWS regions in the account, including global services like IAM, STS, and Route 53. The AWS Well-Architected Security pillar requires multi-region coverage.

What is an organization trail in CloudTrail?

An organization trail (is_organization_trail = true) logs API activity from all accounts in an AWS Organization into a single S3 bucket in the management account. It is the recommended approach for multi-account environments and requires the CloudTrail service principal to be enabled in AWS Organizations.

How long should CloudTrail logs be retained?

The AWS Well-Architected Security pillar does not specify a minimum retention period; CIS AWS Foundations Benchmark v3.0.0 (control 3.7) recommends S3 log retention of at least 365 days. Regulations like GDPR, PCI DSS, and HIPAA have their own requirements. Use S3 lifecycle rules to transition older logs to S3 Glacier for cost-effective long-term retention.

Why does the CloudTrail S3 bucket need a specific bucket policy?

The CloudTrail service requires explicit permission to write logs to your S3 bucket. The bucket policy must include a statement that allows the cloudtrail.amazonaws.com service principal to call s3:PutObject on the bucket prefix where CloudTrail writes logs. Without this, trail creation succeeds but log delivery fails silently.

Get an automated CloudTrail configuration review

ArchGuard reviews your Terraform for CloudTrail, logging, and detection gaps across the AWS Well-Architected Security pillar and delivers a branded PDF in 24 hours.

See how it works