4. IAM治理与权限边界
在单账号、单环境的早期阶段,IAM 治理往往只是“给开发者一个 AdministratorAccess 就完事”。但当基础设施即代码(IaC)把资源扩展到 dev、staging、prod 多个账号时,权限问题会从“能不能用”变成“会不会越权、会不会误删、能不能审计”。本节要解决的核心问题是:如何用 IAM 策略、角色、权限边界、SCP 这四层机制,把最小权限原则真正落到多环境里,而不是停留在口号上。
四层权限机制各自管什么
先厘清四种机制的作用范围,这是后续组合设计的基础。
- IAM 策略(Policy):定义“允许或拒绝哪些动作作用于哪些资源”,可以附加到用户、角色或资源上。它是权限的最小表达单元。
- IAM 角色(Role):一种可被临时扮演的身份,通过信任策略决定“谁可以扮演我”。CI/CD 流水线、跨账号访问、EC2 实例都应使用角色而非长期密钥。
- 权限边界(Permissions Boundary):附加到用户或角色上的“权限上限”。即使身份策略允许某动作,只要边界不允许,最终仍然被拒绝。它不授予权限,只限制权限。
- SCP(Service Control Policy):组织级(Organizations)策略,作用于整个账号或 OU,规定账号内所有身份“最多能做什么”。它同样不授予权限,只做上限约束。
关键关系是:SCP 限制账号,权限边界限制单个身份,身份策略授予具体权限,角色提供临时凭证。四者叠加后,最终有效权限是它们的交集。
多环境落地:从账号结构开始
最小权限在多环境中的第一道设计不是写策略,而是划分账号与 OU。推荐结构是:根 OU 下分 Security、Infrastructure、Workloads,Workloads 下再按环境分 dev、staging、prod 三个 OU。每个环境一个独立账号,这样 SCP 可以按 OU 差异化下发,资源天然隔离,计费也清晰。
以 prod OU 为例,可以附加如下 SCP 片段(示例,非实测结果):
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "DenyDangerousActions",
"Effect": "Deny",
"Action": [
"iam:CreateUser",
"iam:CreateAccessKey",
"cloudtrail:StopLogging",
"cloudtrail:DeleteTrail",
"organizations:LeaveOrganization"
],
"Resource": "*"
},
{
"Sid": "DenyOutsideApprovedRegions",
"Effect": "Deny",
"NotAction": ["iam:*", "sts:*", "cloudfront:*"],
"Resource": "*",
"Condition": {
"StringNotEquals": {
"aws:RequestedRegion": ["ap-southeast-1", "us-east-1"]
}
}
}
]
}
这段 SCP 做了两件事:禁止创建长期 IAM 用户和访问密钥(强制走角色),禁止关闭审计日志;同时把资源操作限制在批准的区域。注意 DenyOutsideApprovedRegions 使用了 NotAction 排除全局服务,否则 IAM、STS 这类全局调用会被误伤。
权限边界:给“可能写错”的身份加保险
开发者角色通常需要一定的自主权,但又不该拥有删除生产数据库的能力。权限边界的价值在于:即使开发者自己修改了角色的身份策略,也无法突破边界。下面是一个开发者角色的边界示例:
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "DeveloperBoundary",
"Effect": "Allow",
"Action": [
"s3:*", "dynamodb:*", "lambda:*",
"logs:*", "cloudwatch:*", "sqs:*", "sns:*",
"ec2:Describe*", "ec2:CreateTags",
"iam:PassRole", "sts:AssumeRole"
],
"Resource": "*"
}
]
}
边界只列“允许的服务大类”,不写具体资源 ARN——资源粒度的控制交给身份策略。这样边界稳定、易维护,身份策略则按项目灵活调整。创建角色时通过 --permissions-boundary 参数附加:
aws iam create-role \
--role-name dev-iac-deployer \
--assume-role-policy-document file://trust-policy.json \
--permissions-boundary arn:aws:iam::111122223333:policy/DeveloperBoundary
角色与信任策略:让 IaC 不持有长期密钥
CI/CD 流水线访问 AWS 的正确方式是使用 OIDC 联合身份,而不是把 AK/SK 塞进环境变量。GitHub Actions 的信任策略示例:
{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Principal": {
"Federated": "arn:aws:iam::111122223333: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:my-org/my-infra:ref:refs/heads/main"
}
}
}]
}
条件里的 sub 是关键:它把角色限定到特定仓库的特定分支,其他仓库或 PR 分支无法扮演。dev 与 prod 各建一个角色,prod 角色的信任策略只允许 main 分支,dev 可以放宽到 develop。
最小权限的迭代方法
最小权限不是一次写成的,而是“先给合理范围,再根据实际调用收敛”。推荐流程:
- 初始身份策略按服务大类授予,配合权限边界兜底。
- 开启 CloudTrail,把日志投递到 CloudWatch Logs。
- 用 CloudWatch Logs Insights 查询一段时间内该角色实际调用的动作:
fields eventSource, eventName
| filter userIdentity.arn like /dev-iac-deployer/
| stats count() by eventSource, eventName
| sort count desc
把结果与当前策略对比,删除从未出现的动作,把 Resource: "*" 收敛到实际访问的 ARN 前缀。每季度重复一次,权限就会逐步逼近真实需求。
Deny 会覆盖账号内所有身份,包括根用户和 AdministratorAccess。修改 SCP 前务必在非生产 OU 验证,避免把自己锁在账号外。建议保留一个不受 SCP 限制的 break-glass 角色,并对其使用强 MFA。
多环境权限对照
| 维度 | dev | staging | prod |
|---|---|---|---|
| SCP 严格度 | 宽松,允许实验服务 | 中等,限制区域 | 严格,禁止 IAM 用户、禁止关审计 |
| 权限边界 | 覆盖常用服务 | 覆盖常用服务 | 收窄到部署必需服务 |
| CI/CD 角色信任 | develop + main 分支 | main 分支 | 仅 main 分支 + 人工审批 |
| 人工访问 | 开发者角色,可写 | 只读 + 受限写 | 只读,写操作走流水线 |
把这张表落到 IaC 里,就是用同一套 Terraform 模块,通过变量传入不同环境的 SCP ARN、边界 ARN 和信任条件。环境差异体现在变量文件,而不是复制三份策略。这样既保证结构一致,又让 prod 的收紧有据可查、可审计、可回滚。