IAM 策略建模:条件键、边界与跨账号

用 Terraform 建模 IAM 权限:策略文档的 Effect/Action/Resource 语义、用 data source 生成 JSON、条件键与标签约束、权限边界与会话策略、跨账号信任关系,以及策略验证与组织级治理。

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 与证书,那是外部可见面里同样需要精细建模的一环。

延伸阅读

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「terraform」更多文章

  1. Helm Provider 与应用发布:值注入与回滚
  2. 模块注册表与分发:版本、文档与测试
  3. DNS 与证书编排:托管区域与自动验证