Building a Multi-Account AWS Landing Zone

One AWS account with tag-based separation concentrates blast radius and fails audits. A landing zone design for 10-80 engineer teams: OU topology, service control policies, identity, networking restraint, and the Control Tower decision, with a reference template.

Template included

Landing zone reference kit

Copy as markdown to paste into your repo, or download a branded PDF for sharing with non-technical stakeholders.

Download PDF

The Problem

The single-account AWS estate is the default outcome of a startup that shipped fast. Production, staging, the CI runners, a data engineer’s Redshift experiment, and an abandoned marketing microsite all live in account 481516234200, separated by naming prefixes and a tagging policy that was enforced for about six weeks in 2023. The security boundary between staging and production is IAM policy syntax. Somewhere in the 140 roles that have accumulated, at least one carries a Resource: "*" nobody has audited, and a compromised staging workload can reach production database snapshots through it.

The damage shows up on three fronts. Security: blast radius. A leaked CI token hands an attacker the entire company, because the account is the only hard wall AWS gives you and everything lives inside it. Operations: shared quotas. The load test that exhausted Lambda concurrency took production down with it, and the same goes for EC2 vCPU limits, EIPs, and API rate limits, all of which are account-scoped. Audit: the SOC 2 assessor asks how production is isolated from development, and the honest answer is “a tag.” For a HIPAA workload that answer gets expensive, because environment separation, access minimization, and tamper-evident logging are things assessors expect to see structurally, not procedurally.

The fix has been stable for years: AWS Organizations, one account per isolation boundary, grouped into OUs, constrained by service control policies. What keeps teams at 15 to 40 engineers from doing it is the memory of what multi-account used to mean: credential juggling, thirteen browser profiles, cross-account confusion. That era ended. IAM Identity Center makes many accounts feel like one login, and Organizations makes accounts nearly free to create. The remaining cost is design: which accounts, which OUs, which guardrails. That design is the landing zone, and getting it approximately right at 20 engineers is dramatically cheaper than untangling one account at 60.

The Approach

Five OUs, roughly ten accounts. The diagram is the architecture; the subsections argue for it.

Management account

billing + org admin

nothing runs here

Security OU

Infrastructure OU

Workloads OU

Sandbox OU

log-archive

org CloudTrail, Object Lock

no human logins

security-tooling

GuardDuty, Security Hub

delegated admin

network

peering/TGW, IPAM, egress

shared-services

CI runners, ECR, tooling

prod

staging

sandbox-*

budget-capped, nuked nightly

One account per blast radius

A VPC separates networks. An account separates everything: IAM principals, S3 buckets, KMS keys, Lambda functions, DynamoDB tables, service quotas, billing, and the scope of a stolen credential. A large fraction of AWS never touches a VPC at all, which is exactly why VPC-only separation fails quietly. Your “isolated” staging environment shares an IAM universe with production, and one over-permissive role is a bridge. Between accounts, every access path must be explicit: a resource policy or an assumed role, both of which are visible, auditable, and revocable. “Who can touch production” stops being a policy-analysis research project and becomes a list of principals in one account. That is a sentence you can put in front of an auditor.

The account roles, briefly. The management account runs nothing: no VPCs, no workloads, no data buckets. SCPs do not apply to it (more below), so anything running there is ungoverned by definition. It exists for billing and org administration, and its root credentials live in a safe. The Security OU holds two accounts. The log-archive account receives the organization CloudTrail and Config history into an S3 bucket with Object Lock in compliance mode; nobody logs in here, and even an administrator cannot delete a log object before its retention expires. The security-tooling account is delegated administrator for GuardDuty, Security Hub, and IAM Access Analyzer, and is where a security engineer actually works. The Infrastructure OU holds the network account (connectivity, IPAM, egress) and shared-services (CI runners, ECR, internal tooling). The Workloads OU holds prod and staging as separate accounts, not separate VPCs, so that promotion through environments exercises the same IaC against genuinely identical topology. The Sandbox OU is where the freedom you removed from production goes: per-engineer accounts with a $200 budget alarm, no connectivity to anything, wiped nightly with aws-nuke.

That is ten accounts. Resist the urge to mint one per microservice; at 10-80 engineers, account sprawl recreates the original governance problem with extra steps.

Service control policies: a deny-list, not permissions

SCPs are the most misunderstood piece of Organizations. They grant nothing. They are a ceiling: an action is allowed only if IAM policy grants it AND no SCP denies it. The default FullAWSAccess policy stays attached everywhere, and you layer denies on top. And they never apply to the management account, which is the technical reason nothing may run there. The guardrails you spent a week designing simply do not exist in that account.

