AWS Startup Landing Zone


1. Goal#

这个方案适合新的 startup 公司直接建立 AWS 基础架构。目标是先把安全边界做好,再让 dev / uat / prod 可以快速交付。

security first:
    no workload in management account
    no long-lived IAM user access keys
    all human access through IAM Identity Center
    separate dev / uat / prod accounts
    central security account manages security services
    central log archive account stores immutable logs
    SCP blocks dangerous operations before engineers make mistakes

startup default:
    use AWS Control Tower if the company expects production, audit, compliance, or customer security review
    use the Lite version only when cost and operation overhead must be extremely low

2. Account Structure#

Account OU Purpose Human Access
management Root AWS Organizations, Control Tower, billing only 2-3 founders / platform owners
log-archive Security CloudTrail, Config, access logs, VPC Flow Logs, ALB logs, S3 server access logs security read-only, no normal write
security-tooling Security GuardDuty, Security Hub, IAM Access Analyzer, Detective, audit tools security admin
shared-services Infrastructure CI runners, ECR shared images, internal DNS, optional network tooling platform team
dev Workloads/Dev developer environment engineers
uat Workloads/UAT pre-prod verification engineers + QA
prod Workloads/Prod customer production very limited
sandbox Sandbox experiments, short-lived engineers, budget-limited

建议一开始就创建 7 个账号。少一个账号省不了多少复杂度,但以后拆分日志、安全、生产会很痛苦。

AWS Organizations
├── Security
│   ├── log-archive
│   └── security-tooling
├── Infrastructure
│   └── shared-services
├── Workloads
│   ├── Dev
│   │   └── dev
│   ├── UAT
│   │   └── uat
│   └── Prod
│       └── prod
├── Sandbox
│   └── sandbox
└── Suspended

3. Setup Order#

不要从某个业务账号开始建资源。先建 landing zone,再建 workload。

1. create management account
2. enable MFA on root, store root credential in company password vault
3. enable IAM Identity Center
4. create AWS Organization with all features
5. set up AWS Control Tower landing zone
6. create Security OU and Workloads OU
7. create log-archive and security-tooling accounts
8. create dev / uat / prod / shared-services accounts through Account Factory
9. delegate security services to security-tooling account
10. enable organization CloudTrail and centralized logs
11. attach baseline SCP to OUs
12. create IAM Identity Center permission sets
13. create deploy roles for CI/CD
14. run baseline verification

4. Control Tower#

Control Tower 是 startup 可以接受的标准做法,特别是有生产系统、客户数据、支付、金融、医疗、B2B 企业客户时。

Decision Recommendation
Home Region 选择主要业务 region,例如 ap-east-1
Governed Regions 只启用业务需要的 regions
IAM Identity Center 开启
Log Archive Account 使用 Control Tower 创建或注册已有账号
Audit Account 用作 security-tooling,或后续注册为 Security Tooling
Account Creation 统一通过 Account Factory
Guardrails / Controls 先开 mandatory + strongly recommended security controls

必须启用的 controls 类型:

preventive:
    disallow public S3 buckets
    disallow disabling CloudTrail
    disallow deleting log archive resources
    disallow root user access in member accounts
    restrict governed regions

detective:
    detect unrestricted inbound 0.0.0.0/0 on security groups
    detect public S3 access
    detect unencrypted EBS / RDS / S3
    detect disabled GuardDuty / Config / CloudTrail

proactive:
    validate infrastructure templates before deployment
    require encryption and private access on new resources

这些 Control Tower controls 不是自己手写 policy,而是在 Control Tower console / API / CloudFormation 里启用 AWS 已经提供的 controls。

Control Type You Do AWS Creates / Uses Notes
Preventive enable control on OU SCP / RCP style guardrail 在 API 调用前直接 deny
Detective enable control on OU AWS Config managed rule 发现违规资源,通常不会阻止创建
Proactive enable control on OU CloudFormation hook 在 CloudFormation provisioning 前校验模板
Custom baseline create SCP yourself AWS Organizations SCP 用于公司自己的强制规则,见 7. Baseline SCP

Decision:

using Control Tower:
    enable built-in controls from Control Tower console or API
    do not recreate those built-in controls as custom SCP unless there is a gap
    still create custom SCP for company-specific baseline rules

not using Control Tower:
    create SCP manually in AWS Organizations
    use AWS Config / Security Hub separately for detective controls
    proactive controls need IaC pipeline checks, for example cfn-guard / Checkov / OPA

Console path:

AWS Control Tower console
    Controls
    search control by name or objective
    choose target OU, for example Workloads/Prod
    Enable control
    wait until status = Enabled

CLI pattern:

export AWS_PAGER=""
export REGION="ap-east-1"
export TARGET_OU_ARN="arn:aws:organizations::<MANAGEMENT_ACCOUNT_ID>:ou/o-exampleorgid/ou-abcd-prod"
export CONTROL_IDENTIFIER="<CONTROL_TOWER_CONTROL_ARN_FROM_CONTROL_CATALOG>"

aws controltower enable-control \
  --control-identifier "$CONTROL_IDENTIFIER" \
  --target-identifier "$TARGET_OU_ARN" \
  --region "$REGION"

Get OU ARN:

aws organizations describe-organizational-unit \
  --organizational-unit-id ou-abcd-prod \
  --query 'OrganizationalUnit.Arn' \
  --output text

Verify enabled controls:

aws controltower list-enabled-controls \
  --target-identifier "$TARGET_OU_ARN" \
  --region "$REGION"

CloudFormation example for managing a Control Tower control as code:

Resources:
  ProdControl:
    Type: AWS::ControlTower::EnabledControl
    Properties:
      ControlIdentifier: <CONTROL_TOWER_CONTROL_ARN_FROM_CONTROL_CATALOG>
      TargetIdentifier: arn:aws:organizations::<MANAGEMENT_ACCOUNT_ID>:ou/o-exampleorgid/ou-abcd-prod

Recommended startup enablement:

Area Prefer Control Tower Built-in Control Also Create Custom SCP
S3 public access yes prod-only deny public bucket policy if needed
CloudTrail protection yes deny disabling CloudTrail / GuardDuty / Security Hub
Log archive protection yes deny deleting log bucket objects and policy
Root user access yes deny root in member accounts
Region restriction yes, use Region deny control only if not using Control Tower
Encryption detective/proactive controls SCP for EBS encryption if EC2 is used
IAM user / access key ban no universal built-in for every org rule prod deny IAM users and access keys

Apply order:

1. enable Control Tower mandatory controls
2. enable strongly recommended security controls on Workloads / Security OUs
3. enable elective controls only after testing in Sandbox or Dev
4. create custom SCP from section 7 for gaps and company rules
5. never attach a new restrictive SCP to Prod before testing it in Sandbox

5. Central Security Services#

Use delegated administrator. 不要在 management account 里日常运营安全服务。

