3. AWS Secrets Manager 进阶:权限策略、跨账户访问与自动轮换
上一章我们把 Secrets Manager 的基础操作捋了一遍——创建、读取、版本管理,基本够用了。但在生产环境里,你会发现真实的需求远不止这些:一个密钥该给谁读?怎么让另一个账号的 EC2 也能用?密钥能不能自动定期换?嗯,这才是进阶的门槛。我自己的经验是,这三个问题如果没想透,后面迟早要返工。
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 账户专门管理密钥,然后各个业务账户的应用需要读取。怎么实现?
- 在密钥所有者账户(A) 创建资源策略,指定允许外部账户的角色访问。
- 在目标账户(B) 创建 IAM 角色,并设置信任策略允许 A 账户的角色做 AssumeRole。
- 在应用侧 使用 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 等服务的密码。适用于服务支持同时使用新旧密码的场景。
- 双用户轮换:交替使用两个独立用户,逐个更新密码,确保任何时候至少有一个有效用户。适用于零停机场景。
实现步骤其实不复杂:
- 在 Secrets Manager 里启用自动轮换,指定轮换间隔(比如 30 天)。
- 提供一个 Lambda 函数,实现轮换逻辑。AWS 给 RDS、Redis、DocumentDB 提供了现成的模板。
- Lambda 执行角色需要有
secretsmanager:PutSecretValue和操作目标服务(如 RDS)的权限。 - 设置 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 会创建一个新的版本,并用 AWSCURRENT 标签指向它。旧版本会标记为 AWSPREVIOUS。所以应用端读取应该始终使用 AWSCURRENT,而不是硬编码版本 ID。
好了,这三个进阶能力串起来,就是一套安全可靠的密钥生命周期管理方案。权限策略搞定「谁可以干什么」,跨账户访问打通组织边界,自动轮换保证密钥不长期暴露。嗯,这样你的生产环境 Secrets Manager 才算真正用到位了。