Landing Zone Design

What is a Landing Zone? A Landing Zone is the foundational account and project structure that enforces governance, security, and operational standards from day zero — before any workload is deployed. It is the starting configuration every team inherits when they begin using the cloud.

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 Effect: Even an account's root user is subject to SCPs. This makes SCPs the strongest preventive guardrail in AWS. SCPs are evaluated before IAM policies — a Deny in an SCP cannot be overridden by any IAM Allow.
// 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
Design principle: Maximize preventive guardrails — they are always active and require no human response. Detective guardrails are a safety net for gaps not addressed preventively. Treat detective findings as signals to promote controls to the preventive layer over time.
Next: Hybrid Connectivity — Configure VPN, Direct Connect, Cloud Interconnect, cross-cloud BGP routing, and DNS resolution to connect this Landing Zone to on-premises and to a second cloud provider.