3. AWS Secrets Manager 进阶:权限策略、跨账户访问与自动轮换

上一章我们把 Secrets Manager 的基础操作捋了一遍——创建、读取、版本管理,基本够用了。但在生产环境里,你会发现真实的需求远不止这些:一个密钥该给谁读?怎么让另一个账号的 EC2 也能用?密钥能不能自动定期换?嗯,这才是进阶的门槛。我自己的经验是,这三个问题如果没想透,后面迟早要返工。

本章核心脉络:权限策略(IAM 策略 vs 资源策略) → 跨账户访问 → 自动轮换。三者环环相扣,箭头方向就是配置顺序。
AWS Secrets Manager 进阶知识结构 AWS Secrets Manager 进阶 权限策略 IAM 策略 vs 资源策略 最小权限原则 condition 上下文键 跨账户访问 资源策略 + 信任策略 外部 ID 防混淆 AssumeRole 流程 自动轮换 轮换策略(单用户/双用户) Lambda 执行角色 CloudWatch 定时触发 实践顺序建议 先从本账号的 IAM 策略入手 → 测试读取成功 → 再配置跨账户资源策略 → 最后启用自动轮换 定义权限边界 建立跨账号信任 自动化轮换生效

3.1 权限策略:IAM 策略与资源策略的区别

很多人一开始搞混这两个东西。IAM 策略是「谁可以做什么」,挂在用户/角色/用户组上;资源策略是「这个密钥允许谁访问」,直接挂到密钥上。说白了,IAM 策略是主体视角,资源策略是客体视角。

我个人习惯先写 IAM 策略。假设你的应用需要从 Secrets Manager 读取数据库密码,我会给它的执行角色加上这样的内联策略:

{
  "Version": "2012-10-17",
  "Statement": [{
    "Effect": "Allow",
    "Action": "secretsmanager:GetSecretValue",
    "Resource": "arn:aws:secretsmanager:us-east-1:123456789012:secret:db/prod/mysql-*"
  }]
}

注意 Resource 字段用了通配符。为什么?因为我可能把多个版本的密钥放在同一前缀下,这样就不用每次都改策略。但记得要限定区域和账号,别不小心开了全通配。

一个小技巧:把 secretsmanager:GetSecretValue 和 secretsmanager:DescribeSecret 分开授权。我曾经见过只给 GetSecretValue 却忘了 DescribeSecret,导致应用启动时无法获取密钥元数据。虽然能读到值,但日志里全是权限警告。

3.2 资源策略——更精细的控制

资源策略的优势在于你可以按密钥级别设置访问条件。比如我只允许来自 VPC 内特定 IP 段的请求读取某个密钥:

{
  "Version": "2012-10-17",
  "Statement": [{
    "Effect": "Allow",
    "Principal": { "AWS": "arn:aws:iam::123456789012:role/AppProdRole" },
    "Action": "secretsmanager:GetSecretValue",
    "Resource": "*",
    "Condition": {
      "IpAddress": { "aws:SourceIp": "10.0.0.0/16" }
    }
  }]
}

嗯,这里要注意:资源策略里 Resource 必须写 "*",因为它是挂载在具体密钥上的,AWS 会自动匹配。我当时第一次配的时候写成了具体的 ARN,结果提示无效。文档里其实写了,但藏在角落里。

对比维度 IAM 策略 资源策略
挂载位置用户、角色、用户组密钥本身
适用场景角色级批量授权跨账户、精细化条件控制
Resource 字段可写具体 ARN 或通配必须为 "*"
支持 Principal不支持(默认隐含自身账户)支持,用于跨账户
联合评估必须与资源策略同时 Allow 才生效必须与 IAM 策略同时 Allow

3.3 跨账户访问——打通不同的 AWS 账户