A starter set that covers most of the risk for a regulated SMB: deny leaving the organization, deny tampering with CloudTrail, deny root-user actions, restrict regions to the ones you operate in, and protect the log-archive bucket’s configuration. The first three in one policy, attached at the root:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "DenyLeavingOrg",
      "Effect": "Deny",
      "Action": "organizations:LeaveOrganization",
      "Resource": "*"
    },
    {
      "Sid": "DenyCloudTrailTamper",
      "Effect": "Deny",
      "Action": [
        "cloudtrail:StopLogging",
        "cloudtrail:DeleteTrail",
        "cloudtrail:UpdateTrail",
        "cloudtrail:PutEventSelectors"
      ],
      "Resource": "*"
    },
    {
      "Sid": "DenyRootUser",
      "Effect": "Deny",
      "Action": "*",
      "Resource": "*",
      "Condition": {
        "StringLike": { "aws:PrincipalArn": "arn:aws:iam::*:root" }
      }
    }
  ]
}

Two operational cautions. First, stage every SCP through a low-stakes OU before it reaches Workloads; the AccessDenied error message now names the offending SCP, but debugging inheritance across nested OUs is still tedious at 5 p.m. on a Friday. Second, mind the 5-policies-per-OU and quota-limited document size; consolidate statements rather than attaching a policy per rule.

Control Tower or Terraform

Control Tower gives you a working landing zone in an afternoon: org CloudTrail, Config baseline, a curated guardrail catalog, and Account Factory for vending accounts. The costs arrive later. It is opinionated about OU structure, its guardrails drift-check against their own model rather than yours, landing zone version upgrades are an event, and Account Factory provisions through Service Catalog, which is slow and awkward to drive from automation. AFT (Account Factory for Terraform) exists to fix that last problem and brings four repositories, CodePipeline, CodeBuild, and a DynamoDB table of its own. At this scale, AFT tends to be the worst of both worlds: Control Tower’s rigidity plus a bespoke pipeline someone must maintain.

Rolling your own means aws_organizations_organization, aws_organizations_organizational_unit, aws_organizations_account, and aws_organizations_policy in plain Terraform, and owning the baseline yourself: the org trail, the Config aggregator, GuardDuty org enrollment, Identity Center assignments. More upkeep, full control, and the entire org becomes a single terraform plan whose drift your existing drift detection tooling already watches. The org definition for a ten-account estate is a few hundred lines.

The recommendation for a 10-80 engineer regulated SMB: if you already run Terraform through CI and one engineer owns platform, roll your own and skip Control Tower entirely. The org config will be managed exactly like everything else you operate, which matters when an auditor asks for change history. Choose Control Tower only if platform ownership is genuinely nobody’s job yet and you are willing to live inside its rails as the price of AWS maintaining the baseline. Do not adopt AFT at this scale.

Identity: one door, many rooms

IAM Identity Center from day one, federated to Google Workspace or Entra, with SCIM sync so offboarding cascades automatically. Zero per-account IAM users for humans. Access is expressed as permission sets assigned to groups per account: ReadOnly everywhere, Developer (write in staging, read in prod), PlatformAdmin, SecurityAudit. Session duration is a control, not a default: 8 hours for staging work, 1 hour for anything administrative in prod. The compliance dividend is real: a quarterly access review becomes an export of assignments per account instead of an archaeology dig through IAM users, and that export feeds your evidence pipeline directly.

Break-glass is the exception that proves the design. Two IAM users in the management account, hardware MFA, credentials sealed offline, and an EventBridge rule on their console logins that pages whoever is on call. Test the procedure twice a year and log that you did; an untested break-glass account is a locked fire exit.

Networking: peer first, TGW when it earns it

Interrogate the mesh before building it. At this scale the honest connectivity requirement is usually shared-services to prod and shared-services to staging: two VPC peering connections, zero hourly cost. Transit Gateway runs about $36 per attachment per month plus $0.02/GB processed, and it earns that when you have four or more VPCs needing mutual reachability, a VPN or Direct Connect hub, or a centralized egress requirement. Centralized egress through the network account is genuinely attractive for regulated workloads (one choke point for flow logs and inspection) but it adds a data-processing toll and a shared failure domain, so let an assessor finding or an incident justify it rather than building it speculatively.

The one thing that is a day-one obligation: a non-overlapping CIDR plan, in IPAM or a spreadsheet, covering accounts you have not created yet. Nearly every other networking decision here is reversible. Overlapping CIDRs are not, and they surface at the worst moment, usually mid-migration.

Landing the plane

Done is testable. All human access flows through Identity Center and the old IAM users are deleted. The org CloudTrail lands in log-archive, and an administrator attempting to permanently delete a log object version (a delete with --version-id) gets denied by Object Lock (run this test; the failure is the pass). An unversioned delete merely adds a delete marker, and the data remains recoverable underneath it. SCPs are attached and have been exercised from a workload account, not just written. The production workload actually runs in the prod account: an org where the accounts exist but the monolith still lives in the old account is scaffolding, not a landing zone. Budget alarms exist per account, and the org definition itself sits under drift detection.

Two connections worth making explicit. This topology is the grown-up version of the three-account split in minimum viable HIPAA on AWS; if you built that, you retrofit into this by adding OUs and SCPs around accounts you already have. And the org trail flowing into log-archive is the raw feed for an evidence pipeline, which is where this architecture starts paying for itself every audit cycle.

