1. 策略文档的结构与语义
一句话总结: IAM 策略是「默认全拒绝 + 显式允许」的集合模型,每条 statement 由 Effect、Action、Resource、Condition 四要素构成,缺一不可地决定了最终权限。
很多人把 IAM 策略当成配置文件来写,结果得到一个「能跑但过宽」的权限集。正确的心智模型是:策略是求值规则,不是清单。
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "ReadOwnBucket",
"Effect": "Allow",
"Action": ["s3:GetObject", "s3:ListBucket"],
"Resource": [
"arn:aws:s3:::app-assets",
"arn:aws:s3:::app-assets/*"
]
}
]
}
四要素的常见误解:
| 要素 | 常见误解 | 正确理解 |
|---|---|---|
| Effect | 可省略 | 必须显式写 Allow 或 Deny |
| Action | 可用通配符省事 | 通配符扩大爆炸半径 |
| Resource | 只写对象 ARN | 桶级操作与对象级操作 ARN 不同 |
| Condition | 可选项 | 精细化授权的唯一入口 |
# 桶级操作(ListBucket)作用于桶本身,对象级操作(GetObject)作用于 key
resource "aws_iam_policy" "assets_read" {
name = "${local.prefix}-assets-read"
policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Effect = "Allow"
Action = ["s3:ListBucket"]
Resource = [aws_s3_bucket.assets.arn]
}, {
Effect = "Allow"
Action = ["s3:GetObject"]
Resource = ["${aws_s3_bucket.assets.arn}/*"]
}]
})
}
一句话:
Resource写错是 IAM 策略最常见的静默失败——策略能创建成功,但调用时永远 AccessDenied。
2. 用数据源生成策略 JSON
一句话总结: 手写 JSON 字符串无法引用 Terraform 资源属性,
aws_iam_policy_document数据源把策略变成可插值的 HCL,是策略即代码的基础设施。
data "aws_iam_policy_document" "app_s3" {
statement {
sid = "ListBucket"
effect = "Allow"
actions = ["s3:ListBucket"]
resources = [aws_s3_bucket.assets.arn]
}
statement {
sid = "ObjectReadWrite"
effect = "Allow"
actions = [
"s3:GetObject",
"s3:PutObject",
]
resources = ["${aws_s3_bucket.assets.arn}/uploads/*"]
}
}
数据源的优势在于资源 ARN 直接引用,桶改名或换账号时策略自动跟随,不会留下悬空 ARN。
动态生成多条相似 statement 时用 dynamic 块:
data "aws_iam_policy_document" "per_env" {
dynamic "statement" {
for_each = var.environments
content {
sid = "Access${title(statement.value)}Bucket"
effect = "Allow"
actions = ["s3:GetObject"]
resources = [
"arn:aws:s3:::${var.project}-${statement.value}/*"
]
}
}
}
| 做法 | 可维护性 | 风险 |
|---|---|---|
| 手写 JSON 字符串 | 低,无法引用资源 | ARN 硬编码漂移 |
jsonencode 内联 | 中,可引用资源 | 无 Sid,审计困难 |
aws_iam_policy_document | 高 | 语法与原生 JSON 略有差异 |
一句话:策略文档的每一处硬编码 ARN,都是未来一次「权限莫名失效」的种子。
3. 条件键与精细化授权
一句话总结: 条件键把「谁能做」细化为「在什么条件下能做」,是迈向最小权限最有效的一步,代价是可读性下降。
最常用的条件键集中在来源身份、网络位置与资源标签三类:
data "aws_iam_policy_document" "restricted" {
statement {
sid = "OnlyViaTLS"
effect = "Deny"
actions = ["s3:*"]
resources = [
aws_s3_bucket.assets.arn,
"${aws_s3_bucket.assets.arn}/*",
]
condition {
test = "Bool"
variable = "aws:SecureTransport"
values = ["false"]
}
}
}
注意这里用的是 Deny:显式拒绝的优先级高于任何 Allow,因此「强制 HTTPS」类规则应当写成 Deny。
| 条件键 | 约束维度 | 典型用法 |
|---|---|---|
aws:SecureTransport | 传输安全 | 强制 TLS |
aws:SourceVpce | 网络路径 | 只允许经终端节点访问 |
aws:PrincipalTag/team | 主体标签 | 按团队隔离 |
aws:RequestedRegion | 地理边界 | 数据驻留合规 |
aws:MultiFactorAuthAge | 会话强度 | 敏感操作强制 MFA |
condition {
test = "StringEquals"
variable = "aws:RequestedRegion"
values = ["cn-north-1", "cn-northwest-1"]
}
3.1 用标签做属性化访问控制
基于标签的访问控制(ABAC)能让策略数量不随资源增长:
data "aws_iam_policy_document" "abac" {
statement {
effect = "Allow"
actions = ["ec2:StartInstances", "ec2:StopInstances"]
resources = ["*"]
condition {
test = "StringEquals"
variable = "aws:ResourceTag/Owner"
values = ["$${aws:PrincipalTag/team}"]
}
}
}
$${...} 的写法用于在 HCL 中输出字面的 ${...} 策略变量,写成单个 $ 会被 Terraform 当成插值而报错。
一句话:条件键写对了,策略数量会随团队增长而非随资源增长。
4. 权限边界与会话策略
一句话总结: 权限边界是「能力上限」,它不授予任何权限,只限制身份策略最多能到达哪里;对开发者自助建角色场景,它是唯一可扩展的护栏。
resource "aws_iam_role" "developer" {
name = "${local.prefix}-developer"
assume_role_policy = data.aws_iam_policy_document.developer_assume.json
permissions_boundary = aws_iam_policy.boundary.arn
}
resource "aws_iam_policy" "boundary" {
name = "${local.prefix}-boundary"
policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Effect = "Allow"
Action = ["s3:*", "dynamodb:*", "logs:*", "cloudwatch:*"]
Resource = "*"
}]
})
}
有效权限是身份策略与权限边界的交集:
| 层 | 作用 | 是否授予权限 |
|---|---|---|
| 身份策略 | 定义允许的操作 | 是 |
| 权限边界 | 定义能力上限 | 否,只裁剪 |
| SCP | 组织级上限 | 否,只裁剪 |
| 会话策略 | 临时会话进一步收窄 | 否,只裁剪 |
角色上的 max_session_duration 也常被忽略:默认 1 小时,长时间批处理任务需要提前调高,否则会在中途失效。
一句话:边界不是「更严格的策略」,而是「策略的天花板」——写错边界会让整个角色失去权限。
5. 跨账号角色与信任策略
一句话总结: 跨账号访问靠信任策略而非身份策略,信任策略的
Principal决定「谁能扮演」,并且应当配合ExternalId或条件键防止混淆代理问题。
data "aws_iam_policy_document" "cross_trust" {
statement {
effect = "Allow"
actions = ["sts:AssumeRole"]
principals {
type = "AWS"
identifiers = ["arn:aws:iam::${var.trusted_account_id}:root"]
}
condition {
test = "StringEquals"
variable = "sts:ExternalId"
values = [var.external_id]
}
condition {
test = "StringEquals"
variable = "aws:PrincipalOrgID"
values = [var.org_id]
}
}
}
两个条件缺一不可:ExternalId 防止第三方服务被诱导扮演角色,PrincipalOrgID 防止把账号 ID 转让给组织外的人后仍可访问。
调用侧则用数据源读取凭证,而不是硬编码密钥:
provider "aws" {
alias = "target"
region = var.target_region
assume_role {
role_arn = "arn:aws:iam::${var.target_account_id}:role/${local.prefix}-cross"
external_id = var.external_id
session_name = "terraform-${var.environment}"
}
}
| 信任方式 | 适用场景 | 安全强度 |
|---|---|---|
| 账号 root 主体 + ExternalId | 第三方集成 | 中高 |
| 指定具体角色 ARN | 组织内服务互联 | 高 |
PrincipalOrgID 约束 | 多账号组织 | 高 |
| OIDC 联合(CI) | 无密钥流水线 | 高 |
一句话:跨账号信任策略的每一个宽松条件,都是别人进入你账号的一条通道。
6. 策略验证与合规检查
一句话总结: 策略写完后必须验证「能否解析」「是否过宽」「是否漂移」,这三类检查分别由 API 校验、静态分析与合规扫描承担。
第一步是语法与语义校验,避免创建出永远不生效的策略:
# 用 AWS CLI 校验策略文档语法(不创建资源)
aws iam simulate-custom-policy \
--policy-input-list file://policy.json \
--action-names s3:GetObject
第二步是检查通配符滥用。一条 Action: "*" 配上 Resource: "*" 就是管理员权限:
# 在仓库中搜索高危模式
grep -rn '"Action": *"\*"' --include='*.json' --include='*.tf' .
grep -rn 'Resource *= *"\*"' --include='*.tf' .
第三步是把检查固化进流水线:
# 用变量限制通配符,强制在代码评审时暴露
variable "allow_wildcard_resource" {
type = bool
default = false
description = "显式开启才允许 Resource 为通配符"
}
resource "aws_iam_policy" "guarded" {
lifecycle {
precondition {
condition = !var.allow_wildcard_resource
error_message = "禁止在生产环境使用通配符资源。"
}
}
}
| 检查类型 | 工具 | 拦截时机 |
|---|---|---|
| 语法校验 | IAM API / simulate-custom-policy | 创建前 |
| 通配符扫描 | grep / Checkov | 提交时 |
| 权限模拟 | simulate-principal-policy | 合并前 |
| 漂移检测 | terraform plan | 每次运行 |
一句话:能被自动化拦住的权限问题,就不应该留给代码评审去发现。
7. 组织级治理与 SCP
一句话总结: SCP 是组织根部的「全局上限」,它不授予权限,但可以让某些操作在任何账号内都无法执行,是防止单点误配置的最后一道闸。
resource "aws_organizations_policy" "deny_region" {
name = "deny-outside-region"
description = "禁止在指定区域外创建资源"
type = "SERVICE_CONTROL_POLICY"
content = jsonencode({
Version = "2012-10-17"
Statement = [{
Sid = "DenyOutsideApprovedRegions"
Effect = "Deny"
Action = "*"
Resource = "*"
Condition = {
StringNotEquals = {
"aws:RequestedRegion" = var.approved_regions
}
}
}]
})
}
resource "aws_organizations_policy_attachment" "root" {
policy_id = aws_organizations_policy.deny_region.id
target_id = var.organization_root_id
}
SCP 的作用范围与 IAM 策略完全不同:
| 维度 | IAM 策略 | SCP |
|---|---|---|
| 作用对象 | 用户 / 角色 / 组 | 账号 / OU |
| 是否授权 | 是 | 否 |
| 是否影响管理账号 | 不适用 | 不影响 |
| 生效顺序 | 求值后决定 | 先行裁剪 |
把 SCP 与权限边界叠加使用,可以形成「组织级区域限制 + 账号级能力上限 + 角色级最小授权」的三层结构。
一句话:SCP 不能授予权限,只能收回权限——它的价值在于「让错误的配置也做不到」。
8. 总结
IAM 策略建模的本质是层层收窄:
| 环节 | 要点 |
|---|---|
| 结构 | Effect/Action/Resource/Condition 四要素齐备 |
| 生成 | 用 aws_iam_policy_document 引用资源 ARN |
| 条件键 | 用 Deny + 条件强制 TLS、区域、标签约束 |
| ABAC | 用 $${aws:PrincipalTag/...} 让策略不随资源膨胀 |
| 边界 | 边界是上限不是授权,写错即全失效 |
| 跨账号 | 信任策略 + ExternalId + PrincipalOrgID |
| 验证 | 语法校验、通配符扫描、权限模拟三层 |
| 治理 | SCP 先行裁剪,与边界叠加成三层结构 |
一句话收尾:权限是唯一「配错也不会立刻报错」的基础设施。一个过宽的 S3 策略会让一切正常运转,直到某天数据出现在不该出现的地方。把策略写成可引用资源 ARN 的代码、用条件键收窄到具体场景、用边界与 SCP 兜住底线,这套组合拳的价值不在于「更安全」,而在于「让安全成为可审查的代码」。下一篇转向 DNS 与证书,那是外部可见面里同样需要精细建模的一环。
延伸阅读
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。