Skip to content
~/sph.sh

AWS Control Tower Multi-Account Strategy: From Landing Zone to Enterprise Governance

A practical guide to designing and implementing AWS Control Tower multi-account strategy covering OU structure, SCPs, RCPs, Account Factory for Terraform, IAM Identity Center, and centralized security architecture.

AWS Control Tower provides the foundation for a well-governed, multi-account AWS environment. This post covers practical decision-making for OU structure design, guardrail selection, Account Factory automation, and cross-account access patterns. Rather than rehashing AWS documentation, the focus is on when to use which approach and the practical trade-offs involved.

Last verified against AWS Control Tower Landing Zone 4.0 and AFT v1.18.x (March 2026). AWS services evolve rapidly; always cross-check with official documentation for the latest changes.

Why Multi-Account Strategy Matters

Starting with a single AWS account works fine for small teams. The problems show up as the organization grows. Here is what tends to happen without a governed multi-account strategy:

  • Security sprawl: Teams create accounts ad-hoc with no baseline controls. Unencrypted S3 buckets, public resources, and missing CloudTrail logs become the norm.
  • Compliance drift: Manual guardrail enforcement degrades over time. Controls get disabled, configurations get changed, and nobody notices until an audit.
  • Access management chaos: IAM users scattered across accounts, shared root credentials, no centralized identity management.
  • Cost attribution blindness: No visibility into which team, project, or environment drives costs across dozens of accounts.
  • Slow account provisioning: A new project needs an account? That takes weeks of manual setup, security review, and configuration.
  • Inconsistent environments: Dev, staging, and prod accounts configured differently, leading to deployment issues that only appear in production.

A multi-account strategy addresses these problems by isolating workloads, enforcing guardrails at the organizational level, and automating account provisioning. Control Tower gives you a well-architected baseline to build on.

Organizational Unit Structure Design

The OU structure is the backbone of your governance model. Design OUs around policy boundaries (what controls apply where), not around your org chart.

Control Tower traditionally auto-creates the Security OU with Log Archive and Audit accounts. Since Landing Zone version 4.0 (late 2025), this is no longer mandatory; you can define fully custom OU structures as long as hub accounts (Log Archive, Audit) share the same parent OU. The Security OU remains the default for new setups and is still a widely used pattern. You add the rest based on your governance needs.

Note: If you are upgrading from Landing Zone 3.x, the Security OU migration is not automatic. Review the Landing Zone 4.0 migration guide before making OU structural changes. Pre-existing custom OUs can now be designated directly as hub OU targets without restructuring.

Flat vs Nested OUs

AWS supports up to 5 levels of nesting, but 2-3 is the practical maximum. Deeper nesting makes SCP inheritance harder to reason about and debug.

Use nested OUs when:

  • You have more than 20 accounts in a single OU
  • Different environments (prod/staging/dev) need different policy boundaries
  • You need to apply SCPs at the environment level, not just the workload level

Keep it flat when:

  • Your organization has fewer than 30 accounts total
  • All environments share the same governance requirements
  • You want simpler SCP inheritance to reason about

Key Design Decisions

DecisionRecommendationRationale
Workload isolationBy product, nested by environmentAllows environment-specific SCPs within each product boundary
Infrastructure OUSeparate from WorkloadsDifferent policy needs: shared services need broader access
Suspended OUAlways createNever delete accounts; suspend and move here first
Exceptions OUUse sparinglyFor accounts with special compliance needs that cannot fit standard OU policies
Sandbox OUBudget-cappedDeveloper experimentation with financial guardrails

Here is a CDK example for creating the additional OUs that Control Tower does not auto-create:

