Building a Multi-Account Zero-Trust Governance Architecture in AWS Using Terraform, SCPs, and CloudTrail
Introduction
Most AWS “security projects” stop at deploying a few services and calling it secure.
Real cloud governance engineering is different.
The difficult part is not provisioning infrastructure. The difficult part is enforcing preventive controls, limiting blast radius, centralising audit visibility, handling SCP inheritance boundaries, and dealing with the operational realities of AWS Organisations and Terraform reconciliation.
In this project, I built a Terraform-managed AWS governance architecture implementing:
AWS Organizations
Nested Organisational Units (OUs)
Service Control Policies (SCPs)
Centralised CloudTrail logging
Encrypted audit storage
Region restriction governance
CloudTrail tamper protection
Terraform automation
Failure simulation and validation testing
The objective was not only to deploy infrastructure, but to validate whether preventive governance controls actually worked under real operational conditions.
Architecture Overview
The AWS Organisations hierarchy was designed to separate governance domains from workload domains.
Root
├── Security
│ ├── Audit
│ └── LogArchive
│
└── Workloads
├── Prod
└── Dev
This structure models a simplified enterprise landing-zone style architecture where:
Security OUs isolate governance functions
Workload OUs isolate application environments
SCP inheritance can be applied hierarchically
Blast radius can be reduced through organisational segmentation
Why Zero-Trust Governance Matters
In many AWS environments, the management account becomes a high-risk trust anchor with broad administrative privileges.
Without governance controls:
CloudTrail logging can be disabled
Resources can be deployed in unauthorised regions
Security visibility can be bypassed
Blast radius becomes uncontrolled
The objective of this lab was to implement preventive governance mechanisms instead of relying only on detective monitoring.
Deploying AWS Organisations Using Terraform
The AWS Organisation and OU hierarchy were deployed entirely through Terraform.
Organization Resource resource "aws_organizations_organization" "org" { feature_set = "ALL"
lifecycle { ignore_changes = [ enabled_policy_types ] } }
The ignore_changes lifecycle block became necessary because AWS Organisations policy-type propagation introduced Terraform reconciliation drift issues during repeated apply operations.
Organizational Units
resource "aws_organizations_organizational_unit" "security" { name = "Security" parent_id = aws_organizations_organization.org.roots[0].id }
resource "aws_organizations_organizational_unit" "workloads" { name = "Workloads" parent_id = aws_organizations_organization.org.roots[0].id }
Nested child OUs were then created under both governance domains.
Implementing Service Control Policies (SCPs)
Two primary preventive governance controls were implemented:
CloudTrail tamper protection
Region restriction governance
SCP 1 — CloudTrail Tamper Protection
The first SCP prevented users from disabling or deleting CloudTrail logging.
Policy Definition
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "DenyCloudTrailTampering",
"Effect": "Deny",
"Action": [
"cloudtrail:StopLogging",
"cloudtrail:DeleteTrail"
],
"Resource": "*"
}
]
}
Initial SCP Inheritance Failure
Initially, the SCP was attached only to the Workloads OU.
Testing was performed from the AWS management account located directly under the organisation root.
The SCP did not apply.
This exposed an important AWS Organisations inheritance nuance:
SCP inheritance only applies downward from the attachment point.
The management account remained unrestricted until the SCP was attached directly to the organisation's root level.
Centralised CloudTrail Logging
A centralised audit logging pipeline was created using:
Organisation-wide CloudTrail
Encrypted S3 storage
Versioning
Public access blocking
Terraform Configuration
resource "aws_cloudtrail" "org_trail" { name = "OrganizationTrail" s3_bucket_name = aws_s3_bucket.cloudtrail_logs.id include_global_service_events = true is_multi_region_trail = true enable_logging = true }
Securing the Audit Bucket
The centralised CloudTrail bucket is implemented:
SSE-S3 encryption
S3 versioning
Public access blocking
This ensured:
encrypted audit retention
accidental deletion resistance
public exposure prevention
CloudTrail Bucket Policy Failure
CloudTrail trail creation initially failed with the following error:
InsufficientS3BucketPolicyException
CloudTrail requires explicit bucket permissions allowing:
ACL validation
log delivery operations
A dedicated bucket policy had to be added before the trial creation succeeded.
This highlighted an operational detail frequently omitted in simplified tutorials.
Validating Preventive Governance Controls
The environment was then tested using AWS CLI failure simulations.
The goal was to verify whether preventive governance controls actually blocked unauthorised operations.
CloudTrail Tampering Simulation
The following command attempted to disable CloudTrail logging:
aws cloudtrail stop-logging --name OrganizationTrail --region ap-south-1
After SCP attachment at the organization root level, the request returned:
AccessDenied
This validated:
SCP enforcement
centralized governance
preventive security controls
policy inheritance behavior
CloudTrail Audit Validation
The denied API call was then verified inside CloudTrail Event History.
This confirmed:
attempted tampering
API identity tracking
centralized audit visibility
forensic event retention
AWS Service Architecture Nuance
Testing revealed that regional SCP enforcement behaves differently depending on service architecture.
For example:
EC2 APIs behaved as expected
S3 APIs demonstrated globally scoped behavior nuances
This highlighted an important governance consideration:
Not all AWS services behave consistently under region-based SCP conditions.
Terraform Drift and AWS Organisations Reconciliation Issues
AWS Organisations introduced several operational edge cases during Terraform reconciliation.
One recurring issue involved:
SERVICE_CONTROL_POLICY disable: couldn't find resource
This occurred because AWS Organisations policy-type propagation is eventually consistent and not strongly transactional.
Terraform repeatedly attempted to reconcile the organisation policy state during apply operations.
This required the following workaround:
lifecycle { ignore_changes = [ enabled_policy_types ] }
This became one of the most important operational lessons during the implementation.
Key Engineering Takeaways
This project highlighted several real-world governance engineering realities:
AWS Organisations is eventually consistent
SCP inheritance boundaries matter
Management accounts require special governance consideration
CloudTrail requires explicit S3 permissions
Governance infrastructure is operationally persistent
Terraform reconciliation against AWS Organisations can drift
Versioned audit buckets complicate destroy workflows
Preventive controls must be validated through failure simulation
Conclusion
This project evolved far beyond a simple Terraform deployment exercise.
The most valuable outcomes came from:
governance edge cases
SCP inheritance debugging
CloudTrail permission failures
Terraform reconciliation drift
operational teardown behaviour
Those are the same categories of issues encountered in real cloud platform engineering environments.
Building governance controls is relatively easy.
Validating, troubleshooting, and operationalising them is where the actual engineering begins.