不用长期密钥也能把权限交出去
目标
角色确实比长期密钥更安全,但信任策略一旦写得宽松,这份安全就会全部消失。本实验无需云账号:亲手编写信任策略,再亲手做一个能识别危险写法的检查器,来确认问题出在哪里。
信任策略与权限策略
把这两份文档搞混,就什么都对不上。
- 权限策略——这个角色能做什么
- 信任策略——谁可以扮演这个角色
本实验用到的是信任策略这一侧,只有最后一步是权限策略。
条件
- 应用运行在 EC2 实例上。
- 需要向合作方账号
210987654321开放读取权限。 - CI 只允许从仓库
acme/platform的refs/heads/main部署。 - 开发者在创建 ECS 任务时,必须能够附加任务角色。
语法检查
策略通常是以代码而不是控制台的方式提交的。JSON 一旦损坏,报错信息就与策略内容无关,所以每次都检查一下会更快。
python3 -m json.tool /root/roles/trust-ec2.json
要创建的内容
| 文件 | 内容 |
|---|---|
/root/roles/trust-ec2.json |
由实例扮演的角色 |
/root/roles/trust-partner.json |
向外部账号开放的角色 |
/root/roles/trust-ci.json |
通过联合身份扮演的角色 |
/root/roles/lint_trust.py |
危险信任策略检查器 |
/root/roles/passrole.json |
传递角色的权限 |
/root/roles/06-session.md |
会话时长计算 |
/root/roles/07-notes.md |
总结 |
参考
第 4 步的检查器会同时对藏起来的四份策略和你创建的三份策略运行。 漏掉危险的策略算失败,把安全的策略误判为危险也算失败。
让实例自己获取凭据
把 EC2 实例可以扮演的角色的信任策略写入 /root/roles/trust-ec2.json。Version 的值为 2012-10-17。
信任策略只决定“谁可以扮演这个角色”。能做什么,则由另一份权限策略决定。
关键在于主体不是人,而是服务。在 Principal 中写入 Service,操作则只有扮演角色这一个动作。
把这个角色附加到实例后,实例内运行的程序无需密钥文件就能获取凭据。凭据会在过期前自动刷新,不需要人工干预。
向外部账号开放时要多填一项
把合作方账号 210987654321 可以扮演的角色的信任策略写入 /root/roles/trust-partner.json。不能只写账号 ID。
只写账号 ID 的话,该账号里的任何人都可以扮演这个角色。只要合作方的一名员工凭据泄露,我们的角色就会被打开。
因此要再加一个只有双方知道的值作为条件:用 StringEquals 比较 sts:ExternalId,该值要难以猜测,长度至少为八个字符。
没有这个条件的话,委托该合作方办事的第三方就可能经由合作方触达我们的角色。
在 CI 中去掉密钥
把 CI Runner 通过 OIDC 扮演的角色的信任策略写入 /root/roles/trust-ci.json。只允许从仓库 acme/platform 的 refs/heads/main 扮演。
通过联合身份扮演时,操作不是 sts:AssumeRole,而是 sts:AssumeRoleWithWebIdentity,主体是写在 Principal.Federated 中的身份提供商。
漏掉条件的话,任何使用该 CI 服务的仓库都能扮演我们的角色。这是最常见的错误,一行条件就能堵住。
用 StringEquals 锁定 :sub,并同时检查 :aud。如果用 StringLike 配合 *,只写仓库部分,那么该仓库的任何分支都能扮演这个角色,与没有条件相差无几。
识别危险信任策略的检查器
创建 /root/roles/lint_trust.py。用 python3 /root/roles/lint_trust.py <정책.json>(占位符为策略 JSON 文件)调用时,对危险的策略输出其危险之处并以退出码 1 结束,对安全的策略以退出码 0 结束。
检查三件事。
- 主体是否敞开:
Principal本身是*,或Principal.AWS是* - 已把外部账号放在
Principal.AWS中,却没有sts:ExternalId条件 - 操作为
sts:AssumeRoleWithWebIdentity,但没有:sub条件,或值中含有*
评分器也会把前三步中创建的策略放进这个检查器测试。把安全的策略判为危险同样算失败——一旦开始出现误报,人们就会把检查器关掉。
收窄传递角色的权限
开发者在创建 ECS 任务时必须能够附加任务角色。把这项权限写入 /root/roles/passrole.json,并把可附加的角色和可传递到的服务也一并收窄。
创建资源时给该资源附加角色,实际上等于“可以让别人替我做我自己做不了的事”的权限。如果把 Resource 设为 *,低权限用户就能创建附加了管理员角色的资源,从而提升权限。
在 Resource 中写入允许附加的角色的 ARN。也可以只固定名称前半部分,后半部分放开。
还要用 iam:PassedToService 条件限定只有传递给哪个服务时才可以。这样就堵住了把同一个角色传递给其他服务的路径。
会话时长决定响应速度
假设凭据刚签发就泄露了。发现需要 3 小时,决定如何响应需要 1 小时。分别计算会话为 12 小时和 1 小时时,撤销权限之后仍然剩余的时间,并在 /root/roles/06-session.md 中至少写三行。
即使缩减了角色的权限,已经签发的会话也不会被取消。在剩余有效期内,它仍然可以照常使用。
到撤销为止需要 3 + 1 小时。会话为 12 小时时,之后还剩多少?会话为 1 小时时呢?
把数字写下来,“会话要短”就不再是个人喜好,而成了可以计算的结论。
总结三件事
在 /root/roles/07-notes.md 中至少写三行:长期密钥与临时凭据的区别、信任策略决定什么、对外部账号和联合身份不能漏掉什么。
正文中必须包含 만료、신뢰 정책 和 조건(均为韩文,依次意为“过期”“信任策略”“条件”)。