CloudTrail Multi-Region Setup in Terraform: Account-Wide Trail
Last reviewed: 2026-05-27 · 10 min read
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.
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.
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.
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}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.
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.
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
IAM Least Privilege in Terraform →
KMS Key Rotation in Terraform →
VPC Security Groups Audit in Terraform →
AWS Security pillar overview →
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