typescript
import * as organizations from 'aws-cdk-lib/aws-organizations';
// Control Tower creates Security OU by default (optional since 2025).// These are the additional OUs you manage via IaC.
const infraOU = new organizations.CfnOrganizationalUnit(this, 'InfraOU', {  name: 'Infrastructure',  parentId: rootId,  tags: [{ key: 'ManagedBy', value: 'control-tower' }],});
const workloadsOU = new organizations.CfnOrganizationalUnit(this, 'WorkloadsOU', {  name: 'Workloads',  parentId: rootId,});
// Nested OUs for environment separationconst prodOU = new organizations.CfnOrganizationalUnit(this, 'ProdOU', {  name: 'Production',  parentId: workloadsOU.attrId,});
const stagingOU = new organizations.CfnOrganizationalUnit(this, 'StagingOU', {  name: 'Staging',  parentId: workloadsOU.attrId,});
const devOU = new organizations.CfnOrganizationalUnit(this, 'DevOU', {  name: 'Development',  parentId: workloadsOU.attrId,});
const suspendedOU = new organizations.CfnOrganizationalUnit(this, 'SuspendedOU', {  name: 'Suspended',  parentId: rootId,});

Service Control Policies (SCPs)

SCPs are the primary guardrail mechanism. They define the maximum permissions available to accounts within an OU. Layer SCPs at different OU levels for defense-in-depth.

SCP Layering Strategy

Region Restriction

Restricting regions prevents accidental resource creation in regions you do not use. This is essential for data residency compliance.

Warning: Control Tower has its own region deny control. Do NOT create custom region deny SCPs if you are using Control Tower's built-in region deny. They can conflict. Use Control Tower's configurable region deny control instead.

If you need a custom region deny outside of Control Tower, here is the pattern. Prefer Control Tower's built-in configurable region deny control first; only use a custom SCP when you need finer-grained exclusions that the built-in control does not support:

json
{  "Version": "2012-10-17",  "Statement": [    {      "Sid": "DenyNonApprovedRegions",      "Effect": "Deny",      "NotAction": [        "iam:*",        "organizations:*",        "sts:*",        "support:*",        "billing:*",        "budgets:*",        "health:*",        "trustedadvisor:*"      ],      "Resource": "*",      "Condition": {        "StringNotEquals": {          "aws:RequestedRegion": [            "eu-central-1",            "eu-west-1",            "us-east-1"          ]        }      }    }  ]}

Note the NotAction list. Global services like IAM, Organizations, and STS must be excluded because they operate outside of regional boundaries.

Protecting Security Services

This SCP prevents anyone except Control Tower execution roles from tampering with CloudTrail:

json
{  "Version": "2012-10-17",  "Statement": [    {      "Sid": "PreventCloudTrailModification",      "Effect": "Deny",      "Action": [        "cloudtrail:StopLogging",        "cloudtrail:DeleteTrail",        "cloudtrail:UpdateTrail",        "cloudtrail:PutEventSelectors"      ],      "Resource": "*",      "Condition": {        "StringNotLike": {          "aws:PrincipalArn": [            "arn:aws:iam::*:role/AWSControlTowerExecution",            "arn:aws:iam::*:role/stacksets-exec-*"          ]        }      }    }  ]}

Apply similar patterns for AWS Config, GuardDuty, and Security Hub. The principle is the same: protect the audit trail from modification by anyone except the governance automation.

SCPs vs RCPs vs Declarative Policies

The policy landscape expanded significantly with Resource Control Policies (RCPs) and Declarative Policies. Each serves a different purpose.

Decision Framework

Comparison Matrix

FeatureSCPsRCPsDeclarative Policies
What they controlPrincipal permissions (who can do what)Resource permissions (who can access what)Service configurations
ScopeMember accountsMember accountsMember accounts
Primary use casePrevent specific API callsPrevent cross-org resource accessEnforce service defaults
Example"No one can disable CloudTrail""S3 buckets accessible only by org members""EC2 AMIs cannot be shared publicly"
Supported servicesAll AWS servicesS3, STS, KMS, SQS, Secrets Manager, ECR, OpenSearch Serverless, Cognito, CloudWatch Logs, DynamoDB (list continues to grow; check official docs for the latest)EC2, EBS, VPC (specific attributes only)
ComplexityMediumMediumLow