Service Delegated Admin Enable In
GuardDuty security-tooling all accounts, all governed regions
Security Hub security-tooling all accounts, all governed regions
IAM Access Analyzer security-tooling organization analyzer
AWS Config Aggregator security-tooling all accounts, all governed regions
CloudTrail management creates org trail, logs to log-archive all accounts, all regions
Detective security-tooling enable when incidents need graph investigation
Macie security-tooling enable for S3 sensitive data accounts first

GuardDuty organization setup:

export AWS_PAGER=""
export SECURITY_ACCOUNT_ID="111122223333"
export REGION="ap-east-1"

aws organizations enable-aws-service-access \
  --service-principal guardduty.amazonaws.com

aws guardduty enable-organization-admin-account \
  --admin-account-id "$SECURITY_ACCOUNT_ID" \
  --region "$REGION"

DETECTOR_ID="$(aws guardduty list-detectors \
  --region "$REGION" \
  --query 'DetectorIds[0]' \
  --output text)"

aws guardduty update-organization-configuration \
  --detector-id "$DETECTOR_ID" \
  --auto-enable-organization-members ALL \
  --region "$REGION"

Security Hub organization setup:

export SECURITY_ACCOUNT_ID="111122223333"
export REGION="ap-east-1"

aws organizations enable-aws-service-access \
  --service-principal securityhub.amazonaws.com

aws securityhub enable-organization-admin-account \
  --admin-account-id "$SECURITY_ACCOUNT_ID" \
  --region "$REGION"

aws securityhub enable-security-hub \
  --enable-default-standards \
  --region "$REGION"

IAM Access Analyzer:

aws organizations enable-aws-service-access \
  --service-principal access-analyzer.amazonaws.com

aws accessanalyzer create-analyzer \
  --analyzer-name org-external-access \
  --type ORGANIZATION \
  --region ap-east-1

6. Log Archive#

Log archive account 只做日志存储,不部署业务系统。

log buckets:
    org-cloudtrail-logs-<org-id>
    org-config-logs-<org-id>
    org-vpc-flow-logs-<org-id>
    org-alb-access-logs-<org-id>
    org-waf-logs-<org-id>

bucket defaults:
    S3 Block Public Access = on
    bucket versioning = on
    default encryption = SSE-S3 or SSE-KMS
    object ownership = Bucket owner enforced
    lifecycle = keep security logs 365-2555 days based on compliance
    Object Lock = enable for CloudTrail bucket if compliance requires immutability

Organization CloudTrail:

export LOG_BUCKET="org-cloudtrail-logs-o-exampleorgid"
export TRAIL_NAME="org-management-events"
export REGION="ap-east-1"

aws cloudtrail create-trail \
  --name "$TRAIL_NAME" \
  --s3-bucket-name "$LOG_BUCKET" \
  --is-organization-trail \
  --is-multi-region-trail \
  --enable-log-file-validation \
  --region "$REGION"

aws cloudtrail start-logging \
  --name "$TRAIL_NAME" \
  --region "$REGION"

aws cloudtrail get-trail-status \
  --name "$TRAIL_NAME" \
  --region "$REGION"

CloudTrail log bucket policy:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "DenyInsecureTransport",
      "Effect": "Deny",
      "Principal": "*",
      "Action": "s3:*",
      "Resource": [
        "arn:aws:s3:::org-cloudtrail-logs-o-exampleorgid",
        "arn:aws:s3:::org-cloudtrail-logs-o-exampleorgid/*"
      ],
      "Condition": {
        "Bool": {
          "aws:SecureTransport": "false"
        }
      }
    },
    {
      "Sid": "AWSCloudTrailAclCheck",
      "Effect": "Allow",
      "Principal": {
        "Service": "cloudtrail.amazonaws.com"
      },
      "Action": "s3:GetBucketAcl",
      "Resource": "arn:aws:s3:::org-cloudtrail-logs-o-exampleorgid",
      "Condition": {
        "StringEquals": {
          "aws:SourceOrgID": "o-exampleorgid"
        }
      }
    },
    {
      "Sid": "AWSCloudTrailWrite",
      "Effect": "Allow",
      "Principal": {
        "Service": "cloudtrail.amazonaws.com"
      },
      "Action": "s3:PutObject",
      "Resource": "arn:aws:s3:::org-cloudtrail-logs-o-exampleorgid/AWSLogs/o-exampleorgid/*",
      "Condition": {
        "StringEquals": {
          "aws:SourceOrgID": "o-exampleorgid",
          "s3:x-amz-acl": "bucket-owner-full-control"
        }
      }
    },
    {
      "Sid": "DenyDeleteLogsExceptLogArchiveAdmin",
      "Effect": "Deny",
      "Principal": "*",
      "Action": [
        "s3:DeleteObject",
        "s3:DeleteObjectVersion",
        "s3:PutLifecycleConfiguration",
        "s3:PutBucketPolicy",
        "s3:DeleteBucketPolicy"
      ],
      "Resource": [
        "arn:aws:s3:::org-cloudtrail-logs-o-exampleorgid",
        "arn:aws:s3:::org-cloudtrail-logs-o-exampleorgid/*"
      ],
      "Condition": {
        "ArnNotLike": {
          "aws:PrincipalArn": [
            "arn:aws:iam::<LOG_ARCHIVE_ACCOUNT_ID>:role/LogArchiveAdmin",
            "arn:aws:iam::<LOG_ARCHIVE_ACCOUNT_ID>:role/AWSReservedSSO_BreakGlassAdmin_*"
          ]
        }
      }
    }
  ]
}

Placeholders:

o-exampleorgid: AWS Organizations ID
<LOG_ARCHIVE_ACCOUNT_ID>: log-archive account id
LogArchiveAdmin: only security/platform admins can assume this role

7. Baseline SCP#

Attach SCP to OU, not individual accounts. Test first on Sandbox OU, then Dev, UAT, Prod.

7.1 Deny Root User In Member Accounts#

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "DenyRootUser",
      "Effect": "Deny",
      "Action": "*",
      "Resource": "*",
      "Condition": {
        "StringLike": {
          "aws:PrincipalArn": "arn:aws:iam::*:root"
        }
      }
    }
  ]
}

Attach:

Workloads OU
Infrastructure OU
Sandbox OU

不要 attach 到 Root OU,避免影响 management account root 的紧急恢复任务。

7.2 Deny Leaving Organization#

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "DenyLeaveOrganization",
      "Effect": "Deny",
      "Action": "organizations:LeaveOrganization",
      "Resource": "*"
    }
  ]
}