真实项目里,你可能有一个 DevOps 账户专门管理密钥,然后各个业务账户的应用需要读取。怎么实现?

  1. 在密钥所有者账户(A) 创建资源策略,指定允许外部账户的角色访问。
  2. 在目标账户(B) 创建 IAM 角色,并设置信任策略允许 A 账户的角色做 AssumeRole。
  3. 在应用侧 使用 STS 获取临时凭证,再调用 GetSecretValue。

举个例子。A 账户的密钥资源策略:

{
  "Principal": { "AWS": "arn:aws:iam::B-ACCOUNT-ID:role/CrossAccountReader" },
  "Action": "secretsmanager:GetSecretValue",
  "Resource": "*"
}

B 账户的角色信任策略:

{
  "Version": "2012-10-17",
  "Statement": [{
    "Effect": "Allow",
    "Principal": { "AWS": "arn:aws:iam::A-ACCOUNT-ID:role/SecretManagerAdmin" },
    "Action": "sts:AssumeRole"
  }]
}
避坑指南:我曾经因为忘了在资源策略里写 Principal,结果 B 账户一直报 AccessDenied。排查了半天,才发现资源策略默认只允许当前账户。跨账户必须显式指定外部 Principal。

另外,如果涉及到第三方合作,强烈建议加上 sts:ExternalId 条件,防止混淆代理人攻击。具体就是在信任策略里加一行:

"Condition": {
  "StringEquals": { "sts:ExternalId": "your-unique-id" }
}

3.4 自动轮换——让密钥“自我更新”

手动轮换密钥?在测试环境偶尔搞搞还行。生产环境里,我建议直接上自动轮换。AWS 提供了两种内置轮换策略:

  • 单用户轮换:创建一个新版本,同时更新 RDS/Redis 等服务的密码。适用于服务支持同时使用新旧密码的场景。
  • 双用户轮换:交替使用两个独立用户,逐个更新密码,确保任何时候至少有一个有效用户。适用于零停机场景。

实现步骤其实不复杂:

  1. 在 Secrets Manager 里启用自动轮换,指定轮换间隔(比如 30 天)。
  2. 提供一个 Lambda 函数,实现轮换逻辑。AWS 给 RDS、Redis、DocumentDB 提供了现成的模板。
  3. Lambda 执行角色需要有 secretsmanager:PutSecretValue 和操作目标服务(如 RDS)的权限。
  4. 设置 CloudWatch Events(现在叫 EventBridge)定时规则触发 Lambda。

下面是一个典型的 Lambda 轮换代码骨架(Python):

def lambda_handler(event, context):
    secret_id = event['SecretId']
    client = boto3.client('secretsmanager')
    secret = client.get_secret_value(SecretId=secret_id)
    # 解析旧密码,连接数据库
    # 更新数据库中的密码为新密码
    new_password = generate_secure_password()
    client.put_secret_value(SecretId=secret_id, SecretString='{"username":"...","password":"'+new_password+'"}')
    return {'statusCode': 200}
个人经验:一定要在 Lambda 里做重试和幂等处理。我遇到过 CloudWatch 触发重复调用的 case,结果两个并发轮换互相覆盖,差点导致密码乱掉。后来我在第一个步骤里加了一个「当前阶段」的状态判断,只有处于“创建”阶段才进行数据库更新。

另一个容易忽略的点:轮换过程中,Lambda 会创建一个新的版本,并用 AWSCURRENT 标签指向它。旧版本会标记为 AWSPREVIOUS。所以应用端读取应该始终使用 AWSCURRENT,而不是硬编码版本 ID。

警告:如果你手动通过控制台或 CLI 更新了密钥,自动轮换会以你手动设置的版本为基准继续工作。但如果你删除了某些历史版本,可能破坏轮换的状态机。我建议全程用自动轮换来管理,不要混用。

好了,这三个进阶能力串起来,就是一套安全可靠的密钥生命周期管理方案。权限策略搞定「谁可以干什么」,跨账户访问打通组织边界,自动轮换保证密钥不长期暴露。嗯,这样你的生产环境 Secrets Manager 才算真正用到位了。