The Template

The OU map, the two SCPs not shown above, and the Terraform repo layout.

Root
├── Management account        billing + org admin; runs nothing; root creds in a safe
├── Security OU
│   ├── log-archive           org CloudTrail + Config -> S3, Object Lock (compliance mode)
│   └── security-tooling      GuardDuty / Security Hub / Access Analyzer delegated admin
├── Infrastructure OU
│   ├── network               peering or TGW, IPAM, (later) central egress
│   └── shared-services       CI runners, ECR, artifact storage, internal tools
├── Workloads OU
│   ├── prod                  1h admin sessions, tightest SCPs
│   └── staging               mirrors prod topology; SCP canary target
└── Sandbox OU
    └── sandbox-<engineer>    $200 budget alarm, no peering, aws-nuke nightly

Region restriction, attached at the root (adjust the region list and the exempt global services to taste):

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "DenyOutsideAllowedRegions",
      "Effect": "Deny",
      "NotAction": [
        "iam:*", "organizations:*", "sts:*", "cloudfront:*",
        "route53:*", "support:*", "budgets:*", "health:*"
      ],
      "Resource": "*",
      "Condition": {
        "StringNotEquals": {
          "aws:RequestedRegion": ["us-east-1", "us-west-2"]
        }
      }
    }
  ]
}

Log-archive protection, attached to the Security OU. The pipeline role is exempted; otherwise this SCP would also block the Terraform stack that manages the bucket’s versioning, lock, and lifecycle configuration, and day-2 applies would fail:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "ProtectLogArchiveBucket",
      "Effect": "Deny",
      "Action": [
        "s3:DeleteBucket",
        "s3:DeleteBucketPolicy",
        "s3:PutBucketVersioning",
        "s3:PutBucketObjectLockConfiguration",
        "s3:PutLifecycleConfiguration"
      ],
      "Resource": "arn:aws:s3:::org-log-archive-*",
      "Condition": {
        "ArnNotLike": {
          "aws:PrincipalArn": "arn:aws:iam::*:role/terraform-pipeline"
        }
      }
    }
  ]
}

Terraform repo layout, one state per stack, applied by a pipeline role:

terraform/
├── backend.hcl
├── org/                      # runs against the management account
│   ├── main.tf               # organization, OUs, member accounts
│   ├── scps.tf               # policy documents + OU attachments
│   └── identity.tf           # Identity Center permission sets + assignments
├── security/
│   ├── log-archive/          # bucket, Object Lock, org CloudTrail
│   └── tooling/              # GuardDuty / Security Hub delegated admin
├── infrastructure/
│   └── network/              # VPCs, peering or TGW, IPAM pools
└── modules/
    ├── organizations/        # OU + account factory (tags, close-on-delete)
    ├── scp/                  # policy rendering + attachment
    ├── log-archive/          # bucket with lock, replication, lifecycle
    ├── identity/             # permission set + group assignment pattern
    └── network/              # standard VPC shape, flow logs on by default

Operating Notes

The account count wants to grow. Let it, slowly.

A new account is justified by a new blast radius worth isolating: a new compliance boundary, a workload with its own on-call, a data classification that must not share IAM space with the rest. It is not justified by a new microservice or a new team lead’s preference. Ten to fifteen accounts is the healthy range for this size of company. If you are at forty, you have rebuilt the governance problem you started with, except now it has a monthly baseline bill in every account — Config, GuardDuty, and Security Hub all meter per account — and nobody remembers what prod-svc-legacy-2 does.

SCPs are production changes

An SCP typo can lock every engineer out of every workload account simultaneously, which makes the SCP repo the highest-blast-radius code you own. Treat it accordingly: PR review with plan output, rollout through the staging OU with a soak period before it touches Workloads, and a CloudWatch alarm on organizations:* mutations from any principal that is not the pipeline role. The upside in a regulated shop is that this discipline is self-documenting. Your SCP git history, with reviews and timestamps, is change-control evidence an auditor accepts without a follow-up meeting.

Retrofitting under a live workload

The landing zone is designed so production never has to move. Invite the existing account into the org and place it under the Workloads OU as prod, exactly where it belongs. Build the new accounts around it: log-archive first (start capturing the org trail immediately, since evidence accrues from day one), then Identity Center, then shared-services, and migrate CI runners and staging out of the old account at leisure. Apply SCPs to the staging OU, watch for a week, then extend to prod. The anti-pattern is the migration weekend: a planned big-bang move of production into a “clean” account, which converts a governance project into an availability incident. Prod stays put. The org grows around it.

Still running everything in one account?

A landing zone is a three-week project with the right hands, and a three-year regret without them.

Kaan Systems runs fractional platform engineering for regulated SMBs. We stand up landing zones greenfield and retrofit them under live HIPAA and SOC 2 workloads, no migration weekend required. If tags are doing the work of boundaries in your AWS estate, talk to us.

Open a conversation