7.3 Deny Disabling Security Baseline#

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "DenyDisableCloudTrailConfigGuardDutySecurityHub",
      "Effect": "Deny",
      "Action": [
        "cloudtrail:StopLogging",
        "cloudtrail:DeleteTrail",
        "cloudtrail:UpdateTrail",
        "cloudtrail:PutEventSelectors",
        "config:DeleteConfigurationRecorder",
        "config:StopConfigurationRecorder",
        "config:DeleteDeliveryChannel",
        "guardduty:DeleteDetector",
        "guardduty:DisassociateFromMasterAccount",
        "guardduty:DisassociateFromAdministratorAccount",
        "guardduty:StopMonitoringMembers",
        "securityhub:DisableSecurityHub",
        "securityhub:DisassociateFromAdministratorAccount",
        "access-analyzer:DeleteAnalyzer"
      ],
      "Resource": "*",
      "Condition": {
        "ArnNotLike": {
          "aws:PrincipalArn": [
            "arn:aws:iam::*:role/AWSControlTowerExecution",
            "arn:aws:iam::*:role/OrganizationSecurityAdmin",
            "arn:aws:iam::*:role/AWSReservedSSO_BreakGlassAdmin_*"
          ]
        }
      }
    }
  ]
}

7.4 Restrict Regions#

优先使用 Control Tower 的 region deny control。没有 Control Tower 时,再使用 SCP。下面示例只允许 ap-east-1us-east-1,global services 需要放进 NotAction

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "DenyUnsupportedRegions",
      "Effect": "Deny",
      "NotAction": [
        "a4b:*",
        "acm:*",
        "aws-marketplace-management:*",
        "aws-marketplace:*",
        "aws-portal:*",
        "budgets:*",
        "ce:*",
        "chime:*",
        "cloudfront:*",
        "config:*",
        "cur:*",
        "directconnect:*",
        "ec2:DescribeRegions",
        "ec2:DescribeTransitGateways",
        "ec2:DescribeVpnGateways",
        "fms:*",
        "globalaccelerator:*",
        "health:*",
        "iam:*",
        "importexport:*",
        "kms:*",
        "mobileanalytics:*",
        "networkmanager:*",
        "organizations:*",
        "pricing:*",
        "route53:*",
        "route53domains:*",
        "s3:GetAccountPublic*",
        "s3:ListAllMyBuckets",
        "s3:PutAccountPublic*",
        "shield:*",
        "sts:*",
        "support:*",
        "trustedadvisor:*",
        "waf:*",
        "waf-regional:*",
        "wafv2:*",
        "wellarchitected:*"
      ],
      "Resource": "*",
      "Condition": {
        "StringNotEquals": {
          "aws:RequestedRegion": [
            "ap-east-1",
            "us-east-1"
          ]
        },
        "ArnNotLike": {
          "aws:PrincipalArn": [
            "arn:aws:iam::*:role/AWSControlTowerExecution",
            "arn:aws:iam::*:role/AWSReservedSSO_BreakGlassAdmin_*"
          ]
        }
      }
    }
  ]
}

7.5 Require IMDSv2#

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "DenyRunInstanceWithoutIMDSv2",
      "Effect": "Deny",
      "Action": "ec2:RunInstances",
      "Resource": "arn:aws:ec2:*:*:instance/*",
      "Condition": {
        "StringNotEquals": {
          "ec2:MetadataHttpTokens": "required"
        }
      }
    }
  ]
}

7.6 Require EBS Encryption#

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "DenyUnencryptedEBSVolume",
      "Effect": "Deny",
      "Action": [
        "ec2:CreateVolume",
        "ec2:RunInstances"
      ],
      "Resource": "*",
      "Condition": {
        "Bool": {
          "ec2:Encrypted": "false"
        }
      }
    }
  ]
}

7.7 Deny Public S3 Bucket Policy In Prod#

这条只 attach 到 Workloads/Prod,避免生产 bucket 被直接公开。

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "DenyPublicS3Policy",
      "Effect": "Deny",
      "Action": [
        "s3:PutBucketPolicy",
        "s3:PutBucketAcl",
        "s3:PutObjectAcl"
      ],
      "Resource": "*",
      "Condition": {
        "ArnNotLike": {
          "aws:PrincipalArn": [
            "arn:aws:iam::*:role/AWSControlTowerExecution",
            "arn:aws:iam::*:role/OrganizationSecurityAdmin",
            "arn:aws:iam::*:role/AWSReservedSSO_BreakGlassAdmin_*"
          ]
        }
      }
    }
  ]
}

如果业务确实需要 public website,用 CloudFront + OAC,不要把 S3 bucket 直接 public。

7.8 Prod Deny IAM User And Access Key#

生产环境不创建 IAM User,不创建长期 access key。

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "DenyIAMUsersAndAccessKeysInProd",
      "Effect": "Deny",
      "Action": [
        "iam:CreateUser",
        "iam:CreateLoginProfile",
        "iam:CreateAccessKey",
        "iam:UpdateAccessKey",
        "iam:AttachUserPolicy",
        "iam:PutUserPolicy"
      ],
      "Resource": "*",
      "Condition": {
        "ArnNotLike": {
          "aws:PrincipalArn": [
            "arn:aws:iam::*:role/AWSControlTowerExecution",
            "arn:aws:iam::*:role/AWSReservedSSO_BreakGlassAdmin_*"
          ]
        }
      }
    }
  ]
}

7.9 Quarantine SCP#

发现账号泄露时,把账号移动到 Suspended OU,并 attach 这条 SCP。

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "QuarantineAccount",
      "Effect": "Deny",
      "Action": "*",
      "Resource": "*",
      "Condition": {
        "ArnNotLike": {
          "aws:PrincipalArn": [
            "arn:aws:iam::*:role/AWSReservedSSO_BreakGlassAdmin_*",
            "arn:aws:iam::*:role/OrganizationSecurityAdmin"
          ]
        }
      }
    }
  ]
}

8. IAM Identity Center#

不要给人发 IAM user access key。人用 SSO,机器用 role。

Permission Set Attach To Permission
BreakGlassAdmin management, security-tooling, log-archive, prod AdministratorAccess, very few people
PlatformAdmin shared-services, dev, uat admin except billing/org
ProdPowerUser prod PowerUserAccess + deny IAM/security baseline changes
DeveloperReadOnly dev/uat/prod ViewOnlyAccess
SecurityAudit all accounts SecurityAudit + ViewOnlyAccess
BillingReadOnly management billing read

Permission boundary for application roles:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "DenyPrivilegeEscalation",
      "Effect": "Deny",
      "Action": [
        "iam:CreateUser",
        "iam:CreateAccessKey",
        "iam:AttachUserPolicy",
        "iam:PutUserPolicy",
        "iam:PutRolePolicy",
        "iam:AttachRolePolicy",
        "iam:UpdateAssumeRolePolicy",
        "iam:CreatePolicyVersion",
        "iam:SetDefaultPolicyVersion",
        "iam:PassRole"
      ],
      "Resource": "*"
    },
    {
      "Sid": "AllowBasicReadOwnContext",
      "Effect": "Allow",
      "Action": [
        "iam:GetRole",
        "iam:ListRolePolicies",
        "iam:ListAttachedRolePolicies"
      ],
      "Resource": "*"
    }
  ]
}

9. Required IAM Roles#

