A Terraform landing zone: the multi-account AWS foundation that application teams deploy onto. It covers AWS Organizations with guardrails, centralized tamper-resistant logging, organization-wide threat detection, a segmented hub-and-spoke network with shared egress, single sign-on for people, and keyless deploys for pipelines. All of it is driven by three YAML files and checked in CI without an AWS account.
Stack: Terraform 1.13 (AWS provider 6.x, native terraform test with a mocked provider) · AWS
Organizations, SCPs, tag policies · IAM Identity Center · CloudTrail, Config, GuardDuty, Security Hub,
IAM Access Analyzer · Transit Gateway, IPAM, RAM · Python 3.13 (platformctl) · tflint · Checkov ·
GitHub Actions
Status: designed and tested, not deployed. Nothing here has been applied to a real AWS organization. Every module is validated and unit-tested against a mocked provider, linted, and security-scanned in CI. docs/OPERATIONS.md describes the bootstrap for a real organization.
Once more than one team builds on AWS, a single account stops working:
- No blast-radius control. Production and experiments share IAM, quotas and a bill.
- Weak audit evidence. Logs live in the same account an attacker or a mistake could delete them from.
- Network sprawl. Every team builds its own VPC with its own NAT gateways, and address ranges overlap, so networks can't be connected later.
- Long-lived credentials. IAM users and access keys for people and CI pipelines end up in laptops and CI secrets.
- Drift and inconsistency. Each account is set up by hand, slightly differently, with no record of why.
Organizations need a foundation where adding an account is a reviewed pull request, every account is born with the same security controls, and some things simply can't be done by anyone:
- disabling logging,
- leaving the organization,
- using unapproved regions,
- production talking to development.
| Capability | How |
|---|---|
| Accounts as code | Accounts, OUs and their owners, budgets and VPC sizes live in config/organization.yaml. Terraform vends the accounts; removing an entry never closes an account |
| Guardrails | 8 service control policies (deny-list): keep CloudTrail, Config, GuardDuty and Security Hub on; protect platform roles; region allow-list; no IAM users or access keys; IMDSv2 and encrypted volumes; protect production backups; sandbox spending limits; suspended accounts can do nothing. A tag policy enforces owner, environment and cost-center |
| Tamper-resistant logs | An organization CloudTrail (all accounts, all regions, log-file validation), AWS Config and VPC flow logs all go to one log-archive account. The bucket has S3 Object Lock, a dedicated KMS key, and a policy that only this organization's logging services can write to and only platform roles can change |
| Threat detection | GuardDuty (S3, EKS, malware, RDS) and Security Hub (AWS Foundational Security Best Practices + CIS 3.0), auto-enabled in every new account and administered from a dedicated security account. Findings from all regions are aggregated. Critical findings and public-access findings page the security team. Eight CIS alarms watch the organization trail (root use, guardrail changes, trail tampering…) |
| Segmented network | IPAM pools per region and environment (no overlapping VPCs, ever). A Transit Gateway with one route table per domain, where production and non-production can't reach each other, enforced with blackhole routes against hairpinning. Central egress (one set of NAT gateways for the whole organization). A workload VPC module with private, data and transit subnets, gateway endpoints and flow logs |
| People: single sign-on | IAM Identity Center permission sets (PlatformAdmin with 1-hour sessions, SecurityAudit, Developer with a network/IAM fence, ReadOnly, BillingViewer). Group-to-account assignments come from config/identity.yaml, including ou:<Name> and all targets. Engineers are read-only in production; only on-call can change it |
| Pipelines: no keys | Per-repository GitHub Actions OIDC roles, bound to the matching GitHub environment. They are capped by a permissions boundary that any role they create must also carry, so a pipeline can't escalate its own privileges |
| Secure defaults per account | EBS encryption, an S3 account-level public access block, IMDSv2 by default, a strict password policy, Config recording, and a monthly budget with forecast alerts |
flowchart TB
subgraph mgmt[Management account]
ORG[AWS Organizations<br/>OUs · SCPs · tag policy]
TRAIL[Organization CloudTrail<br/>+ CIS metric alarms]
SSO[IAM Identity Center]
end
subgraph sec[Security OU]
LA[log-archive<br/>Object Lock · KMS · 7-year retention]
SA[security<br/>GuardDuty · Security Hub · Config aggregator · Access Analyzer]
end
subgraph infra[Infrastructure OU]
NET[network<br/>IPAM · Transit Gateway · central egress]
SS[shared-services]
end
subgraph wl[Workloads OU]
P[Production<br/>payments-prod]
NP[NonProduction<br/>payments-staging · analytics-dev]
end
SB[Sandbox OU<br/>sandbox-research]
TRAIL -->|all accounts| LA
P & NP & SS & NET -->|Config · flow logs| LA
P & NP & SB -.->|findings| SA
P <-->|TGW: prod domain| NET
NP <-->|TGW: non-prod domain| NET
NET -->|NAT| INTERNET((Internet))
SSO -.->|permission sets| P & NP & SB
flowchart LR
RP[production route table] -->|0.0.0.0/0| EG[egress VPC · NAT]
RN[nonproduction route table] -->|0.0.0.0/0| EG
RS[shared route table] -->|0.0.0.0/0| EG
RP <-->|propagated| RS
RN <-->|propagated| RS
RP -->|10.32.0.0/12 → blackhole| BH1[dropped]
RN -->|10.16.0.0/12 → blackhole| BH2[dropped]
Details: docs/DESIGN.md · decisions: docs/adr/ · the generated inventory of accounts, guardrails, access and addresses: docs/generated/inventory.md.
config/ organization.yaml · network.yaml · identity.yaml ← the platform, as data
policies/scp/ service control policies (JSON, deny-list)
policies/iam/ inline policies for permission sets
modules/ organization · log-archive · org-trail · security-baseline · account-baseline ·
network-hub · workload-vpc · identity-center (each with tests/*.tftest.hcl)
live/ one Terraform root per account type: management · log-archive · security · network · workload
tools/platformctl validate · plan-cidrs · cost · inventory
docs/ design, operations runbook, ADRs, known limitations, generated inventory
| Gate | What it proves |
|---|---|
platformctl validate --strict |
The config respects AWS hard limits that would otherwise fail half-way through an apply: • 5 SCPs per target, including FullAWSAccess• 5,120 characters per SCP • account root e-mail length It also checks the design rules: • deny-only SCPs • prod/non-prod isolation • no pool or reserved-range overlap • pool capacity • tag-policy values • identity references and session limits |
terraform test (8 modules, 32 tests) |
Each module behaves as designed, against a mocked AWS provider in plan mode. No credentials, no API calls, no cost. Examples: production blackholes the non-production pool; the log bucket only accepts this organization's services; deploy roles carry the boundary; removing an account never closes it |
terraform validate on every live root |
The roots wire the modules together correctly |
| tflint · Checkov | Lint and security scan; every Checkov skip carries its justification inline (19 skips, 0 failures) |
platformctl inventory --check |
The documented inventory matches the config |
| gitleaks | No secrets anywhere in the history |
pip install -e "tools[dev]"platformctl validate # all checks on config/ and policies/platformctl plan-cidrs # which addresses each workload VPC will get, subnet by subnetplatformctl cost # what the platform itself costs per month, line by linecd modules/network-hub && terraform init && terraform test # Terraform >= 1.11Baseline cost of the platform itself: about $660 per month for this 9-account example, before any workload runs. The largest items are Transit Gateway attachments, GuardDuty, Security Hub, Config, IPAM and the NAT gateways. The breakdown with every unit price is in the generated inventory. Ways to reduce it are in docs/OPERATIONS.md.
| docs/DESIGN.md | Account structure, guardrails, logging, detection, network, identity, threat model |
| docs/OPERATIONS.md | Bootstrap order, adding and closing accounts, break-glass, incident runbooks, cost |
| docs/ONBOARDING.md | For application teams: requesting an account, deploying from GitHub Actions, networking rules |
| docs/adr/ | Architecture decision records |
| docs/known-limitations.md | What is not covered yet |
| docs/generated/inventory.md | Accounts, guardrails per account, access matrix, address plan, cost (generated) |