Links#
- AWS Security Reference Architecture
- AWS Control Tower
- AWS Control Tower controls
- AWS Organizations SCP examples
- Amazon GuardDuty Organizations
- AWS CloudTrail organization trail
- IAM Identity Center
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 low2. 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
└── Suspended3. 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 verification4. 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 / OPAConsole path:
AWS Control Tower console
Controls
search control by name or objective
choose target OU, for example Workloads/Prod
Enable control
wait until status = EnabledCLI 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 textVerify 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-prodRecommended 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 Sandbox5. 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-16. 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 immutabilityOrganization 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 role7. 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-1 和 us-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/ViewOnlyAccessGitHub 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 Environment10. 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, SSMSecurity 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 accessibility11. 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 dataExample 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 baselineProd 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 normal13. 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 itBudget 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 channel16. 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-1Expected 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 = ACTIVE17. 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 timeline18. 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 roles18.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 control18.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_document 或 jsonencode(),再 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_account18.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 only18.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 resourcesProduction 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