Role Account Who Assumes Purpose
AWSControlTowerExecution every enrolled account Control Tower account baseline automation
OrganizationSecurityAdmin security-tooling security team manage org security services
SecurityAuditRole every member account security-tooling cross-account security read
LogArchiveAdmin log-archive security/platform leads log bucket and retention admin
GitHubDeployRole dev/uat/prod GitHub Actions OIDC deploy application
TerraformProvisionRole dev/uat/prod/shared CI OIDC or platform SSO provision infra
BreakGlassAdmin every account emergency SSO group emergency recovery

SecurityAuditRole trust policy in member accounts:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "AllowSecurityToolingAssumeRole",
      "Effect": "Allow",
      "Principal": {
        "AWS": "arn:aws:iam::<SECURITY_ACCOUNT_ID>:role/OrganizationSecurityAdmin"
      },
      "Action": "sts:AssumeRole"
    }
  ]
}

Attach AWS managed policies:

arn:aws:iam::aws:policy/SecurityAudit
arn:aws:iam::aws:policy/job-function/ViewOnlyAccess

GitHub Actions OIDC deploy role trust policy:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "AllowGitHubActionsOIDC",
      "Effect": "Allow",
      "Principal": {
        "Federated": "arn:aws:iam::<ACCOUNT_ID>:oidc-provider/token.actions.githubusercontent.com"
      },
      "Action": "sts:AssumeRoleWithWebIdentity",
      "Condition": {
        "StringEquals": {
          "token.actions.githubusercontent.com:aud": "sts.amazonaws.com"
        },
        "StringLike": {
          "token.actions.githubusercontent.com:sub": [
            "repo:<GITHUB_ORG>/<REPO>:environment:dev",
            "repo:<GITHUB_ORG>/<REPO>:environment:uat",
            "repo:<GITHUB_ORG>/<REPO>:environment:prod"
          ]
        }
      }
    }
  ]
}

Example ECS deploy policy:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "ECRPushPull",
      "Effect": "Allow",
      "Action": [
        "ecr:GetAuthorizationToken"
      ],
      "Resource": "*"
    },
    {
      "Sid": "ECRRepoAccess",
      "Effect": "Allow",
      "Action": [
        "ecr:BatchCheckLayerAvailability",
        "ecr:BatchGetImage",
        "ecr:CompleteLayerUpload",
        "ecr:DescribeImages",
        "ecr:DescribeRepositories",
        "ecr:GetDownloadUrlForLayer",
        "ecr:InitiateLayerUpload",
        "ecr:PutImage",
        "ecr:UploadLayerPart"
      ],
      "Resource": "arn:aws:ecr:ap-east-1:<ACCOUNT_ID>:repository/<SERVICE_NAME>"
    },
    {
      "Sid": "ECSDeploy",
      "Effect": "Allow",
      "Action": [
        "ecs:DescribeServices",
        "ecs:DescribeTaskDefinition",
        "ecs:RegisterTaskDefinition",
        "ecs:UpdateService"
      ],
      "Resource": "*"
    },
    {
      "Sid": "AllowPassOnlyTaskRoles",
      "Effect": "Allow",
      "Action": "iam:PassRole",
      "Resource": [
        "arn:aws:iam::<ACCOUNT_ID>:role/<SERVICE_NAME>-task-role",
        "arn:aws:iam::<ACCOUNT_ID>:role/<SERVICE_NAME>-task-execution-role"
      ],
      "Condition": {
        "StringEquals": {
          "iam:PassedToService": "ecs-tasks.amazonaws.com"
        }
      }
    }
  ]
}

TerraformProvisionRole trust policy 建议和 GitHub OIDC 类似,但 repo/environment 要单独限制:

dev: allow repo:<org>/infra-live:environment:dev
uat: allow repo:<org>/infra-live:environment:uat
prod: allow repo:<org>/infra-live:environment:prod, require manual approval in GitHub Environment

10. Network Baseline#

Startup 不要一开始做过重的 hub-spoke 网络,除非有 VPN、multi-VPC、shared firewall 需求。

Environment VPC CIDR Example Internet
dev one VPC 10.10.0.0/16 NAT allowed
uat one VPC 10.20.0.0/16 NAT allowed
prod one VPC 10.30.0.0/16 private subnets for workloads
shared-services one VPC 10.40.0.0/16 CI runners / endpoints

Minimum VPC standard:

public subnets:
    ALB, NAT Gateway, bastion only if absolutely needed

private app subnets:
    ECS/EKS/EC2 application workloads

private data subnets:
    RDS, ElastiCache, OpenSearch

security:
    no SSH/RDP from internet
    use SSM Session Manager
    VPC Flow Logs to log-archive
    interface VPC endpoints for ECR, CloudWatch Logs, Secrets Manager, SSM

Security group rule:

ALB:
    inbound 443 from 0.0.0.0/0
    outbound app port to app security group

App:
    inbound app port only from ALB security group
    outbound 443 to VPC endpoints / NAT

Database:
    inbound DB port only from app security group
    no public accessibility

11. Data And Secrets#

Required defaults:

S3:
    block public access
    bucket owner enforced
    versioning on for important buckets
    encryption on

RDS:
    encryption on
    deletion protection on in prod
    automated backup on
    no public access

Secrets:
    use Secrets Manager or SSM Parameter Store SecureString
    no secrets in GitHub Actions variables
    no secrets in Docker images
    rotate database credentials when possible

KMS:
    use AWS managed keys for low-risk startup workloads
    use customer managed keys for prod databases, secrets, and regulated data

Example Secrets Manager app policy:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "ReadOnlyServiceSecrets",
      "Effect": "Allow",
      "Action": [
        "secretsmanager:GetSecretValue",
        "secretsmanager:DescribeSecret"
      ],
      "Resource": "arn:aws:secretsmanager:ap-east-1:<ACCOUNT_ID>:secret:/prod/order-api/*"
    },
    {
      "Sid": "DecryptSecretKMSKey",
      "Effect": "Allow",
      "Action": "kms:Decrypt",
      "Resource": "arn:aws:kms:ap-east-1:<ACCOUNT_ID>:key/<KMS_KEY_ID>",
      "Condition": {
        "StringEquals": {
          "kms:ViaService": "secretsmanager.ap-east-1.amazonaws.com"
        }
      }
    }
  ]
}

12. CI/CD Rules#

dev:
    auto deploy from main or develop
    broad engineer access acceptable

uat:
    deploy release candidate
    test data only, no production data

prod:
    deploy only from protected tag or approved GitHub Environment
    no direct console changes except incident
    Terraform plan must be reviewed
    application deploy role cannot modify IAM baseline

Prod deploy checklist:

before deploy:
    ticket/change link
    reviewed diff
    rollback plan
    database migration checked
    alarm silence not used as a workaround

after deploy:
    ALB 5xx normal
    ECS/EKS task healthy
    CloudWatch logs no new error spike
    business metric normal

13. Monitoring And Alerting#

Required alerts:

