4. 存储桶策略语法详解:JSON结构、Principal、Action、Resource、Condition
说实话,刚接触桶策略那会儿,看着一长串 JSON 我也有点头大。但你把它拆成五块就简单了:Principal、Action、Resource、Condition,再加一个框架 Statement。我个人习惯把策略 JSON 想象成一张申请表:谁(Principal)能做什么(Action)对什么(Resource)在什么条件下(Condition)。
4.1 JSON 结构概览
一个完整的桶策略长这样:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"AWS": "arn:aws:iam::123456789012:user/Alice"
},
"Action": [
"s3:GetObject",
"s3:PutObject"
],
"Resource": "arn:aws:s3:::my-bucket/*",
"Condition": {
"IpAddress": {
"aws:SourceIp": "192.168.1.0/24"
}
}
}
]
}
最外层两个固定字段:Version 和 Statement。Version 目前只能用 2012-10-17,别问为什么,AWS 定的。Statement 是一个数组,每条策略规则就是数组里的一项。
每次写新策略,我习惯先搭好这个骨架:
{
"Version": "2012-10-17",
"Statement": []
}
然后往 Statement 里塞具体规则。下面咱们逐个拆解。
4.2 Principal —— 谁
Principal 指定谁能拿到这个策略的权限。可以是:
- AWS 账户:
"AWS": "123456789012" - IAM 用户/角色:
"AWS": "arn:aws:iam::123456789012:user/Alice" - 联合身份用户:用
"Federated"配合 SAML 或 Web Identity - 所有人(匿名):
"*"—— 慎用!
我记得有个项目要做公开读访问,领导说“直接给所有人 GetObject 权限吧”。我坚持加了 IP 限制,结果第二天就发现有人在疯狂扫我们的桶。嗯…… "*" 不是不能用,但一定要配合 Condition。
"AWS": "123456789012",不用一个个列 ARN。但要注意——这个 Principal 包括根用户和所有 IAM 用户,粒度比较粗。
匿名访问的写法:
"Principal": "*"
等价于 "Principal": {"AWS": "*"}。还有一种 "CanonicalUser" 用于 S3 的原始账户 ID,现在很少用了,知道就行。
4.3 Action —— 做什么
Action 列表告诉 S3 允许或拒绝哪些操作。常用的:
| 类别 | Action | 说明 |
|---|---|---|
| 读操作 | s3:GetObject | 下载对象 |
| 写操作 | s3:PutObject | 上传对象 |
| 删除 | s3:DeleteObject | 删除对象 |
| 列表 | s3:ListBucket | 列出桶内对象 |
| 权限 | s3:PutBucketPolicy | 修改桶策略 |
| 跨域 | s3:GetBucketCors | 读取 CORS 配置 |
可以用通配符 * 代表所有 S3 操作:"Action": "s3:*"。但生产环境我从来不建议这样写——你想想看,给了 ListBucket 和 GetObject 还不够吗?给到 PutBucketPolicy 就等于把桶的钥匙交出去了。
"Action": "s3:*" ,结果发现运维同事不小心把桶的日志配置删了。最小权限原则不是说着玩的。
4.4 Resource —— 对什么
Resource 指定策略影响的范围。对 S3 来说,桶本身和桶里的对象是两个不同的资源 ARN:
- 桶:
arn:aws:s3:::my-bucket(注意结尾没有/*) - 对象(所有):
arn:aws:s3:::my-bucket/* - 特定前缀:
arn:aws:s3:::my-bucket/images/*
常见错误:想允许用户列出桶的内容,结果 Resource 写成了 arn:aws:s3:::my-bucket/*。ListBucket 操作是针对桶的,不是对象。所以你必须同时写两个 Resource:
"Resource": [
"arn:aws:s3:::my-bucket",
"arn:aws:s3:::my-bucket/*"
]
说白了,一个策略语句里,s3:ListBucket 只能配桶 ARN,s3:GetObject 只能配对象 ARN。混着写会报验证错误。我刚开始学的时候老搞混,后来记了一句口诀:“桶上放列表,对象上读写”。
4.5 Condition —— 在什么条件下
Condition 是桶策略最灵活的部分,也是很多人容易忽略的。它本质上是一组判断条件,只有条件满足时策略才生效。
常用的 Condition 类型:
| 条件键 | 示例 | 场景 |
|---|---|---|
aws:SourceIp | 192.168.0.0/16 | 只允许公司内网访问 |
aws:SourceVpce | vpce-xxx | 只允许通过 VPC Endpoint 访问 |
s3:x-amz-server-side-encryption | AES256 | 强制上传时加密 |
s3:Prefix | logs/ | 限制 ListBucket 到特定前缀 |
aws:MultiFactorAuthPresent | true | 要求 MFA 才能操作 |
Condition 结构是:
"Condition": {
"条件运算符": {
"条件键": "条件值"
}
}
常用运算符:IpAddress(IP 匹配)、StringEquals(字符串相等)、ArnLike(ARN 通配)、Bool(布尔值)。
aws:SourceIp 是 10.0.0.0/8,但忘了写 IpAddress 而是写了 StringEquals。结果当然是不匹配,所有请求都被拒绝。记住:IP 判断必须用 IpAddress,不用 StringEquals。
4.6 组合起来:一个完整的示例
假设我们要实现这样一个场景:
- 允许账号
123456789012下的 IAM 用户Alice从公司内网上传日志到my-bucket/logs/ - 要求所有上传的对象必须使用 SSE-S3 加密
- 拒绝任何来自外网的访问
策略如下:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"AWS": "arn:aws:iam::123456789012:user/Alice"
},
"Action": "s3:PutObject",
"Resource": "arn:aws:s3:::my-bucket/logs/*",
"Condition": {
"IpAddress": {
"aws:SourceIp": "10.0.0.0/8"
},
"StringEquals": {
"s3:x-amz-server-side-encryption": "AES256"
}
}
},
{
"Effect": "Deny",
"Principal": "*",
"Action": "s3:*",
"Resource": [
"arn:aws:s3:::my-bucket",
"arn:aws:s3:::my-bucket/*"
],
"Condition": {
"NotIpAddress": {
"aws:SourceIp": "10.0.0.0/8"
}
}
}
]
}
注意这里用了两条 Statement:先明确允许 Alice 从内网上传加密日志,再用一条 Deny 拒绝所有来自外网的请求(包括 Alice 自己如果走了外网 IP 也会被拒绝)。这就是 显式 Deny 覆盖显式 Allow 的经典例子。
图中把五个核心元素和它们的位置关系画清楚了。你每次写策略时,脑子里过一遍这个结构,基本不会漏字段。
好了,这一节的核心就是吃透 JSON 那五个字段。下一轮实战中你会频繁用到它们,到时候再回头看这个结构图,会觉得特别顺。