用用户与策略控制访问
目标
创建服务用户,亲手编写并附加最小权限策略,养成把权限范围收窄到前缀级别的习惯。
为什么重要
存储泄露事故大多不是来自复杂的攻击,而是来自开得过宽的权限。比如以“只有图片而已”为由,把存储桶开放为匿名读取,几个月后备份或用户上传的内容就混进了这个存储桶。把策略的 Resource 设为整个存储桶(arn:aws:s3:::bucket/*)也是同样的问题——只要一个服务的凭据泄露,整个存储桶就暴露了。如果收窄到前缀,损害就被限制在那个前缀之内。而且这同时也是对实现缺陷的防御。MinIO 的 CVE-2025-62506 是一个通过绕过会话策略而提升权限的漏洞。在那样的时刻,写窄策略的习惯就是最后一道防线。
本实验使用与 AWS 相同的 IAM API(aws iam ...)。服务器是 SeaweedFS 的 S3 网关,它以与 AWS 相同的调用接收用户、访问密钥、托管策略和附加操作。因此这里用到的命令和策略文档,可以原样带到 AWS 账号中使用。
步骤
- 用管理员 profile
local执行aws iam create-user --user-name appuser,并把通过aws iam create-access-key --user-name appuser得到的密钥写入 profileapp(同时写入endpoint_url和region)。 - 尝试
aws --profile app s3 ls s3://lab-media,并把结果保存到/root/pol/denied.txt。文件中必须有AccessDenied。 - 在
/root/pol/readonly.json中写入策略。允许s3:GetObject和s3:ListBucket,Resource有arn:aws:s3:::lab-media和arn:aws:s3:::lab-media/*两个。 - 用
aws iam create-policy --policy-name labreadonly --policy-document file:///root/pol/readonly.json创建托管策略,并用aws iam attach-user-policy附加到 appuser。aws --profile app s3 ls s3://lab-media必须成功。 aws --profile app s3 cp /opt/fixtures/s3/readme.txt s3://lab-media/doc/nope.txt必须失败。在/root/pol/write.txt中写入write_denied=true。- 在
/root/pol/imgonly.json中写入把Resource收窄为arn:aws:s3:::lab-media/img/*的策略,以labimgonly为名创建并附加(解除较宽泛的labreadonly)。用 appuser 查询img/logo.png必须成功,查询doc/sales.csv必须失败。在/root/pol/prefix.txt中写入img=ok doc=denied。 - 用存储桶策略,只把
lab-media的public/前缀开放为匿名下载。匿名 GET 访问public/之下的对象必须返回 200,访问private/之下的对象必须返回 403。在/root/pol/anon.txt中写入public=200 private=403。
参考
- 用户与密钥:
aws iam list-users·aws iam list-access-keys --user-name appuser - 查看策略列表和附加情况:
aws iam list-policies·aws iam list-attached-user-policies --user-name appuser - 策略 ARN 不要手写,而是用
list-policies查找。AWS 中包含账号 ID(arn:aws:iam::123456789012:policy/…),而这台服务器中为空(arn:aws:iam:::policy/…),格式不同。 - 用同样的名称再次执行
create-policy会得到EntityAlreadyExists。要修改文档时,使用aws iam create-policy-version --set-as-default。 - 存储桶策略:
aws s3api put-bucket-policy --bucket lab-media --policy file://…· 确认用get-bucket-policy - IAM 的变更可能不会立即生效。AWS 也需要几秒钟,因此附加之后立即确认时,多重试几次更稳妥。
- 这台服务器的局限:直接写到用户上的内联策略(
put-user-policy)只接受Allow(Deny会得到MalformedPolicyDocument)。托管策略和存储桶策略也接受Deny,所以本实验使用 AWS 推荐的托管策略。 - 常见错误 1:漏掉
ListBucket——对象可以读取,却无法列出。它们是不同的 Action,Resource 的格式也不同。 - 常见错误 2:把
Resource设为整个存储桶——一个凭据泄露,全部暴露。
创建服务用户
用管理员 profile local 执行 aws iam create-user --user-name appuser,并把通过 aws iam create-access-key --user-name appuser 得到的密钥写入 profile app(同时写入 endpoint_url 和 region)。
用管理员 profile 创建用户并签发访问密钥。密钥(secret key)只会在签发响应中显示一次,立即写入 profile。
确认没有策略的用户被拒绝
尝试 aws --profile app s3 ls s3://lab-media,并把结果保存到 /root/pol/denied.txt。文件中必须有 AccessDenied。
用新 profile 尝试列出会被拒绝。默认情况下没有任何权限。
编写只读策略
在 /root/pol/readonly.json 中写入策略。允许 s3:GetObject 和 s3:ListBucket,Resource 有 arn:aws:s3:::lab-media 和 arn:aws:s3:::lab-media/* 两个。
用 JSON 写出 Effect、Action、Resource。列出内容和读取对象是不同的 Action。
创建并附加托管策略,使读取成功
用 aws iam create-policy --policy-name labreadonly --policy-document file:///root/pol/readonly.json 创建托管策略,并用 aws iam attach-user-policy 附加到 appuser。aws --profile app s3 ls s3://lab-media 必须成功。
创建策略和把它附加到用户,是两个独立的步骤。附加之后,生效可能需要几秒钟。
确认写入仍然被拒绝
aws --profile app s3 cp /opt/fixtures/s3/readme.txt s3://lab-media/doc/nope.txt 必须失败。在 /root/pol/write.txt 中写入 write_denied=true。
如果是只读策略,上传就应该失败。失败才是正常的。
创建限定前缀的策略
在 /root/pol/imgonly.json 中写入把 Resource 收窄为 arn:aws:s3:::lab-media/img/* 的策略,以 labimgonly 为名创建并附加(解除较宽泛的 labreadonly)。用 appuser 查询 img/logo.png 必须成功,查询 doc/sales.csv 必须失败。在 /root/pol/prefix.txt 中写入 img=ok doc=denied。
在 Resource 中把通配符一直加到前缀。要让 img/ 可以访问,而 doc/ 不可以。
只把特定前缀匿名公开
用存储桶策略,只把 lab-media 的 public/ 前缀开放为匿名下载。匿名 GET 访问 public/ 之下的对象必须返回 200,访问 private/ 之下的对象必须返回 403。在 /root/pol/anon.txt 中写入 public=200 private=403。
开放整个存储桶和只开放一个前缀,事故的规模不同。
留意 Resource 末尾的那一个斜杠。本实验的要点全在这里。
"Resource": "arn:aws:s3:::lab-media/public*" → public 으로 시작하는 모든 키
"Resource": "arn:aws:s3:::lab-media/public/*" → public/ 아래 키만
S3 中没有文件夹。策略看的只有字符串前缀,所以漏掉斜杠的话,像 public-backup.tar 这样仅仅以 public 开头的根对象也会被一起开放。用 aws s3api get-bucket-policy --bucket lab-media 确认服务器上实际生效的 Resource。
一个存储桶只有一份存储桶策略,每次执行 put-bucket-policy 都会整体替换。要更改范围时,重新提交整份文档。