Signal Source Alert
Root login CloudTrail / EventBridge immediate critical
Console login without MFA CloudTrail critical
CloudTrail stopped CloudTrail / GuardDuty / Config critical
GuardDuty high severity GuardDuty critical
Public S3 bucket Security Hub / Config high
Security group 0.0.0.0/0 to admin port Security Hub / Config high
IAM access key created CloudTrail high
Billing anomaly Cost Anomaly Detection high
Prod ALB 5xx CloudWatch high
Prod RDS CPU/storage/connection CloudWatch high

EventBridge root login rule:

{
  "source": ["aws.signin"],
  "detail-type": ["AWS Console Sign In via CloudTrail"],
  "detail": {
    "userIdentity": {
      "type": ["Root"]
    }
  }
}

14. Cost Guardrails#

Security baseline 会产生费用,尤其是 AWS Config、GuardDuty、CloudTrail data events、VPC Flow Logs、Security Hub。

Startup 默认:

must enable:
    CloudTrail management events
    GuardDuty all accounts
    Security Hub key standards in prod
    AWS Config via Control Tower baseline
    billing budgets and anomaly detection

enable selectively:
    CloudTrail S3 data events only for sensitive buckets
    VPC Flow Logs for prod VPC and critical private subnets
    Macie for buckets with customer data
    Detective after security team can use it

Budget alerts:

aws budgets create-budget \
  --account-id <MANAGEMENT_ACCOUNT_ID> \
  --budget '{
    "BudgetName": "monthly-startup-total",
    "BudgetLimit": { "Amount": "1000", "Unit": "USD" },
    "TimeUnit": "MONTHLY",
    "BudgetType": "COST"
  }' \
  --notifications-with-subscribers '[
    {
      "Notification": {
        "NotificationType": "ACTUAL",
        "ComparisonOperator": "GREATER_THAN",
        "Threshold": 80,
        "ThresholdType": "PERCENTAGE"
      },
      "Subscribers": [
        { "SubscriptionType": "EMAIL", "Address": "aws-alerts@example.com" }
      ]
    }
  ]'

15. Implementation Checklist#

organization:
    [ ] management account root MFA enabled
    [ ] IAM Identity Center enabled
    [ ] Control Tower landing zone created
    [ ] Security / Infrastructure / Workloads / Sandbox / Suspended OUs created
    [ ] log-archive / security-tooling / shared-services / dev / uat / prod accounts created

security services:
    [ ] GuardDuty delegated admin = security-tooling
    [ ] Security Hub delegated admin = security-tooling
    [ ] IAM Access Analyzer organization analyzer enabled
    [ ] AWS Config aggregator enabled
    [ ] CloudTrail org trail enabled and logging

logging:
    [ ] log buckets have block public access
    [ ] log buckets have versioning
    [ ] CloudTrail log file validation enabled
    [ ] VPC Flow Logs enabled for prod
    [ ] lifecycle policy configured

scp:
    [ ] deny root in member accounts
    [ ] deny leaving organization
    [ ] deny disabling security baseline
    [ ] restrict unsupported regions
    [ ] require IMDSv2
    [ ] require EBS encryption
    [ ] prod deny IAM users/access keys
    [ ] quarantine SCP ready

iam:
    [ ] no IAM users for humans
    [ ] Identity Center groups mapped
    [ ] break-glass access tested
    [ ] GitHub OIDC provider created
    [ ] deploy roles limited by repo/environment
    [ ] SecurityAuditRole deployed to every account

workloads:
    [ ] dev / uat / prod use separate AWS accounts
    [ ] prod has no public database
    [ ] prod secrets stored in Secrets Manager or SSM SecureString
    [ ] prod deploy requires approval
    [ ] alarms connected to on-call channel

16. Verification Commands#

aws organizations list-accounts

aws organizations list-policies \
  --filter SERVICE_CONTROL_POLICY

aws cloudtrail get-trail-status \
  --name org-management-events \
  --region ap-east-1

aws guardduty list-detectors \
  --region ap-east-1

aws securityhub get-enabled-standards \
  --region ap-east-1

aws accessanalyzer list-analyzers \
  --region ap-east-1

Expected result:

accounts:
    management, log-archive, security-tooling, shared-services, dev, uat, prod exist

cloudtrail:
    IsLogging = true
    LatestDeliveryError = empty

guardduty:
    detector exists in every governed region

securityhub:
    enabled standards visible in security-tooling account

access analyzer:
    org-external-access analyzer status = ACTIVE

17. Production Rules#

never:
    use management account for workloads
    create IAM users for engineers
    put prod and dev in same AWS account
    store secrets in GitHub, Docker images, AMI, or source code
    allow direct database public access
    disable CloudTrail / GuardDuty to save money

allowed with approval:
    temporary prod console write access
    new public endpoint
    new region
    IAM permission expansion
    logging retention reduction

incident:
    move compromised account to Suspended OU
    attach Quarantine SCP
    preserve CloudTrail and VPC Flow Logs
    rotate affected credentials
    review GuardDuty and CloudTrail timeline

18. Terraform Resource Appendix#

Terraform 可以管理大部分 baseline。建议拆成 3 个 state,不要把所有东西放在一个 state:

state 1 - org-baseline:
    AWS Organizations
    OU / account
    SCP
    Control Tower controls
    delegated admin

state 2 - security-baseline:
    log archive bucket
    organization CloudTrail
    GuardDuty / Security Hub / Access Analyzer / Config
    alerting

state 3 - workload-baseline:
    IAM roles
    VPC baseline
    secrets / KMS
    deploy roles

18.1 What Terraform Should And Should Not Manage#

Setting Terraform Resource Notes
AWS Organization aws_organizations_organization management account only
OU aws_organizations_organizational_unit create Security / Infrastructure / Workloads / Sandbox / Suspended
Accounts aws_organizations_account use only if not using Control Tower Account Factory / AFT
SCP aws_organizations_policy, aws_organizations_policy_attachment use SERVICE_CONTROL_POLICY
Control Tower landing zone aws_controltower_landing_zone optional; many teams still launch first landing zone from console
Control Tower controls aws_controltower_control enables AWS built-in controls on OU
CloudTrail log bucket aws_s3_bucket, aws_s3_bucket_policy, bucket sub-resources log-archive account
Organization CloudTrail aws_cloudtrail management account, home region
GuardDuty delegated admin aws_guardduty_organization_admin_account management account
GuardDuty org config aws_guardduty_detector, aws_guardduty_organization_configuration, aws_guardduty_organization_configuration_feature security-tooling account
Security Hub delegated admin aws_securityhub_organization_admin_account management account
Security Hub org config aws_securityhub_account, aws_securityhub_organization_configuration, aws_securityhub_standards_subscription security-tooling account
IAM Access Analyzer aws_accessanalyzer_analyzer organization analyzer in security-tooling
AWS Config recorder aws_config_configuration_recorder, aws_config_delivery_channel, aws_config_configuration_recorder_status per account/region or Control Tower managed
AWS Config aggregator aws_config_configuration_aggregator security-tooling account
IAM Identity Center permission sets aws_ssoadmin_permission_set, aws_ssoadmin_managed_policy_attachment, aws_ssoadmin_permission_set_inline_policy, aws_ssoadmin_account_assignment IAM Identity Center must already exist
Identity Center groups aws_identitystore_group, aws_identitystore_group_membership only if Terraform owns groups; otherwise use SCIM/IdP
GitHub OIDC aws_iam_openid_connect_provider, aws_iam_role, aws_iam_role_policy per target account
Cross-account security audit role aws_iam_role, aws_iam_role_policy_attachment deploy to every member account
Budget aws_budgets_budget management account
Cost anomaly aws_ce_anomaly_monitor, aws_ce_anomaly_subscription management account
Root login alert aws_cloudwatch_event_rule, aws_cloudwatch_event_target, aws_sns_topic, aws_sns_topic_policy security or management account
VPC baseline aws_vpc, aws_subnet, aws_route_table, aws_nat_gateway, aws_vpc_endpoint, aws_flow_log per workload account
KMS aws_kms_key, aws_kms_alias, aws_kms_key_policy prod data and secrets
Secrets aws_secretsmanager_secret, aws_secretsmanager_secret_version, aws_secretsmanager_secret_rotation never store real secret value in Git state
Optional Macie aws_macie2_account, aws_macie2_organization_admin_account, aws_macie2_organization_configuration enable for sensitive S3 accounts
Optional Detective aws_detective_graph, aws_detective_organization_admin_account enable when incident investigation needs it

