Landing Zone Design
Landing Zone Principles
A well-designed Landing Zone embodies several non-negotiable principles that prevent governance drift as the organization scales.
Governance from Day 0
Security controls, network segmentation, and compliance baselines are deployed before any workload. New accounts are pre-configured, not retroactively hardened.
Separation of Concerns
Each account or project has a single well-defined purpose. Security tooling, networking, logging, and workloads are isolated from each other by account/project boundaries — not just by IAM.
Least Privilege by Default
No account or service account starts with administrative access. Permissions are granted narrowly and reviewed periodically. Service Control Policies and Org Policies enforce ceilings that even account admins cannot exceed.
Infrastructure as Code
The entire Landing Zone — account structure, SCPs, Org Policies, VPCs, IAM roles, logging configuration — is expressed in Terraform and version-controlled in Git. All changes go through CI/CD review.
AWS Landing Zone and Control Tower
AWS Control Tower automates the creation of a multi-account Landing Zone built on AWS Organizations. It provisions a set of mandatory accounts, applies Service Control Policies, enables guardrails, and sets up centralized logging and audit trails.
Organizational Unit Structure
Root (Management Account)
├── Security OU
│ ├── Audit Account # CloudTrail, Config aggregator, Security Hub master
│ └── Log Archive Account # Centralized S3 log bucket (immutable, lifecycle-managed)
│
├── Infrastructure OU
│ ├── Network Account # Transit Gateway, Direct Connect, DNS
│ └── Shared Services Account # Active Directory, internal DNS, AMI catalog, Vault
│
├── Workloads OU
│ ├── Production OU
│ │ ├── prod-app-alpha # One AWS account per application/team in prod
│ │ └── prod-app-beta
│ ├── Staging OU
│ │ ├── staging-app-alpha
│ │ └── staging-app-beta
│ └── Dev OU
│ ├── dev-app-alpha
│ └── dev-app-beta
│
└── Sandbox OU
└── sandbox-engineer-name # Individual disposable accounts, strict cost limits
Service Control Policies (SCPs)
SCPs define permission boundaries at the OU or account level. They cannot grant permissions — they can only restrict the maximum permissions any principal in the attached account may exercise, regardless of IAM policies attached to that principal.
// SCP: Deny leaving the AWS Organization
// Attach at Root OU to prevent any member account from removing itself
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "DenyLeaveOrganization",
"Effect": "Deny",
"Action": [
"organizations:LeaveOrganization"
],
"Resource": "*"
}
]
}
// SCP: Require MFA for sensitive IAM actions
// Denies critical IAM mutations unless the request was made with MFA
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "DenyIAMWithoutMFA",
"Effect": "Deny",
"Action": [
"iam:CreateUser",
"iam:DeleteUser",
"iam:AttachUserPolicy",
"iam:CreateAccessKey",
"iam:UpdateAccountPasswordPolicy"
],
"Resource": "*",
"Condition": {
"BoolIfExists": {
"aws:MultiFactorAuthPresent": "false"
}
}
}
]
}
// SCP: Deny creation of resources outside approved regions
// Prevents accidental deployments to unmonitored regions
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "DenyUnapprovedRegions",
"Effect": "Deny",
"NotAction": [
"iam:*",
"organizations:*",
"route53:*",
"budgets:*",
"waf:*",
"cloudfront:*",
"sts:*",
"support:*",
"trustedadvisor:*"
],
"Resource": "*",
"Condition": {
"StringNotEquals": {
"aws:RequestedRegion": [
"ap-southeast-1",
"us-east-1"
]
}
}
}
]
}
GCP Landing Zone and Resource Manager
GCP's resource hierarchy enforces IAM and Org Policy constraints top-down. The hierarchy flows from Organization to Folders to Projects to Resources, with each level inheriting policies from all levels above it.
Folder Hierarchy
Organization: example.com
│
├── Folder: Security
│ ├── Project: security-audit-prod # Security Command Center, log export
│ └── Project: security-vault-prod # HashiCorp Vault, KMS keys
│
├── Folder: Infrastructure
│ ├── Project: net-host-prod # Shared VPC host project (prod)
│ ├── Project: net-host-nonprod # Shared VPC host project (non-prod)
│ └── Project: dns-shared # Cloud DNS, forwarding zones
│
├── Folder: Workloads
│ ├── Folder: Production
│ │ ├── Project: app-alpha-prod # App Alpha production workloads
│ │ └── Project: app-beta-prod # App Beta production workloads
│ ├── Folder: Staging
│ │ ├── Project: app-alpha-staging
│ │ └── Project: app-beta-staging
│ └── Folder: Development
│ ├── Project: app-alpha-dev
│ └── Project: app-beta-dev
│
└── Folder: Sandbox
└── Project: sandbox-engineer-name # Individual projects, budget cap applied
Organization Policies
Org Policies enforce resource configuration constraints across all projects in a folder or organization. Unlike IAM, Org Policies restrict what can be created/configured, not who can do it.
# Terraform: enforce Org Policies at the organization level
# Disable VM serial port access across the entire org
resource "google_organization_policy" "disable_serial_port" {
org_id = var.org_id
constraint = "compute.disableSerialPortAccess"
boolean_policy { enforced = true }
}
# Require OS Login on all Compute Engine instances
resource "google_organization_policy" "require_os_login" {
org_id = var.org_id
constraint = "compute.requireOsLogin"
boolean_policy { enforced = true }
}
# Restrict which regions resources can be created in
resource "google_organization_policy" "allowed_regions" {
org_id = var.org_id
constraint = "gcp.resourceLocations"
list_policy {
allow {
values = [
"in:asia-southeast1-locations",
"in:us-east1-locations",
"in:global-locations" # required for global resources (DNS, LB)
]
}
suggested_value = "asia-southeast1"
}
}
# Restrict domain of GCP identities that can be granted IAM roles
resource "google_organization_policy" "domain_restriction" {
org_id = var.org_id
constraint = "iam.allowedPolicyMemberDomains"
list_policy {
allow {
values = [
"C0xxxxxxxxx", # Your Google Workspace Customer ID
]
}
}
}
# Disable creation of default service account keys
resource "google_organization_policy" "disable_sa_key_creation" {
org_id = var.org_id
constraint = "iam.disableServiceAccountKeyCreation"
boolean_policy { enforced = true }
}
Account and Project Strategy
The following account types represent distinct functional purposes in a Landing Zone. Each type carries specific baseline controls, IAM permissions, and networking configuration.
| Account Type | Purpose | Key Controls | Who Has Access |
|---|---|---|---|
| Management / Root | AWS Organizations root; billing consolidation; Control Tower management | Restrict all workloads via SCP; no direct deployments; MFA enforced on all users | Cloud Platform team only |
| Audit | Centralized security findings aggregation (Security Hub, SCC); read-only Config aggregator | Read-only by default; no direct resource creation; immutable findings store | Security team (read), SIEM integration (write from other accounts) |
| Log Archive | Centralized, tamper-evident log storage (CloudTrail, VPC Flow Logs, access logs) | S3 Object Lock (WORM); no delete permissions; 7-year retention; access logged | Write-only from org member accounts; read by Security/Audit only |
| Network (Hub) | Transit Gateway (AWS) or Shared VPC host (GCP); Direct Connect / Interconnect termination; centralized NAT | Network changes require platform team approval; no workloads deployed here | Network/Platform team only |
| Shared Services | Internal DNS, AD/LDAP, AMI catalog, internal CA, container image registry, Vault | Hardened baseline; no internet egress except approved paths; SLA-backed | Platform team (admin); all workload accounts (read/use) |
| Production | Live customer-facing workloads | No manual console access (break-glass only); all changes via IaC pipeline; strict SCP | Applications (via service accounts/roles); on-call engineers (break-glass) |
| Staging | Pre-production environment; integration testing; performance testing | Mirrors production controls; can allow broader developer read access | Development teams (read); CI/CD pipelines (write) |
| Development | Feature development; unit/integration testing | Relaxed cost guardrails; no sensitive data; budget alerts; auto-shutdown schedules | Development teams (full access within account boundary) |
| Sandbox | Individual exploration; proof-of-concept; training | Hard budget cap ($100–500/month); auto-nuke after 30 days; no connectivity to prod | Individual engineers (full access); no shared resources |
Network Hub-and-Spoke Topology
The hub-and-spoke model centralizes shared network services (VPN, Direct Connect, DNS, NAT) in a network hub account or project. Workload VPCs (spokes) attach to the hub for transitive routing and shared egress.
AWS: Transit Gateway Hub-and-Spoke
# Terraform: Transit Gateway in the Network (Hub) account
resource "aws_ec2_transit_gateway" "hub" {
description = "Central TGW for Landing Zone"
amazon_side_asn = 64512
default_route_table_association = "disable" # Use custom route tables
default_route_table_propagation = "disable"
auto_accept_shared_attachments = "enable"
tags = {
Name = "tgw-hub"
Environment = "shared"
ManagedBy = "terraform"
}
}
# Share TGW with spoke accounts via RAM
resource "aws_ram_resource_share" "tgw_share" {
name = "tgw-hub-share"
allow_external_principals = false
}
resource "aws_ram_resource_association" "tgw" {
resource_arn = aws_ec2_transit_gateway.hub.arn
resource_share_arn = aws_ram_resource_share.tgw_share.arn
}
resource "aws_ram_principal_association" "org" {
principal = data.aws_organizations_organization.current.arn
resource_share_arn = aws_ram_resource_share.tgw_share.arn
}
# Spoke VPC attachment (runs in each workload account)
resource "aws_ec2_transit_gateway_vpc_attachment" "spoke" {
transit_gateway_id = var.transit_gateway_id # Shared from hub account
vpc_id = aws_vpc.spoke.id
subnet_ids = aws_subnet.tgw_attachment[*].id
transit_gateway_default_route_table_association = false
transit_gateway_default_route_table_propagation = false
tags = { Name = "tgw-attach-${var.env}-${var.app_name}" }
}
# Default route in spoke VPC pointing to TGW (for internet egress via hub NAT)
resource "aws_route" "default_via_tgw" {
route_table_id = aws_route_table.private.id
destination_cidr_block = "0.0.0.0/0"
transit_gateway_id = var.transit_gateway_id
}
GCP: Shared VPC
# Terraform: Shared VPC — host project and service project attachment
# Enable Shared VPC in the host project
resource "google_compute_shared_vpc_host_project" "host" {
project = var.host_project_id
}
# Attach a service project (workload) to the Shared VPC host
resource "google_compute_shared_vpc_service_project" "app_alpha" {
host_project = var.host_project_id
service_project = var.app_alpha_project_id
depends_on = [google_compute_shared_vpc_host_project.host]
}
# Grant the service project's SA permission to use specific subnets
resource "google_compute_subnetwork_iam_member" "app_alpha_subnet" {
project = var.host_project_id
region = "asia-southeast1"
subnetwork = google_compute_subnetwork.prod_app.name
role = "roles/compute.networkUser"
member = "serviceAccount:${var.app_alpha_compute_sa}"
}
# Subnet in the host project — delegated to service projects
resource "google_compute_subnetwork" "prod_app" {
project = var.host_project_id
name = "subnet-prod-app-asia-se1"
ip_cidr_range = "10.10.1.0/24"
region = "asia-southeast1"
network = google_compute_network.shared_vpc.id
private_ip_google_access = true # Enable access to Google APIs without internet
secondary_ip_range {
range_name = "gke-pods"
ip_cidr_range = "10.100.0.0/18"
}
secondary_ip_range {
range_name = "gke-services"
ip_cidr_range = "10.200.0.0/20"
}
}
Identity: Centralized IdP with SSO
All human access to cloud environments flows through a centralized Identity Provider (IdP). Direct IAM user creation in individual accounts or projects is prohibited by SCP/Org Policy.
AWS IAM Identity Center (SSO)
# Terraform: AWS IAM Identity Center — permission set and account assignment
# Create a permission set (what a user can do in an account)
resource "aws_ssoadmin_permission_set" "developer_readonly" {
name = "DeveloperReadOnly"
description = "Read-only access to developer resources"
instance_arn = local.sso_instance_arn
session_duration = "PT8H" # 8-hour session
}
resource "aws_ssoadmin_managed_policy_attachment" "ro_policy" {
instance_arn = local.sso_instance_arn
permission_set_arn = aws_ssoadmin_permission_set.developer_readonly.arn
managed_policy_arn = "arn:aws:iam::aws:policy/ReadOnlyAccess"
}
# Assign the permission set to an IdP group in a specific account
resource "aws_ssoadmin_account_assignment" "dev_team_staging" {
instance_arn = local.sso_instance_arn
permission_set_arn = aws_ssoadmin_permission_set.developer_readonly.arn
principal_id = data.aws_identitystore_group.dev_team.group_id
principal_type = "GROUP"
target_id = var.staging_account_id
target_type = "AWS_ACCOUNT"
}
# SAML 2.0 external IdP integration (e.g., Okta, Azure AD)
resource "aws_ssoadmin_identity_provider_configuration" "okta" {
instance_arn = local.sso_instance_arn
status = "ENABLED"
identity_provider_type = "EXTERNAL"
federated_identity_provider {
issuer_url = "https://your-org.okta.com"
saml_metadata = file("okta-metadata.xml")
}
}
GCP Cloud Identity and SAML/OIDC Federation
# Terraform: GCP Workforce Identity Federation (external IdP → GCP access)
# Enables users from Okta/Azure AD to log in to GCP without Google accounts
resource "google_iam_workforce_pool" "corp_pool" {
workforce_pool_id = "corp-workforce-pool"
parent = "organizations/${var.org_id}"
location = "global"
display_name = "Corporate Workforce Pool"
description = "SAML federation with corporate Okta IdP"
session_duration = "28800s" # 8 hours
}
resource "google_iam_workforce_pool_provider" "okta_saml" {
workforce_pool_id = google_iam_workforce_pool.corp_pool.workforce_pool_id
location = "global"
provider_id = "okta-saml-provider"
display_name = "Okta SAML"
attribute_mapping = {
"google.subject" = "assertion.subject"
"google.groups" = "assertion.attributes.groups"
"google.display_name" = "assertion.attributes.displayName"
"attribute.department" = "assertion.attributes.department"
}
# Only allow users from specific Okta groups to federate
attribute_condition = "'cloud-gcp-users' in google.groups"
saml {
idp_metadata_xml = file("okta-saml-metadata.xml")
}
}
# Bind a workforce identity to a GCP IAM role
resource "google_folder_iam_member" "dev_team_viewer" {
folder = var.workloads_folder_id
role = "roles/viewer"
member = "principalSet://iam.googleapis.com/${google_iam_workforce_pool.corp_pool.name}/group/cloud-developers"
}
Security Baseline
The security baseline is deployed automatically to every new account or project provisioned by the Landing Zone pipeline. It provides the minimum detective and preventive controls required before any workload can be onboarded.
AWS Security Baseline
# Terraform: baseline security services enabled in every member account
# Enable CloudTrail — management and data event logging
resource "aws_cloudtrail" "baseline" {
name = "org-baseline-trail"
s3_bucket_name = var.log_archive_bucket # In Log Archive account
include_global_service_events = true
is_multi_region_trail = true
enable_log_file_validation = true # Detect tampering via digest files
cloud_watch_logs_group_arn = "${aws_cloudwatch_log_group.trail.arn}:*"
cloud_watch_logs_role_arn = aws_iam_role.trail_cw.arn
event_selector {
read_write_type = "All"
include_management_events = true
data_resource {
type = "AWS::S3::Object"
values = ["arn:aws:s3:::"] # All S3 data events
}
}
tags = { ManagedBy = "terraform"; Classification = "security-baseline" }
}
# Enable AWS Config — resource configuration change tracking
resource "aws_config_configuration_recorder" "baseline" {
name = "baseline-config-recorder"
role_arn = aws_iam_role.config.arn
recording_group {
all_supported = true
include_global_resource_types = true
}
}
# Enable Security Hub — aggregates GuardDuty, Config, Inspector findings
resource "aws_securityhub_account" "baseline" {}
resource "aws_securityhub_standards_subscription" "cis" {
standards_arn = "arn:aws:securityhub:::ruleset/cis-aws-foundations-benchmark/v/1.4.0"
depends_on = [aws_securityhub_account.baseline]
}
# Enable GuardDuty — threat detection (VPC Flow Logs, DNS, CloudTrail analysis)
resource "aws_guardduty_detector" "baseline" {
enable = true
datasources {
s3_logs { enable = true }
kubernetes { audit_logs { enable = true } }
malware_protection { scan_ec2_instance_with_findings { ebs_volumes { enable = true } } }
}
}
GCP Security Baseline
# Terraform: GCP security services enabled for every project
# Enable Cloud Audit Logs (Data Access) — beyond default Admin Activity logs
resource "google_project_iam_audit_config" "baseline" {
project = var.project_id
service = "allServices"
audit_log_config { log_type = "ADMIN_READ" }
audit_log_config { log_type = "DATA_READ" }
audit_log_config { log_type = "DATA_WRITE" }
}
# Export audit logs to centralized log bucket in Security project
resource "google_logging_project_sink" "audit_export" {
project = var.project_id
name = "export-audit-to-security"
destination = "storage.googleapis.com/${var.log_archive_bucket}"
filter = "logName:cloudaudit.googleapis.com"
unique_writer_identity = true
}
# Enable Security Command Center Standard tier at org level (run once at org)
resource "google_scc_organization_custom_module" "cis_baseline" {
organization = var.org_id
display_name = "CIS GCP Benchmark"
enablement_state = "ENABLED"
module_config { severity = "HIGH" }
}
# VPC Service Controls perimeter (prevents data exfiltration from sensitive projects)
resource "google_access_context_manager_service_perimeter" "prod_perimeter" {
parent = "accessPolicies/${var.access_policy_id}"
name = "accessPolicies/${var.access_policy_id}/servicePerimeters/prod-perimeter"
title = "Production Data Perimeter"
status {
resources = ["projects/${var.prod_project_number}"]
restricted_services = [
"bigquery.googleapis.com",
"storage.googleapis.com",
"cloudsql.googleapis.com"
]
ingress_policies {
ingress_from {
sources { access_level = google_access_context_manager_access_level.internal_only.name }
identities = ["serviceAccount:${var.cicd_sa}"]
}
ingress_to {
resources = ["*"]
operations { service_name = "storage.googleapis.com" method_selectors { method = "*" } }
}
}
}
}
Terraform Landing Zone Module Structure
The Landing Zone is expressed as a set of Terraform modules that compose together. A root module provisions all accounts and applies baseline configuration to each. Teams never directly provision accounts — they submit a request that triggers the IaC pipeline.
# Landing Zone Terraform module layout (AWS example)
landing-zone/
├── main.tf # Root module: calls all sub-modules
├── variables.tf # Top-level inputs (org_id, log_bucket, etc.)
├── outputs.tf # Account IDs, TGW ID, SSO instance ARN
├── terraform.tfvars # Environment-specific values (not secrets)
│
├── modules/
│ ├── aws-account/ # Provision a member account via Organizations
│ │ ├── main.tf # aws_organizations_account resource
│ │ ├── variables.tf # account_name, email, ou_id, tags
│ │ └── outputs.tf # account_id
│ │
│ ├── aws-account-baseline/ # Apply security baseline to a member account
│ │ ├── main.tf # CloudTrail, Config, GuardDuty, SecurityHub
│ │ ├── iam.tf # Baseline IAM roles (ReadOnly, Admin, Break-glass)
│ │ └── variables.tf # account_id, log_archive_bucket, region
│ │
│ ├── aws-scp/ # SCP definitions and OU attachments
│ │ ├── deny_regions.json
│ │ ├── deny_leave_org.json
│ │ ├── require_mfa.json
│ │ └── main.tf # aws_organizations_policy + attachment resources
│ │
│ ├── aws-network-hub/ # Transit Gateway, Route 53 Resolver, centralized NAT
│ │ ├── main.tf
│ │ ├── tgw.tf
│ │ └── dns.tf
│ │
│ └── aws-sso/ # IAM Identity Center permission sets and assignments
│ ├── main.tf
│ └── assignments.tf
│
└── environments/
└── prod/
└── terraform.tfvars # org_id, root_email, region, approved_accounts[]
# modules/aws-account/main.tf — provision a member account
resource "aws_organizations_account" "this" {
name = var.account_name
email = var.account_email
parent_id = var.ou_id
# Prevent Terraform from closing the account on destroy
# (account closure is a deliberate, manual process)
close_on_deletion = false
tags = merge(var.common_tags, {
Name = var.account_name
AccountType = var.account_type
})
lifecycle {
# Prevent accidental deletion of production accounts
prevent_destroy = var.prevent_destroy
ignore_changes = [email] # Email cannot be changed post-creation
}
}
# Immediately apply the baseline after account creation
module "baseline" {
source = "../aws-account-baseline"
providers = {
aws = aws.member # Provider aliased to assume OrganizationAccountAccessRole in new account
}
account_id = aws_organizations_account.this.id
log_archive_bucket = var.log_archive_bucket
region = var.primary_region
security_hub_master = var.security_hub_master_account_id
}
Guardrails: Preventive vs Detective
| Type | Mechanism | AWS Implementation | GCP Implementation | Example |
|---|---|---|---|---|
| Preventive | Blocks the action before it happens. Cannot be bypassed by account-level principals. | Service Control Policies (SCPs) | Organization Policies | Deny creation of public S3 buckets; deny disabling of encryption at rest; deny use of non-approved AMIs |
| Detective | Detects and alerts on non-compliant configurations after the fact. May auto-remediate. | AWS Config Rules + Auto Remediation via Systems Manager or Lambda | Security Command Center (SCC) findings + Cloud Asset Inventory queries | Alert when an S3 bucket ACL is changed to public; flag when a firewall rule opens port 22 to 0.0.0.0/0 |
| Responsive | Automatic remediation of detected violations, or human-in-the-loop escalation workflow. | Config Auto Remediation SSM Document; Security Hub → EventBridge → Lambda | SCC → Pub/Sub → Cloud Function → remediation action | Auto-revoke an overly permissive IAM role; auto-quarantine a compromised instance; auto-rotate exposed credentials |