In practice: Use SCPs for broad permission boundaries. Use RCPs for data perimeter enforcement, specifically preventing data exfiltration to external accounts. Use declarative policies where available; they enforce configuration without the complexity of deny statements.

Note: Declarative policies are currently limited to specific configurations within EC2-related services: VPC Block Public Access, EBS Snapshot Block Public Access, EC2 AMI public sharing restrictions, and similar attribute-level controls. Other services are planned for future support.

Tip: AWS recommends declarative policies over SCPs when a declarative policy exists for your use case. Declarative policies are simpler to manage and less likely to cause unintended access denials.

Account Factory for Terraform (AFT)

AFT automates account provisioning with GitOps-driven workflows. If your organization uses Terraform, AFT is the production-grade choice for account lifecycle management.

AFT Architecture

Module Setup

hcl
module "aft" {  source = "github.com/aws-ia/terraform-aws-control_tower_account_factory"
  # Control Tower management account  ct_management_account_id    = "111111111111"  log_archive_account_id      = "222222222222"  audit_account_id            = "333333333333"  aft_management_account_id   = "444444444444"
  # Terraform distribution  terraform_distribution = "oss"  # or "tfc" for Terraform Cloud  terraform_version      = "1.9.8"  # AFT requires >=1.6.0; check AFT GitHub repo for latest compatible version
  # VCS configuration  vcs_provider = "github"
  # Cost optimization: disable VPC if not needed  aft_enable_vpc = false  # saves ~$32/month in NAT Gateway costs
  # Feature flags  aft_feature_cloudtrail_data_events      = false  aft_feature_enterprise_support          = false  aft_feature_delete_default_vpcs_enabled = true
  tags = {    ManagedBy   = "aft"    Environment = "management"  }}

Note: AFT requires a dedicated management account, separate from the Control Tower management account. Budget for this additional account and its infrastructure costs (~$32-50/month).

Account Requests

Account requests are Terraform modules stored in a Git repository. Each module defines an account with its parameters and tags:

hcl
module "product_a_production" {  source = "./modules/aft-account-request"
  control_tower_parameters = {    AccountEmail              = "[email protected]"    AccountName               = "product-a-production"    ManagedOrganizationalUnit = "Workloads/Production"    SSOUserEmail              = "[email protected]"    SSOUserFirstName          = "Cloud"    SSOUserLastName           = "Admin"  }
  account_tags = {    Environment = "production"    Product     = "product-a"    CostCenter  = "eng-product-a"    ManagedBy   = "aft"  }
  account_customizations_name = "production-baseline"
  change_management_parameters = {    change_requested_by = "platform-team"    change_reason       = "New production account for Product A"  }}

Account Customizations

Account customizations apply a baseline configuration to every new account. This is where you enforce security defaults:

hcl
# account-customizations/production-baseline/terraform/main.tf
# Enforce EBS encryption by defaultresource "aws_ebs_encryption_by_default" "enabled" {  enabled = true}
# Create standard IAM roles for cross-account CI/CD accessresource "aws_iam_role" "deployment_role" {  name = "deployment-role"  assume_role_policy = jsonencode({    Version = "2012-10-17"    Statement = [{      Effect = "Allow"      Principal = {        AWS = "arn:aws:iam::${var.cicd_account_id}:role/pipeline-role"      }      Action = "sts:AssumeRole"      Condition = {        StringEquals = {          "aws:PrincipalOrgID" = var.org_id        }      }    }]  })}
# Enable GuardDuty (delegated admin handles centrally,# but ensure it is not disabled locally)resource "aws_guardduty_detector" "main" {  enable = true}
# Use separate feature resources instead of deprecated datasources blockresource "aws_guardduty_detector_feature" "s3_logs" {  detector_id = aws_guardduty_detector.main.id  name        = "S3_DATA_EVENTS"  status      = "ENABLED"}
resource "aws_guardduty_detector_feature" "eks_audit" {  detector_id = aws_guardduty_detector.main.id  name        = "EKS_AUDIT_LOGS"  status      = "ENABLED"}
resource "aws_guardduty_detector_feature" "malware" {  detector_id = aws_guardduty_detector.main.id  name        = "EBS_MALWARE_PROTECTION"  status      = "ENABLED"}