Do not manage these blindly:

root MFA:
    manual setup, verify by process

initial management account:
    created outside Terraform

Control Tower Account Factory accounts:
    prefer Account Factory / AFT, then import if Terraform must reference them

Control Tower managed roles:
    do not edit AWSControlTowerExecution manually

existing Identity Center users/groups from company IdP:
    prefer SCIM from IdP instead of Terraform

secret values:
    do not commit to Terraform state unless using a protected remote backend and strict access control

18.2 Provider Layout#

Use provider aliases so each state is explicit about which account it manages.

terraform {
  required_version = ">= 1.6.0"

  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 6.0"
    }
  }
}

provider "aws" {
  alias  = "management"
  region = "ap-east-1"
}

provider "aws" {
  alias  = "security"
  region = "ap-east-1"

  assume_role {
    role_arn = "arn:aws:iam::<SECURITY_ACCOUNT_ID>:role/OrganizationSecurityAdmin"
  }
}

provider "aws" {
  alias  = "log_archive"
  region = "ap-east-1"

  assume_role {
    role_arn = "arn:aws:iam::<LOG_ARCHIVE_ACCOUNT_ID>:role/LogArchiveAdmin"
  }
}

provider "aws" {
  alias  = "prod"
  region = "ap-east-1"

  assume_role {
    role_arn = "arn:aws:iam::<PROD_ACCOUNT_ID>:role/TerraformProvisionRole"
  }
}

18.3 Organization, OU, Accounts#

如果使用 Control Tower Account Factory / AFT 创建账号,不要再用 aws_organizations_account 创建同一个账号。Terraform 可以只读取 account id,或 import 之后谨慎管理。

resource "aws_organizations_organization" "this" {
  provider    = aws.management
  feature_set = "ALL"

  enabled_policy_types = [
    "SERVICE_CONTROL_POLICY",
    "TAG_POLICY"
  ]

  aws_service_access_principals = [
    "cloudtrail.amazonaws.com",
    "config.amazonaws.com",
    "guardduty.amazonaws.com",
    "securityhub.amazonaws.com",
    "access-analyzer.amazonaws.com",
    "controltower.amazonaws.com"
  ]
}

resource "aws_organizations_organizational_unit" "security" {
  provider  = aws.management
  name      = "Security"
  parent_id = aws_organizations_organization.this.roots[0].id
}

resource "aws_organizations_organizational_unit" "workloads" {
  provider  = aws.management
  name      = "Workloads"
  parent_id = aws_organizations_organization.this.roots[0].id
}

resource "aws_organizations_organizational_unit" "prod" {
  provider  = aws.management
  name      = "Prod"
  parent_id = aws_organizations_organizational_unit.workloads.id
}

resource "aws_organizations_account" "prod" {
  provider  = aws.management
  name      = "prod"
  email     = "aws-prod@example.com"
  parent_id = aws_organizations_organizational_unit.prod.id

  lifecycle {
    prevent_destroy = true
  }
}

18.4 SCP#

把第 7 节的 JSON 放进 aws_iam_policy_documentjsonencode(),再 attach 到 OU。

data "aws_iam_policy_document" "deny_leave_org" {
  statement {
    sid       = "DenyLeaveOrganization"
    effect    = "Deny"
    actions   = ["organizations:LeaveOrganization"]
    resources = ["*"]
  }
}

resource "aws_organizations_policy" "deny_leave_org" {
  provider    = aws.management
  name        = "deny-leave-organization"
  description = "Deny member accounts leaving the AWS Organization."
  type        = "SERVICE_CONTROL_POLICY"
  content     = data.aws_iam_policy_document.deny_leave_org.json
}

resource "aws_organizations_policy_attachment" "deny_leave_org_workloads" {
  provider  = aws.management
  policy_id = aws_organizations_policy.deny_leave_org.id
  target_id = aws_organizations_organizational_unit.workloads.id
}

Recommended SCP resources:

aws_organizations_policy.deny_root_user
aws_organizations_policy.deny_leave_org
aws_organizations_policy.deny_disable_security_baseline
aws_organizations_policy.restrict_regions
aws_organizations_policy.require_imdsv2
aws_organizations_policy.require_ebs_encryption
aws_organizations_policy.prod_deny_public_s3_policy
aws_organizations_policy.prod_deny_iam_users_access_keys
aws_organizations_policy.quarantine_account

18.5 Control Tower Controls#

Control Tower built-in controls should be enabled by aws_controltower_control, not recreated as custom SCP unless there is a gap.

data "aws_region" "current" {
  provider = aws.management
}

resource "aws_controltower_control" "prod_region_deny" {
  provider = aws.management

  control_identifier = "arn:aws:controltower:${data.aws_region.current.region}::control/<CONTROL_ID>"
  target_identifier  = aws_organizations_organizational_unit.prod.arn

  parameters {
    key   = "AllowedRegions"
    value = jsonencode(["ap-east-1", "us-east-1"])
  }
}

Control identifiers are AWS-managed. Get them from Control Tower controls reference / console / API, then keep them in variables:

variable "control_tower_controls" {
  type = map(string)

  default = {
    prod_region_deny        = "arn:aws:controltower:ap-east-1::control/<CONTROL_ID>"
    s3_public_access        = "arn:aws:controltower:ap-east-1::control/<CONTROL_ID>"
    cloudtrail_protection   = "arn:aws:controltower:ap-east-1::control/<CONTROL_ID>"
    root_user_restriction   = "arn:aws:controltower:ap-east-1::control/<CONTROL_ID>"
    log_archive_protection  = "arn:aws:controltower:ap-east-1::control/<CONTROL_ID>"
  }
}

