我给自己写了一个IAM策略,用于一个太阳能报告代理。当时一次写就,自我感觉良好,以为已经做到了最小权限。

后来逐行细读,才发现并没有。

策略长什么样

这个代理实际只做两件事:从Secrets Manager读取一个密钥,在Bedrock上调用一个Claude模型生成每日报告。就两个动作。我按照这两个动作写了策略,当时还挺满意,直到我回头一行一行地看,才发现自己实际授予的权限和记忆里写的不完全一样。

下面是真实挂在角色上的策略:

{"Version": "2012-10-17","Statement": ["Sid": "SecretsManagerReadConfig","Effect": "Allow","Action": "secretsmanager:GetSecretValue","Resource": "arn:aws:secretsmanager:us-east-1:111122223333:secret:solar-daily-brief/config-*"},"Sid": "BedrockInvokeModel","Effect": "Allow","Action": "bedrock:InvokeModel","Resource": ["arn:aws:bedrock:*::foundation-model/anthropic.claude-*","arn:aws:bedrock:us-east-1:111122223333:inference-profile/us.anthropic.claude-*"}

第一段没问题,第二段开了大口子

先看第一条。一个动作,一个密钥,结尾的-*只是覆盖Secrets Manager在ARN后面自动追加的随机后缀。也就是说,这一条实际上只授权访问一个真实存在的资源,非常精准。

再看第二条。anthropic.claude-*匹配的是账号范围内所有Claude模型,而代码实际只调用其中一个。资源里的bedrock:*::还带了区域通配符,意味着任意区域都可以。这个范围显然超出实际需要。

同一个代理,同一个策略,两条语句的精度完全不同。这个落差就是这篇文章的重点。

这种宽松是怎么来的

没有人会主动决定写一个宽松的策略。它往往是直觉“顺手”产生的。你大概知道代码做什么,就大概给一下权限,想着“多给一点更安全”,然后就过去了。

Secrets Manager那条之所以精确,是因为从控制台复制一个ARN很简单,而且只有一个密钥需要指向。Bedrock那条就松了,因为我没有去看一个具体的ARN,而是在脑子里想“Claude模型”,于是anthropic.claude-*看起来是个合理的表达,省得我去查代码里到底用的哪个模型ID。

问题不在于“再努力记清楚一点”,而在于授权时不该靠记忆,应该靠代码实际做了什么。

代码实际需要的权限

代理自己的源码能解决直觉说不清的事。deye_client.pycollector.py中,对secretsmanager:*的调用只有一次get_secret_value(),针对的是一个固定密钥名,启动时解析一次,进程生命周期内缓存。写报告那一步调用bedrock-runtime.invoke_model(),模型ID从配置读取,只用了一个,不是一整个模型族,也不是运行时动态选择。

这个项目里没有任何代码路径需要第二个密钥或第二个模型。策略里多给的部分,不是最小权限,而是穿着最小权限外衣的猜测。

检查任何策略的方法

把这段逻辑推广到任何一个IAM策略:

  • 打开实际代码,找到所有对云服务的调用点
  • 记录每个调用涉及的资源ARN和操作
  • 对照策略里的每一条Action和Resource,删除没有对应调用的部分
  • 特别注意通配符:是覆盖一个确定前缀,还是覆盖了一整类资源
  • 检查区域字段,避免用代替具体区域

策略里的每一个字符,都应该能对应到代码里的具体行为。如果对应不上,那它就不是最小权限,只是看起来像。