AFT vs Native Account Factory

AspectAFT (Terraform)Native Account Factory
Provisioning modelGitOps (Terraform)Service Catalog / Console
CustomizationFull Terraform flexibilityCloudFormation blueprints
Learning curveHigher (Terraform + AFT knowledge)Lower
Dedicated accountRequiredNot needed
Cost~$32-50/month for AFT infraMinimal
Best forTeams using Terraform, >5 accounts/quarterSmall orgs, CloudFormation shops

IAM Identity Center Cross-Account Access

IAM Identity Center (formerly AWS SSO) provides centralized workforce identity with permission sets mapped to accounts and OUs. This replaces the pattern of creating IAM users in each account.

Permission Set Design

Design permission sets around roles (Admin, PowerUser, ReadOnly) and functions (DBA, SecurityAudit). Map them to accounts based on the principle of least privilege.

GroupProductionStagingDevelopmentSandbox
Platform TeamAdminAccessAdminAccessAdminAccessAdminAccess
DevelopersReadOnlyAccessPowerUserAccessPowerUserAccessAdminAccess
Security TeamSecurityAuditSecurityAuditSecurityAuditSecurityAudit
DBAsDatabaseAdminDatabaseAdminDatabaseAdmin--

Here is a CDK example for a read-only permission set that gives developers production visibility without secrets access:

typescript
import * as sso from 'aws-cdk-lib/aws-sso';
const devProdReadOnly = new sso.CfnPermissionSet(this, 'DevProdReadOnly', {  instanceArn: ssoInstanceArn,  name: 'DeveloperProductionReadOnly',  description: 'Read-only access for developers in production accounts',  sessionDuration: 'PT4H', // 4-hour sessions for production  managedPolicies: [    'arn:aws:iam::aws:policy/ReadOnlyAccess',  ],  inlinePolicy: {    Version: '2012-10-17',    Statement: [      {        Sid: 'AllowCloudWatchInsights',        Effect: 'Allow',        Action: [          'logs:StartQuery',          'logs:GetQueryResults',          'logs:StopQuery',          'cloudwatch:GetMetricData',          'xray:GetTraceSummaries',        ],        Resource: '*',      },      {        Sid: 'DenySecretsAccess',        Effect: 'Deny',        Action: [          'secretsmanager:GetSecretValue',          'ssm:GetParameter*',        ],        Resource: '*',      },    ],  },});
// Assign to developer group for production accountsnew sso.CfnAssignment(this, 'DevProdAssignment', {  instanceArn: ssoInstanceArn,  permissionSetArn: devProdReadOnly.attrPermissionSetArn,  principalId: developerGroupId,  principalType: 'GROUP',  targetId: prodAccountId,  targetType: 'AWS_ACCOUNT',});

Access Patterns

Session duration strategy: Use shorter sessions for production (4 hours) and longer sessions for development (8 hours). This reduces the window of exposure for production credentials.

Federation with external IdP: If your organization uses Okta, Microsoft Entra ID, or Google Workspace, federate with IAM Identity Center rather than managing a separate directory. This gives you a single source of truth for identity. Configure attribute mapping carefully in the SAML federation; group names on the IdP side directly affect permission set assignments in AWS.

SCIM auto-provisioning: If you use an external IdP, enable SCIM provisioning. When a user is disabled in Entra ID or Okta, SCIM automatically removes their access in IAM Identity Center. Without SCIM, access for users removed from the IdP remains open until manually cleaned up.

MFA enforcement strategy: Decide whether to enforce MFA on the IdP side or the AWS side. With an external IdP, enforcing MFA at the IdP is usually more practical; Entra ID's Conditional Access or Okta's authentication policies offer more granular controls like device compliance, location-based restrictions, and risk-based MFA. Enforcing MFA on the AWS side adds a second layer but negatively impacts user experience.

Temporary elevated access (just-in-time): Use IAM Identity Center's temporary elevated access feature to give developers time-limited production access. Instead of permanent admin access, use permission set assignments triggered by an approval workflow that expire automatically. This significantly reduces the risk of "always-on" access.

Break-glass access: Create a dedicated emergency admin role secured behind hardware MFA. Store credentials in a physical safe or secrets vault. Test the break-glass procedure quarterly. Untested emergency access is no emergency access at all. This is even more critical with an external IdP; break-glass roles must be IdP-independent so that an IdP outage doesn't completely cut off AWS access.

IAM Access Analyzer: Use Access Analyzer to validate cross-account access and find unintended permissions. It can identify resources shared outside your organization, which is especially valuable in a multi-account setup.

Warning: Using an external IdP creates a single external dependency point. During an IdP outage, no user can access AWS. Keep break-glass access IdP-independent, monitor SCIM synchronization errors, and document IdP failover scenarios.

Centralized Logging and Security Architecture

Control Tower sets up a centralized logging architecture by default. The Log Archive account receives CloudTrail logs, and the Audit account aggregates security findings.

Key architectural decisions:

  • Organization-level CloudTrail trail: A single trail in the management account with logs centralized in Log Archive. This replaces per-account trails and saves significant storage costs.
  • Delegated admin: Use the Audit account as delegated admin for Security Hub, GuardDuty, and Config. Keep the management account clean.
  • S3 bucket policies: On Log Archive, use the aws:SourceOrgID condition to restrict access to your organization only.
  • Log retention: Use S3 Intelligent-Tiering for archived logs to optimize storage costs over time.
  • Auto-remediation: Set up EventBridge rules in the Audit account to trigger Lambda functions for common findings like public S3 buckets or open security groups.

Tip: If you have account-level trails AND enable the organization trail, you pay for duplicate log storage. Remove account-level trails after enabling the organization trail unless you specifically need data events at the account level.

Control Types: Preventive, Detective, Proactive

Control Tower offers three types of controls. Each serves a different purpose in the governance lifecycle.

Rollout Strategy

Rolling out controls requires a phased approach. Enabling preventive controls on production without testing is a recipe for outages.

Phase 1: Mandatory controls (automatic). Control Tower enables these by default. Do not disable them. Examples: CloudTrail enabled, log archive bucket encryption, Config enabled.

Phase 2: Strongly recommended controls. Enable on Production OU first, then expand. These are mostly detective controls; they flag violations but do not block. Examples: S3 bucket encryption, RDS encryption, EBS encryption.

Phase 3: Custom preventive controls. Test in Sandbox OU before applying to Workloads. Always communicate before enabling preventive controls on existing accounts. Examples: region deny, prevent root account usage, restrict instance types in dev.

Phase 4: Proactive controls. CloudFormation hooks that validate resources before provisioning. Limitation: only works for CloudFormation-deployed resources (not console, CLI, or Terraform). Pair them with detective controls for full coverage.

Warning: Proactive controls only work with CloudFormation. If your team uses Terraform or deploys via CLI, proactive controls will not catch those resources. You need detective controls as a safety net.

Cost Management Across Accounts

Governance has a cost. Understanding it helps you budget appropriately and avoid surprises.

Governance Costs

Control Tower itself has no direct fee. The cost comes from underlying services:

ServiceApproximate CostNotes
AWS Config (baseline rules)$3-5/account/monthDepends on resource count and rule count
CloudTrail (organization trail)S3 storage costsSingle trail vs per-account saves significantly
Config rule evaluations~$0.001/evaluation20 rules x 500 items = ~$10/account/month
AFT infrastructure~$32-50/monthNAT Gateway, CodeBuild, DynamoDB
Security HubVaries by resource countResource-based pricing; see pricing page

Cost optimization tips:

  • Use organization-level Config conformance packs instead of per-account rules where possible
  • Set aft_enable_vpc = false to save NAT Gateway costs when AFT does not need VPC access
  • Remove duplicate account-level CloudTrail trails after enabling the organization trail
  • Use S3 Intelligent-Tiering for log archive buckets

Enforcing Cost Tags

This SCP denies resource creation without a required CostCenter tag. It ensures every resource can be attributed to a team:

json
{  "Version": "2012-10-17",  "Statement": [    {      "Sid": "RequireCostAllocationTags",      "Effect": "Deny",      "Action": [        "ec2:RunInstances",        "rds:CreateDBInstance",        "lambda:CreateFunction",        "ecs:CreateCluster"      ],      "Resource": "*",      "Condition": {        "Null": {          "aws:RequestTag/CostCenter": "true"        }      }    }  ]}

Pair this with AWS Budgets per account and per OU. For sandbox accounts, set aggressive budget caps and restrict expensive service types via SCPs.

Common Pitfalls and Lessons Learned

Control Tower deployments tend to hit the same pitfalls repeatedly. Here are the ones that cause the most pain:

1. Do NOT modify Control Tower-created SCPs. Control Tower manages its own SCPs. Modifying them can cause controls to enter an "unknown" state, requiring a landing zone reset. Create new custom SCPs and attach them alongside instead.

2. Region deny conflicts. Creating custom region deny SCPs when Control Tower's region deny control is already enabled causes conflicts. Use Control Tower's configurable region deny control instead.

3. Management account is NOT protected by SCPs. SCPs do not apply to the management account. This account should be used only for billing and organizational management; never for workloads. Lock it down with IAM policies and MFA.

4. VCS provider selection for AFT. While CodeCommit is available, many teams prefer GitHub, Bitbucket, or GitLab via CodeConnections for better collaboration workflows. Choose the VCS provider that aligns with your existing toolchain.

5. CloudTrail duplicate logging costs. Account-level trails AND the organization trail means duplicate log storage costs. Remove account-level trails after enabling the organization trail.

6. Config rule evaluation costs scale non-linearly. With 20+ controls enabled and hundreds of resources per account, Config costs can surprise you. Monitor with Cost Explorer filtered by the Config service.

7. Landing zone updates are one-way. After updating your landing zone version, you cannot downgrade. Review release notes carefully before upgrading.

8. Proactive controls only work with CloudFormation. They do not catch resources created via console, CLI, or Terraform. Always pair them with detective controls.

9. Nested OU depth vs complexity. AWS supports 5 levels of nesting, but 2-3 levels is the practical maximum. Deeper nesting makes SCP inheritance harder to debug.

10. AFT dedicated account is mandatory. AFT requires its own AWS account. Budget for this additional account and its infrastructure costs.

Key Takeaways

  1. Control Tower is the starting point, not the destination. It gives you a well-architected baseline, but you will customize extensively with custom SCPs, AFT, and additional controls.

  2. OU structure reflects your governance model. Design OUs around policy boundaries, not your org chart.

  3. SCPs, RCPs, and declarative policies are complementary. Use SCPs for action restrictions, RCPs for resource access boundaries, declarative policies for service configuration enforcement.

  4. AFT is the production-grade choice for Terraform teams. If your organization uses Terraform, AFT provides GitOps-driven account provisioning that scales.

  5. Centralized logging is non-negotiable. Organization-level CloudTrail, Config aggregation, and Security Hub delegation should be day-one priorities.

  6. Governance has a cost. Budget $3-10/account/month for Config, CloudTrail, and S3 storage. This is the cost of compliance.

  7. Start with detective controls, graduate to preventive. Flag violations before blocking them. This builds organizational buy-in and prevents outages from overly aggressive guardrails.

  8. The management account is sacred. No workloads, minimal access, MFA everywhere.

Note: Many patterns in this post assume Landing Zone 3.x behavior (auto-created Security OU, baseline controls). If you are on Landing Zone 4.0+, verify OU structure and baseline control behavior against the latest Control Tower release notes. Key changes include optional Security OU, updated baseline controls, and new customization options.

References

Related Posts