18.6 Log Archive And Organization CloudTrail#

Create log bucket in log-archive account, then create organization trail from management account.

resource "aws_s3_bucket" "cloudtrail" {
  provider = aws.log_archive
  bucket   = "org-cloudtrail-logs-o-exampleorgid"

  object_lock_enabled = true

  lifecycle {
    prevent_destroy = true
  }
}

resource "aws_s3_bucket_public_access_block" "cloudtrail" {
  provider                = aws.log_archive
  bucket                  = aws_s3_bucket.cloudtrail.id
  block_public_acls       = true
  ignore_public_acls      = true
  block_public_policy     = true
  restrict_public_buckets = true
}

resource "aws_s3_bucket_versioning" "cloudtrail" {
  provider = aws.log_archive
  bucket   = aws_s3_bucket.cloudtrail.id

  versioning_configuration {
    status = "Enabled"
  }
}

resource "aws_s3_bucket_server_side_encryption_configuration" "cloudtrail" {
  provider = aws.log_archive
  bucket   = aws_s3_bucket.cloudtrail.id

  rule {
    apply_server_side_encryption_by_default {
      sse_algorithm = "AES256"
    }
  }
}

resource "aws_s3_bucket_ownership_controls" "cloudtrail" {
  provider = aws.log_archive
  bucket   = aws_s3_bucket.cloudtrail.id

  rule {
    object_ownership = "BucketOwnerEnforced"
  }
}

resource "aws_s3_bucket_object_lock_configuration" "cloudtrail" {
  provider = aws.log_archive
  bucket   = aws_s3_bucket.cloudtrail.id

  rule {
    default_retention {
      mode = "COMPLIANCE"
      days = 365
    }
  }
}

resource "aws_s3_bucket_policy" "cloudtrail" {
  provider = aws.log_archive
  bucket   = aws_s3_bucket.cloudtrail.id
  policy   = data.aws_iam_policy_document.cloudtrail_bucket.json
}

resource "aws_cloudtrail" "organization" {
  provider = aws.management

  depends_on = [aws_s3_bucket_policy.cloudtrail]

  name                          = "org-management-events"
  s3_bucket_name                = aws_s3_bucket.cloudtrail.id
  is_organization_trail         = true
  is_multi_region_trail         = true
  include_global_service_events = true
  enable_log_file_validation    = true
  enable_logging                = true
}

18.7 GuardDuty, Security Hub, Access Analyzer#

Delegated admin resources run in management account. Organization configuration runs in security-tooling account.

resource "aws_guardduty_organization_admin_account" "security" {
  provider         = aws.management
  admin_account_id = var.security_account_id
}

resource "aws_guardduty_detector" "security" {
  provider = aws.security
  enable   = true
}

resource "aws_guardduty_organization_configuration" "security" {
  provider                         = aws.security
  detector_id                      = aws_guardduty_detector.security.id
  auto_enable_organization_members = "ALL"
}

resource "aws_guardduty_organization_configuration_feature" "s3_data_events" {
  provider    = aws.security
  detector_id = aws_guardduty_detector.security.id
  name        = "S3_DATA_EVENTS"
  auto_enable = "ALL"
}

resource "aws_securityhub_organization_admin_account" "security" {
  provider         = aws.management
  admin_account_id = var.security_account_id
}

resource "aws_securityhub_account" "security" {
  provider = aws.security
}

resource "aws_securityhub_organization_configuration" "security" {
  provider    = aws.security
  auto_enable = true
}

resource "aws_securityhub_standards_subscription" "foundational" {
  provider      = aws.security
  standards_arn = "arn:aws:securityhub:ap-east-1::standards/aws-foundational-security-best-practices/v/1.0.0"
}

resource "aws_accessanalyzer_analyzer" "organization" {
  provider      = aws.security
  analyzer_name = "org-external-access"
  type          = "ORGANIZATION"
}

18.8 AWS Config#

If Control Tower manages AWS Config, do not duplicate per-account recorder resources. Use Terraform mainly for aggregator and extra organization rules.

resource "aws_config_configuration_aggregator" "organization" {
  provider = aws.security
  name     = "organization-config"

  organization_aggregation_source {
    all_regions = true
    role_arn    = aws_iam_role.config_aggregator.arn
  }
}

resource "aws_config_organization_managed_rule" "s3_bucket_public_read_prohibited" {
  provider        = aws.security
  name            = "s3-bucket-public-read-prohibited"
  rule_identifier = "S3_BUCKET_PUBLIC_READ_PROHIBITED"
}

18.9 IAM Identity Center#

IAM Identity Center must be enabled first. If groups come from Okta / Google Workspace / Entra ID through SCIM, let IdP own users/groups and use Terraform only for permission sets and assignments.

data "aws_ssoadmin_instances" "this" {
  provider = aws.management
}

resource "aws_ssoadmin_permission_set" "security_audit" {
  provider         = aws.management
  name             = "SecurityAudit"
  instance_arn     = tolist(data.aws_ssoadmin_instances.this.arns)[0]
  session_duration = "PT4H"
}

resource "aws_ssoadmin_managed_policy_attachment" "security_audit" {
  provider           = aws.management
  instance_arn       = tolist(data.aws_ssoadmin_instances.this.arns)[0]
  permission_set_arn = aws_ssoadmin_permission_set.security_audit.arn
  managed_policy_arn = "arn:aws:iam::aws:policy/SecurityAudit"
}

resource "aws_ssoadmin_account_assignment" "security_audit_prod" {
  provider           = aws.management
  instance_arn       = tolist(data.aws_ssoadmin_instances.this.arns)[0]
  permission_set_arn = aws_ssoadmin_permission_set.security_audit.arn

  principal_id   = var.security_group_id
  principal_type = "GROUP"

  target_id   = var.prod_account_id
  target_type = "AWS_ACCOUNT"
}

18.10 GitHub OIDC Deploy Role#

Create this in each target account. Do not use static AWS access keys in GitHub Actions.

variable "github_oidc_thumbprints" {
  type        = list(string)
  description = "TLS certificate thumbprints for token.actions.githubusercontent.com. Verify before production use."
}

resource "aws_iam_openid_connect_provider" "github" {
  provider = aws.prod

  url             = "https://token.actions.githubusercontent.com"
  client_id_list  = ["sts.amazonaws.com"]
  thumbprint_list = var.github_oidc_thumbprints
}

data "aws_iam_policy_document" "github_deploy_trust" {
  statement {
    effect  = "Allow"
    actions = ["sts:AssumeRoleWithWebIdentity"]

    principals {
      type        = "Federated"
      identifiers = [aws_iam_openid_connect_provider.github.arn]
    }

    condition {
      test     = "StringEquals"
      variable = "token.actions.githubusercontent.com:aud"
      values   = ["sts.amazonaws.com"]
    }

    condition {
      test     = "StringLike"
      variable = "token.actions.githubusercontent.com:sub"
      values   = ["repo:<GITHUB_ORG>/<REPO>:environment:prod"]
    }
  }
}

resource "aws_iam_role" "github_deploy" {
  provider           = aws.prod
  name               = "GitHubDeployRole"
  assume_role_policy = data.aws_iam_policy_document.github_deploy_trust.json
}

resource "aws_iam_role_policy" "github_deploy_ecs" {
  provider = aws.prod
  role     = aws_iam_role.github_deploy.id
  policy   = data.aws_iam_policy_document.github_deploy_ecs.json
}

18.11 Security Audit Role#

Deploy this role to every member account.

data "aws_iam_policy_document" "security_audit_trust" {
  statement {
    effect  = "Allow"
    actions = ["sts:AssumeRole"]

    principals {
      type        = "AWS"
      identifiers = ["arn:aws:iam::<SECURITY_ACCOUNT_ID>:role/OrganizationSecurityAdmin"]
    }
  }
}

resource "aws_iam_role" "security_audit" {
  provider           = aws.prod
  name               = "SecurityAuditRole"
  assume_role_policy = data.aws_iam_policy_document.security_audit_trust.json
}

resource "aws_iam_role_policy_attachment" "security_audit" {
  provider   = aws.prod
  role       = aws_iam_role.security_audit.name
  policy_arn = "arn:aws:iam::aws:policy/SecurityAudit"
}

resource "aws_iam_role_policy_attachment" "view_only" {
  provider   = aws.prod
  role       = aws_iam_role.security_audit.name
  policy_arn = "arn:aws:iam::aws:policy/job-function/ViewOnlyAccess"
}

18.12 Budget And Security Alerts#

resource "aws_sns_topic" "security_alerts" {
  provider = aws.management
  name     = "security-alerts"
}

resource "aws_cloudwatch_event_rule" "root_login" {
  provider    = aws.management
  name        = "root-login"
  description = "Alert when root user signs in."

  event_pattern = jsonencode({
    source      = ["aws.signin"]
    "detail-type" = ["AWS Console Sign In via CloudTrail"]
    detail = {
      userIdentity = {
        type = ["Root"]
      }
    }
  })
}

resource "aws_cloudwatch_event_target" "root_login" {
  provider = aws.management
  rule     = aws_cloudwatch_event_rule.root_login.name
  arn      = aws_sns_topic.security_alerts.arn
}

data "aws_iam_policy_document" "security_alerts_sns" {
  statement {
    effect    = "Allow"
    actions   = ["sns:Publish"]
    resources = [aws_sns_topic.security_alerts.arn]

    principals {
      type        = "Service"
      identifiers = ["events.amazonaws.com"]
    }
  }
}

resource "aws_sns_topic_policy" "security_alerts" {
  provider = aws.management
  arn      = aws_sns_topic.security_alerts.arn
  policy   = data.aws_iam_policy_document.security_alerts_sns.json
}

resource "aws_budgets_budget" "monthly_total" {
  provider     = aws.management
  name         = "monthly-startup-total"
  budget_type  = "COST"
  limit_amount = "1000"
  limit_unit   = "USD"
  time_unit    = "MONTHLY"

  notification {
    comparison_operator        = "GREATER_THAN"
    threshold                  = 80
    threshold_type             = "PERCENTAGE"
    notification_type          = "ACTUAL"
    subscriber_email_addresses = ["aws-alerts@example.com"]
  }
}

resource "aws_ce_anomaly_monitor" "services" {
  provider          = aws.management
  name              = "aws-service-cost-monitor"
  monitor_type      = "DIMENSIONAL"
  monitor_dimension = "SERVICE"
}

resource "aws_ce_anomaly_subscription" "alerts" {
  provider  = aws.management
  name      = "aws-cost-anomaly-alerts"
  frequency = "DAILY"

  monitor_arn_list = [aws_ce_anomaly_monitor.services.arn]

  subscriber {
    type    = "EMAIL"
    address = "aws-alerts@example.com"
  }

  threshold_expression {
    dimension {
      key           = "ANOMALY_TOTAL_IMPACT_ABSOLUTE"
      values        = ["100"]
      match_options = ["GREATER_THAN_OR_EQUAL"]
    }
  }
}

18.13 Workload Baseline#

For each dev / uat / prod account, create VPC, flow logs, endpoints, KMS, and secrets with a separate workload module.

resource "aws_vpc" "main" {
  provider             = aws.prod
  cidr_block           = "10.30.0.0/16"
  enable_dns_hostnames = true
  enable_dns_support   = true

  tags = {
    Name = "prod"
    Env  = "prod"
  }
}

resource "aws_flow_log" "vpc" {
  provider             = aws.prod
  vpc_id               = aws_vpc.main.id
  traffic_type         = "ALL"
  log_destination_type = "s3"
  log_destination      = "arn:aws:s3:::org-vpc-flow-logs-o-exampleorgid/prod/"
}

resource "aws_vpc_endpoint" "secretsmanager" {
  provider            = aws.prod
  vpc_id              = aws_vpc.main.id
  service_name        = "com.amazonaws.ap-east-1.secretsmanager"
  vpc_endpoint_type   = "Interface"
  subnet_ids          = var.private_subnet_ids
  security_group_ids  = [aws_security_group.vpc_endpoint.id]
  private_dns_enabled = true
}

resource "aws_kms_key" "app" {
  provider                = aws.prod
  description             = "prod application secrets and data"
  deletion_window_in_days = 30
  enable_key_rotation     = true
}

resource "aws_kms_alias" "app" {
  provider      = aws.prod
  name          = "alias/prod/app"
  target_key_id = aws_kms_key.app.key_id
}

resource "aws_secretsmanager_secret" "order_api" {
  provider   = aws.prod
  name       = "/prod/order-api/database"
  kms_key_id = aws_kms_key.app.arn
}

Secret value handling:

best:
    create secret metadata in Terraform
    write secret value by CI/CD or break-glass operation
    keep Terraform state free of production secret values

acceptable for non-prod:
    aws_secretsmanager_secret_version with encrypted remote state
    state access limited to platform team only

18.14 Terraform Apply Order#

1. org-baseline:
    aws_organizations_organization
    OU
    accounts or Control Tower Account Factory
    Control Tower landing zone / controls
    SCP

2. security-baseline:
    log archive bucket
    organization CloudTrail
    delegated admin
    GuardDuty / Security Hub / Access Analyzer / Config aggregator
    alerts and budgets

3. workload-baseline:
    SecurityAuditRole in every account
    GitHub OIDC deploy role
    VPC / endpoints / flow logs
    KMS / secrets

4. application-infra:
    ECS/EKS/RDS/S3/CloudFront/application resources

Production Terraform rules:

remote state:
    encrypted S3 backend
    DynamoDB or native state locking
    state bucket in log-archive or shared-services account
    access only from TerraformProvisionRole and platform admins

review:
    terraform plan in CI
    manual approval for prod
    no direct terraform apply from laptops for prod

import:
    import existing Control Tower / Account Factory resources before managing them
    never recreate production accounts, log buckets, or KMS keys