在多用户和多存储桶的多对多分配权限时使用 S3 ABAC 解决IAM Policy长度限制问题
本文介绍在100+用户和100+存储桶的多对多授权场景下,使用S3 ABAC(基于属性的访问控制)通过标签匹配替代传统RBAC方案,解决IAM Managed Policy数量和长度限制问题。方案采用StringLike通配符匹配,以分隔符拼接组名作为标签值,仅需3条固定Policy即可覆盖全部授权,支持读写/只读分级控制,新增用户或存储桶时只需打标签,无需修改Policy。
本文介绍如何使用S3 ABAC功能解决多用户和多存储桶管理权限时候,经常遇到的IAM Policy长度限制问题。
一、方案概述
1、背景
本方案使用 ABAC(Attribute-Based Access Control,基于属性的访问控制)解决以下问题:
- 100+ IAM User 对 100+ S3 存储桶的精确权限管理
- 支持同一用户对不同存储桶分别拥有只读或读写权限
- 规避 IAM Managed Policy 每个 Group 最多 10 条的硬限制(无法提升)
- 规避单条 Managed Policy 6,144 字符的长度限制
- 规避 Inline Policy 的长度限制(User 2,048 / Group 5,120 字符)
- 限制访问来源为 AWS Console 和 VPC(172.31.0.0/16)
2、IAM 限额参考
| 限制项 | 默认值 | 最大值 |
|---|---|---|
| Managed Policies per Group | 10 | 10(不可提升) |
| Managed Policies per User | 10 | 20 |
| 单条 Managed Policy 大小 | 6,144 字符 | 6,144 字符 |
| IAM User 标签值最大长度 | 256 字符 | 256 字符 |
| IAM User 最多标签数 | 50 | 50 |
| S3 Bucket 标签值最大长度 | 256 字符 | 256 字符 |
| S3 Bucket 最多标签数 | 50 | 50 |
标签值长度限制分析:每个组名约 8-10 字符(如
group-01),加上分隔符/,256 字符的标签值可以容纳约 25 个组名。对于绝大多数场景足够使用。如果某个存储桶确实需要被超过 25 个组共享,可以使用多个标签键(如s3-rw-access-2)扩展,但这种极端情况很少见。
3、核心思路
传统 RBAC 方式下,每个 Policy 都要逐一列出存储桶 ARN,随着存储桶数量增长,Policy 长度迅速膨胀。ABAC 方式通过标签匹配实现动态授权:IAM User 和 S3 存储桶上分别打标签,IAM Policy 通过 aws:PrincipalTag 和 aws:ResourceTag 条件键进行匹配,一条 Policy 即可覆盖所有存储桶的授权,无需逐一列出 ARN。
本方案采用两套独立的标签键(s3-rw-access 和 s3-ro-access),分别控制读写权限和只读权限。这样同一个用户可以对存储桶 A 拥有读写权限,同时对存储桶 B 只有只读权限。
具体做法:
- IAM User 标签值:用户所属组名,如
group-01 - S3 Bucket 标签值:用分隔符
/拼接所有有权限的组名,如/group-01/group-02/group-03/ - IAM Policy 条件:使用
StringLike配合通配符*,匹配模式为*${aws:PrincipalTag/s3-rw-access}*
匹配原理:当 zhangwei 的 s3-rw-access 标签值为 group-01 时,Policy 中的条件变为检查存储桶标签值是否匹配 *group-01*。如果存储桶的 s3-rw-access 标签值为 /group-01/group-02/,则 *group-01* 可以匹配成功。
4、ABAC 方案的优势
| 对比项 | 现有 RBAC 方案 | ABAC 方案 |
|---|---|---|
| 每个 Group 需要的 Policy 数量 | 随存储桶增长,可能超过 10 条 | 3 条(固定) |
| 单条 Policy 大小 | 随存储桶增长,可能超过 6,144 字符 | 约 1,500-2,500 字符(固定) |
| 新增存储桶时 | 需修改 Policy,添加 ARN | 只需给新存储桶打标签 |
| 新增用户时 | 需修改 Policy 或创建新 Policy | 只需给新用户打标签 |
| 同一用户对不同桶不同权限 | 需要多条 Policy 分别授权 | 通过 rw/ro 两套标签自然支持 |
| 管理复杂度 | 高,Policy 数量和大小持续增长 | 低,Policy 固定不变 |
二、标签设计
1、关于 IAM Group 无法打标签需要在 IAM User 打标签的问题
IAM Group 不支持打标签。AWS IAM 支持对以下资源打标签:IAM User、IAM Role、IAM Policy、SAML Provider、OpenID Connect Provider、Server Certificate。IAM Group 不在支持列表中。
因此,本方案的标签全部设置在 IAM User 上,通过 aws:PrincipalTag 条件键在 Policy 中引用。
参考文档:Tag IAM users
控制台操作:
- 打开 IAM 控制台 → Users → 选择用户
- 选择 Tags 标签页 → Manage tags
- 添加 Key/Value 对
CLI 操作:
# 设置读写权限标签
aws iam tag-user --user-name user01 --tags Key=s3-rw-access,Value=group-01
# 设置只读权限标签(值同样是用户所属的组名)
aws iam tag-user --user-name user01 --tags Key=s3-ro-access,Value=group-01
2、IAM User 标签设计
IAM User 的标签值保持简单,只写用户所属的组名:
| 用户 | 所属 IAM Group | 标签名:s3-rw-access |
标签名:s3-ro-access |
|---|---|---|---|
| zhangwei(张伟) | group-01 | 标签值:group-01 |
标签值:group-01 |
| liuna(刘娜) | group-01 | 标签值:group-01 |
标签值:group-01` |
| chenyang(陈阳) | group-02 | 标签值:group-02 |
标签值:group-02 |
说明:IAM Group 不支持打标签,标签全部设置在 IAM User 上。用户的标签值始终等于用户所属的组名。
3、S3 Bucket 标签
S3 Bucket 的标签值使用 / 分隔符拼接所有有权限的组名。格式为 /<group-name-1>/<group-name-2>/.../<group-name-n>/。
以下是多对多场景的标签设计示例:
| 存储桶名称 | 标签名:s3-rw-access |
标签名:s3-ro-access |
权限效果 |
|---|---|---|---|
| bucket-01 | 标签值:/group-01/group-02/ |
— | group-01 和 group-02 均可读写 |
| bucket-02 | 标签值:/group-02/ |
标签值:/group-01/ |
group-02 读写,group-01 只读 |
| bucket-03 | 标签值:/group-01/ |
— | 仅 group-01 读写 |
4、无 Deny 规则设计
本方案全部使用 Allow 规则,不使用 Deny 规则。即使配置有误,最多是权限不足(无法访问),不会锁定管理员自己。这是一个重要的安全设计原则。
5、存储桶的标签值长度问题
确认存储桶的标签值长度,不超过 256 字符限制。
三、IAM Policy 设计
1、Policy 架构总览
本方案使用 3 条 Managed Policy,远低于 Group 的 10 条限制:
| Policy 名称 | 用途 | 挂载位置 |
|---|---|---|
S3-ABAC-ListBuckets |
列出存储桶(基础权限) | IAM Group(所有组) |
S3-ABAC-ReadOnly |
只读权限(基于 s3-ro-access 标签 StringLike 匹配) |
IAM Group(所有组) |
S3-ABAC-ReadWrite |
读写权限(基于 s3-rw-access 标签 StringLike 匹配) |
IAM Group(所有组) |
所有 Group 都挂载相同的 3 条 Policy。权限差异完全由 IAM User 和 S3 Bucket 上的标签值决定。
2、Policy 1:S3-ABAC-ListBuckets(基础权限)
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "AllowListAllBuckets",
"Effect": "Allow",
"Action": [
"s3:ListAllMyBuckets",
"s3:GetBucketLocation"
],
"Resource": "*"
},
{
"Sid": "AllowViewBucketTags",
"Effect": "Allow",
"Action": [
"s3:GetBucketTagging",
"s3:ListTagsForResource"
],
"Resource": "arn:aws:s3:::*"
}
]
}
3、Policy 2:S3-ABAC-ReadOnly(只读权限)
使用 StringLike 配合通配符 * 进行标签匹配,匹配模式为 *${aws:PrincipalTag/s3-ro-access}*。
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "AllowReadFromConsoleIfRoTagMatch",
"Effect": "Allow",
"Action": [
"s3:GetObject",
"s3:GetObjectVersion",
"s3:GetObjectTagging",
"s3:GetObjectVersionTagging",
"s3:ListBucket",
"s3:ListBucketVersions",
"s3:GetBucketVersioning"
],
"Resource": [
"arn:aws:s3:::*",
"arn:aws:s3:::*/*"
],
"Condition": {
"StringLike": {
"aws:ResourceTag/s3-ro-access": "*${aws:PrincipalTag/s3-ro-access}*"
},
"IpAddress": {
"aws:SourceIp": "0.0.0.0/0"
}
}
},
{
"Sid": "AllowReadFromVPCIfRoTagMatch",
"Effect": "Allow",
"Action": [
"s3:GetObject",
"s3:GetObjectVersion",
"s3:GetObjectTagging",
"s3:GetObjectVersionTagging",
"s3:ListBucket",
"s3:ListBucketVersions",
"s3:GetBucketVersioning"
],
"Resource": [
"arn:aws:s3:::*",
"arn:aws:s3:::*/*"
],
"Condition": {
"StringLike": {
"aws:ResourceTag/s3-ro-access": "*${aws:PrincipalTag/s3-ro-access}*"
},
"IpAddress": {
"aws:VpcSourceIp": "172.31.0.0/16"
}
}
}
]
}
4、Policy 3:S3-ABAC-ReadWrite(读写权限)
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "AllowReadWriteFromConsoleIfRwTagMatch",
"Effect": "Allow",
"Action": [
"s3:GetObject",
"s3:GetObjectVersion",
"s3:GetObjectTagging",
"s3:GetObjectVersionTagging",
"s3:PutObject",
"s3:PutObjectAcl",
"s3:PutObjectTagging",
"s3:PutObjectVersionTagging",
"s3:PutObjectVersionAcl",
"s3:DeleteObject",
"s3:DeleteObjectVersion",
"s3:DeleteObjectTagging",
"s3:DeleteObjectVersionTagging",
"s3:AbortMultipartUpload",
"s3:RestoreObject",
"s3:PutObjectRetention",
"s3:PutObjectLegalHold",
"s3:ListBucket",
"s3:ListBucketVersions",
"s3:ListBucketMultipartUploads",
"s3:ListMultipartUploadParts",
"s3:GetBucketVersioning"
],
"Resource": [
"arn:aws:s3:::*",
"arn:aws:s3:::*/*"
],
"Condition": {
"StringLike": {
"aws:ResourceTag/s3-rw-access": "*${aws:PrincipalTag/s3-rw-access}*"
},
"IpAddress": {
"aws:SourceIp": "0.0.0.0/0"
}
}
},
{
"Sid": "AllowReadWriteFromVPCIfRwTagMatch",
"Effect": "Allow",
"Action": [
"s3:GetObject",
"s3:GetObjectVersion",
"s3:GetObjectTagging",
"s3:GetObjectVersionTagging",
"s3:PutObject",
"s3:PutObjectAcl",
"s3:PutObjectTagging",
"s3:PutObjectVersionTagging",
"s3:PutObjectVersionAcl",
"s3:DeleteObject",
"s3:DeleteObjectVersion",
"s3:DeleteObjectTagging",
"s3:DeleteObjectVersionTagging",
"s3:AbortMultipartUpload",
"s3:RestoreObject",
"s3:PutObjectRetention",
"s3:PutObjectLegalHold",
"s3:ListBucket",
"s3:ListBucketVersions",
"s3:ListBucketMultipartUploads",
"s3:ListMultipartUploadParts",
"s3:GetBucketVersioning"
],
"Resource": [
"arn:aws:s3:::*",
"arn:aws:s3:::*/*"
],
"Condition": {
"StringLike": {
"aws:ResourceTag/s3-rw-access": "*${aws:PrincipalTag/s3-rw-access}*"
},
"IpAddress": {
"aws:VpcSourceIp": "172.31.0.0/16"
}
}
}
]
}
5、网络限制说明
网络限制嵌入到 ReadOnly 和 ReadWrite Policy 的 Condition 中:
- Statement 1:匹配 Console 访问(
aws:SourceIp,公网 IP) - Statement 2:匹配 VPC 内访问(
aws:VpcSourceIp,172.31.0.0/16)
两个条件键互斥:Console 访问时 aws:VpcSourceIp 不存在,VPC Endpoint 访问时 aws:SourceIp 不存在。
aws:SourceIp设置为0.0.0.0/0表示允许所有公网 IP(即 Console 访问)。如需限制 Console 来源 IP,替换为公司公网出口 IP 段。
四、S3 Bucket Policy
按照方案要求,S3 Bucket Policy 保留为空,所有权限控制通过 IAM Policy 实现。
五、实操演练:完整环境搭建与验证
本章演示多对多场景的完整搭建和验证过程。
1、场景描述
模拟以下多对多授权关系:
- zhangwei(张伟)和 liuna(刘娜)属于 group-01,chenyang(陈阳)属于 group-02
- a-mydev-bucket-01 需要同时被 group-01 和 group-02 读写访问(多对多核心场景)
- a-mydev-bucket-02 仅被 group-02 读写,group-01 只读
- a-mydev-bucket-03 仅被 group-01 读写
IAM User 标签:
| 用户 | 所属 IAM Group | s3-rw-access |
s3-ro-access |
|---|---|---|---|
| zhangwei(张伟) | group-01 | group-01 |
group-01 |
| liuna(刘娜) | group-01 | group-01 |
group-01 |
| chenyang(陈阳) | group-02 | group-02 |
group-02 |
S3 Bucket 标签:
| 存储桶名称 | s3-rw-access |
s3-ro-access |
权限效果 |
|---|---|---|---|
| a-mydev-bucket-01 | /group-01/group-02/ |
— | group-01 和 group-02 均可读写 |
| a-mydev-bucket-02 | /group-02/ |
/group-01/ |
group-02 读写,group-01 只读 |
| a-mydev-bucket-03 | /group-01/ |
— | 仅 group-01 读写 |
2、设置变量
export AWS_ACCOUNT_ID="<your-account-id>"
export AWS_REGION="us-west-2"
3、步骤一:创建 S3 存储桶并启用 ABAC
for BUCKET in a-mydev-bucket-01 a-mydev-bucket-02 a-mydev-bucket-03; do
aws s3api create-bucket --bucket ${BUCKET} --region ${AWS_REGION} \
--create-bucket-configuration LocationConstraint=${AWS_REGION}
echo "Created bucket: ${BUCKET}"
done
# 启用 ABAC
for BUCKET in a-mydev-bucket-01 a-mydev-bucket-02 a-mydev-bucket-03; do
aws s3api put-bucket-abac --bucket ${BUCKET} \
--abac-status Status=Enabled --region ${AWS_REGION}
echo "Enabled ABAC on: ${BUCKET}"
done
4、步骤二:给存储桶打标签
# a-mydev-bucket-01:group-01 和 group-02 均可读写(多对多核心场景)
aws s3control tag-resource \
--account-id ${AWS_ACCOUNT_ID} \
--resource-arn arn:aws:s3:::a-mydev-bucket-01 \
--tags Key=s3-rw-access,Value=/group-01/group-02/
# a-mydev-bucket-02:group-02 读写,group-01 只读
aws s3control tag-resource \
--account-id ${AWS_ACCOUNT_ID} \
--resource-arn arn:aws:s3:::a-mydev-bucket-02 \
--tags Key=s3-rw-access,Value=/group-02/ Key=s3-ro-access,Value=/group-01/
# a-mydev-bucket-03:仅 group-01 读写
aws s3control tag-resource \
--account-id ${AWS_ACCOUNT_ID} \
--resource-arn arn:aws:s3:::a-mydev-bucket-03 \
--tags Key=s3-rw-access,Value=/group-01/
# 验证标签
for BUCKET in a-mydev-bucket-01 a-mydev-bucket-02 a-mydev-bucket-03; do
echo "=== ${BUCKET} ==="
aws s3control list-tags-for-resource \
--account-id ${AWS_ACCOUNT_ID} \
--resource-arn arn:aws:s3:::${BUCKET}
done
预期输出:
=== a-mydev-bucket-01 ===
{
"Tags": [
{
"Key": "s3-rw-access",
"Value": "/group-01/group-02/"
}
]
}
=== a-mydev-bucket-02 ===
{
"Tags": [
{
"Key": "s3-rw-access",
"Value": "/group-02/"
},
{
"Key": "s3-ro-access",
"Value": "/group-01/"
}
]
}
=== a-mydev-bucket-03 ===
{
"Tags": [
{
"Key": "s3-rw-access",
"Value": "/group-01/"
}
]
}
5、步骤三:创建 IAM Policy
cat > /tmp/s3-abac-list.json << 'EOF'
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "AllowListAllBuckets",
"Effect": "Allow",
"Action": ["s3:ListAllMyBuckets", "s3:GetBucketLocation"],
"Resource": "*"
},
{
"Sid": "AllowViewBucketTags",
"Effect": "Allow",
"Action": ["s3:GetBucketTagging", "s3:ListTagsForResource"],
"Resource": "arn:aws:s3:::*"
}
]
}
EOF
cat > /tmp/s3-abac-ro.json << 'ROEOF'
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "AllowReadFromConsoleIfRoTagMatch",
"Effect": "Allow",
"Action": [
"s3:GetObject", "s3:GetObjectVersion",
"s3:GetObjectTagging", "s3:GetObjectVersionTagging",
"s3:ListBucket", "s3:ListBucketVersions",
"s3:GetBucketVersioning"
],
"Resource": ["arn:aws:s3:::*", "arn:aws:s3:::*/*"],
"Condition": {
"StringLike": {
"aws:ResourceTag/s3-ro-access": "*${aws:PrincipalTag/s3-ro-access}*"
},
"IpAddress": {"aws:SourceIp": "0.0.0.0/0"}
}
},
{
"Sid": "AllowReadFromVPCIfRoTagMatch",
"Effect": "Allow",
"Action": [
"s3:GetObject", "s3:GetObjectVersion",
"s3:GetObjectTagging", "s3:GetObjectVersionTagging",
"s3:ListBucket", "s3:ListBucketVersions",
"s3:GetBucketVersioning"
],
"Resource": ["arn:aws:s3:::*", "arn:aws:s3:::*/*"],
"Condition": {
"StringLike": {
"aws:ResourceTag/s3-ro-access": "*${aws:PrincipalTag/s3-ro-access}*"
},
"IpAddress": {"aws:VpcSourceIp": "172.31.0.0/16"}
}
}
]
}
ROEOF
cat > /tmp/s3-abac-rw.json << 'RWEOF'
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "AllowReadWriteFromConsoleIfRwTagMatch",
"Effect": "Allow",
"Action": [
"s3:GetObject", "s3:GetObjectVersion",
"s3:GetObjectTagging", "s3:GetObjectVersionTagging",
"s3:PutObject", "s3:PutObjectAcl",
"s3:PutObjectTagging", "s3:PutObjectVersionTagging",
"s3:PutObjectVersionAcl",
"s3:DeleteObject", "s3:DeleteObjectVersion",
"s3:DeleteObjectTagging", "s3:DeleteObjectVersionTagging",
"s3:AbortMultipartUpload", "s3:RestoreObject",
"s3:PutObjectRetention", "s3:PutObjectLegalHold",
"s3:ListBucket", "s3:ListBucketVersions",
"s3:ListBucketMultipartUploads", "s3:ListMultipartUploadParts",
"s3:GetBucketVersioning"
],
"Resource": ["arn:aws:s3:::*", "arn:aws:s3:::*/*"],
"Condition": {
"StringLike": {
"aws:ResourceTag/s3-rw-access": "*${aws:PrincipalTag/s3-rw-access}*"
},
"IpAddress": {"aws:SourceIp": "0.0.0.0/0"}
}
},
{
"Sid": "AllowReadWriteFromVPCIfRwTagMatch",
"Effect": "Allow",
"Action": [
"s3:GetObject", "s3:GetObjectVersion",
"s3:GetObjectTagging", "s3:GetObjectVersionTagging",
"s3:PutObject", "s3:PutObjectAcl",
"s3:PutObjectTagging", "s3:PutObjectVersionTagging",
"s3:PutObjectVersionAcl",
"s3:DeleteObject", "s3:DeleteObjectVersion",
"s3:DeleteObjectTagging", "s3:DeleteObjectVersionTagging",
"s3:AbortMultipartUpload", "s3:RestoreObject",
"s3:PutObjectRetention", "s3:PutObjectLegalHold",
"s3:ListBucket", "s3:ListBucketVersions",
"s3:ListBucketMultipartUploads", "s3:ListMultipartUploadParts",
"s3:GetBucketVersioning"
],
"Resource": ["arn:aws:s3:::*", "arn:aws:s3:::*/*"],
"Condition": {
"StringLike": {
"aws:ResourceTag/s3-rw-access": "*${aws:PrincipalTag/s3-rw-access}*"
},
"IpAddress": {"aws:VpcSourceIp": "172.31.0.0/16"}
}
}
]
}
RWEOF
# 创建 Policy
aws iam create-policy --policy-name S3-ABAC-ListBuckets \
--policy-document file:///tmp/s3-abac-list.json
aws iam create-policy --policy-name S3-ABAC-ReadOnly \
--policy-document file:///tmp/s3-abac-ro.json
aws iam create-policy --policy-name S3-ABAC-ReadWrite \
--policy-document file:///tmp/s3-abac-rw.json
6、步骤四:创建 IAM Group 并挂载 Policy
aws iam create-group --group-name group-01
aws iam create-group --group-name group-02
for GROUP in group-01 group-02; do
aws iam attach-group-policy --group-name ${GROUP} \
--policy-arn arn:aws:iam::${AWS_ACCOUNT_ID}:policy/S3-ABAC-ListBuckets
aws iam attach-group-policy --group-name ${GROUP} \
--policy-arn arn:aws:iam::${AWS_ACCOUNT_ID}:policy/S3-ABAC-ReadOnly
aws iam attach-group-policy --group-name ${GROUP} \
--policy-arn arn:aws:iam::${AWS_ACCOUNT_ID}:policy/S3-ABAC-ReadWrite
echo "Attached policies to: ${GROUP}"
done
7、步骤五:创建 IAM User 并打标签
# zhangwei(张伟):加入 group-01
aws iam create-user --user-name zhangwei
aws iam add-user-to-group --user-name zhangwei --group-name group-01
aws iam tag-user --user-name zhangwei --tags \
Key=s3-rw-access,Value=group-01 Key=s3-ro-access,Value=group-01
aws iam create-access-key --user-name zhangwei > /tmp/zhangwei-keys.json
echo "zhangwei AccessKey:"
cat /tmp/zhangwei-keys.json | python3 -c "import sys,json; d=json.load(sys.stdin)['AccessKey']; print(f\"AccessKeyId: {d['AccessKeyId']}\nSecretAccessKey: {d['SecretAccessKey']}\")"
# liuna(刘娜):加入 group-01
aws iam create-user --user-name liuna
aws iam add-user-to-group --user-name liuna --group-name group-01
aws iam tag-user --user-name liuna --tags \
Key=s3-rw-access,Value=group-01 Key=s3-ro-access,Value=group-01
aws iam create-access-key --user-name liuna > /tmp/liuna-keys.json
echo "liuna AccessKey:"
cat /tmp/liuna-keys.json | python3 -c "import sys,json; d=json.load(sys.stdin)['AccessKey']; print(f\"AccessKeyId: {d['AccessKeyId']}\nSecretAccessKey: {d['SecretAccessKey']}\")"
# chenyang(陈阳):加入 group-02
aws iam create-user --user-name chenyang
aws iam add-user-to-group --user-name chenyang --group-name group-02
aws iam tag-user --user-name chenyang --tags \
Key=s3-rw-access,Value=group-02 Key=s3-ro-access,Value=group-02
aws iam create-access-key --user-name chenyang > /tmp/chenyang-keys.json
echo "chenyang AccessKey:"
cat /tmp/chenyang-keys.json | python3 -c "import sys,json; d=json.load(sys.stdin)['AccessKey']; print(f\"AccessKeyId: {d['AccessKeyId']}\nSecretAccessKey: {d['SecretAccessKey']}\")"
8、步骤六:上传测试文件并配置 Profile
# 上传测试文件(管理员权限)
for BUCKET in a-mydev-bucket-01 a-mydev-bucket-02 a-mydev-bucket-03; do
echo "hello from ${BUCKET}" | aws s3 cp - s3://${BUCKET}/test.txt
echo "Uploaded test.txt to ${BUCKET}"
done
# 配置测试用户 Profile(替换为步骤五输出的 AccessKey)
aws configure set aws_access_key_id <zhangwei-access-key-id> --profile zhangwei
aws configure set aws_secret_access_key <zhangwei-secret-access-key> --profile zhangwei
aws configure set region ${AWS_REGION} --profile zhangwei
aws configure set aws_access_key_id <liuna-access-key-id> --profile liuna
aws configure set aws_secret_access_key <liuna-secret-access-key> --profile liuna
aws configure set region ${AWS_REGION} --profile liuna
aws configure set aws_access_key_id <chenyang-access-key-id> --profile chenyang
aws configure set aws_secret_access_key <chenyang-secret-access-key> --profile chenyang
aws configure set region ${AWS_REGION} --profile chenyang
9、步骤七:验证权限
echo "============================================"
echo " a-mydev-bucket-01 (rw: group-01, group-02)"
echo "============================================"
echo "--- 测试1: zhangwei 读 a-mydev-bucket-01 (预期: 成功, rw标签含group-01) ---"
aws s3 cp s3://a-mydev-bucket-01/test.txt - --profile zhangwei
echo "--- 测试2: zhangwei 写 a-mydev-bucket-01 (预期: 成功, rw标签含group-01) ---"
echo "write by zhangwei" | aws s3 cp - s3://a-mydev-bucket-01/zhangwei-test.txt --profile zhangwei \
&& echo "写入成功" || echo "写入失败"
echo "--- 测试3: liuna 读 a-mydev-bucket-01 (预期: 成功, rw标签含group-01) ---"
aws s3 cp s3://a-mydev-bucket-01/test.txt - --profile liuna
echo "--- 测试4: liuna 写 a-mydev-bucket-01 (预期: 成功, rw标签含group-01) ---"
echo "write by liuna" | aws s3 cp - s3://a-mydev-bucket-01/liuna-test.txt --profile liuna \
&& echo "写入成功" || echo "写入失败"
echo "--- 测试5: chenyang 读 a-mydev-bucket-01 (预期: 成功, rw标签含group-02) ---"
aws s3 cp s3://a-mydev-bucket-01/test.txt - --profile chenyang
echo "--- 测试6: chenyang 写 a-mydev-bucket-01 (预期: 成功, rw标签含group-02) ---"
echo "write by chenyang" | aws s3 cp - s3://a-mydev-bucket-01/chenyang-test.txt --profile chenyang \
&& echo "写入成功" || echo "写入失败"
echo ""
echo "============================================"
echo " a-mydev-bucket-02 (rw: group-02, ro: group-01)"
echo "============================================"
echo "--- 测试7: zhangwei 读 a-mydev-bucket-02 (预期: 成功, ro标签含group-01) ---"
aws s3 cp s3://a-mydev-bucket-02/test.txt - --profile zhangwei
echo "--- 测试8: zhangwei 写 a-mydev-bucket-02 (预期: 失败, 仅ro匹配) ---"
echo "write by zhangwei" | aws s3 cp - s3://a-mydev-bucket-02/zhangwei-test.txt --profile zhangwei \
&& echo "写入成功" || echo "写入失败(符合预期)"
echo "--- 测试9: liuna 读 a-mydev-bucket-02 (预期: 成功, ro标签含group-01) ---"
aws s3 cp s3://a-mydev-bucket-02/test.txt - --profile liuna
echo "--- 测试10: liuna 写 a-mydev-bucket-02 (预期: 失败, 仅ro匹配) ---"
echo "write by liuna" | aws s3 cp - s3://a-mydev-bucket-02/liuna-test.txt --profile liuna \
&& echo "写入成功" || echo "写入失败(符合预期)"
echo "--- 测试11: chenyang 读 a-mydev-bucket-02 (预期: 成功, rw标签含group-02) ---"
aws s3 cp s3://a-mydev-bucket-02/test.txt - --profile chenyang
echo "--- 测试12: chenyang 写 a-mydev-bucket-02 (预期: 成功, rw标签含group-02) ---"
echo "write by chenyang" | aws s3 cp - s3://a-mydev-bucket-02/chenyang-test.txt --profile chenyang \
&& echo "写入成功" || echo "写入失败"
echo ""
echo "============================================"
echo " a-mydev-bucket-03 (rw: group-01)"
echo "============================================"
echo "--- 测试13: zhangwei 读 a-mydev-bucket-03 (预期: 成功, rw标签含group-01) ---"
aws s3 cp s3://a-mydev-bucket-03/test.txt - --profile zhangwei
echo "--- 测试14: zhangwei 写 a-mydev-bucket-03 (预期: 成功, rw标签含group-01) ---"
echo "write by zhangwei" | aws s3 cp - s3://a-mydev-bucket-03/zhangwei-test.txt --profile zhangwei \
&& echo "写入成功" || echo "写入失败"
echo "--- 测试15: chenyang 读 a-mydev-bucket-03 (预期: 失败, 无标签匹配) ---"
aws s3 cp s3://a-mydev-bucket-03/test.txt - --profile chenyang \
&& echo "读取成功" || echo "读取失败(符合预期)"
echo "--- 测试16: chenyang 写 a-mydev-bucket-03 (预期: 失败, 无标签匹配) ---"
echo "write by chenyang" | aws s3 cp - s3://a-mydev-bucket-03/chenyang-test.txt --profile chenyang \
&& echo "写入成功" || echo "写入失败(符合预期)"
10、预期测试结果
a-mydev-bucket-01(rw: group-01, group-02):
| 测试 | 操作 | 预期结果 | 匹配原理 |
|---|---|---|---|
| 1 | zhangwei 读 a-mydev-bucket-01 | ✅ 成功 | rw 标签 /group-01/group-02/ 匹配 *group-01* |
| 2 | zhangwei 写 a-mydev-bucket-01 | ✅ 成功 | 同上 |
| 3 | liuna 读 a-mydev-bucket-01 | ✅ 成功 | rw 标签 /group-01/group-02/ 匹配 *group-01* |
| 4 | liuna 写 a-mydev-bucket-01 | ✅ 成功 | 同上 |
| 5 | chenyang 读 a-mydev-bucket-01 | ✅ 成功 | rw 标签 /group-01/group-02/ 匹配 *group-02* |
| 6 | chenyang 写 a-mydev-bucket-01 | ✅ 成功 | 同上 |
a-mydev-bucket-02(rw: group-02, ro: group-01):
| 测试 | 操作 | 预期结果 | 匹配原理 |
|---|---|---|---|
| 7 | zhangwei 读 a-mydev-bucket-02 | ✅ 成功 | ro 标签 /group-01/ 匹配 *group-01* |
| 8 | zhangwei 写 a-mydev-bucket-02 | ❌ 拒绝 | rw 标签 /group-02/ 不匹配 *group-01* |
| 9 | liuna 读 a-mydev-bucket-02 | ✅ 成功 | ro 标签 /group-01/ 匹配 *group-01* |
| 10 | liuna 写 a-mydev-bucket-02 | ❌ 拒绝 | rw 标签 /group-02/ 不匹配 *group-01* |
| 11 | chenyang 读 a-mydev-bucket-02 | ✅ 成功 | rw 标签 /group-02/ 匹配 *group-02* |
| 12 | chenyang 写 a-mydev-bucket-02 | ✅ 成功 | 同上 |
a-mydev-bucket-03(rw: group-01):
| 测试 | 操作 | 预期结果 | 匹配原理 |
|---|---|---|---|
| 13 | zhangwei 读 a-mydev-bucket-03 | ✅ 成功 | rw 标签 /group-01/ 匹配 *group-01* |
| 14 | zhangwei 写 a-mydev-bucket-03 | ✅ 成功 | 同上 |
| 15 | chenyang 读 a-mydev-bucket-03 | ❌ 拒绝 | rw 标签 /group-01/ 不匹配 *group-02*,无 ro 标签 |
| 16 | chenyang 写 a-mydev-bucket-03 | ❌ 拒绝 | 同上 |
测试 1-6 是多对多核心验证:a-mydev-bucket-01 的 s3-rw-access=/group-01/group-02/ 同时匹配 zhangwei/liuna 的 *group-01* 和 chenyang 的 *group-02*,两个不同组的用户都能读写同一个桶。
11、清理测试资源
# 删除 Access Key
ZHANGWEI_KEY=$(cat /tmp/zhangwei-keys.json | python3 -c "import sys,json; print(json.load(sys.stdin)['AccessKey']['AccessKeyId'])")
LIUNA_KEY=$(cat /tmp/liuna-keys.json | python3 -c "import sys,json; print(json.load(sys.stdin)['AccessKey']['AccessKeyId'])")
CHENYANG_KEY=$(cat /tmp/chenyang-keys.json | python3 -c "import sys,json; print(json.load(sys.stdin)['AccessKey']['AccessKeyId'])")
aws iam delete-access-key --user-name zhangwei --access-key-id ${ZHANGWEI_KEY}
aws iam delete-access-key --user-name liuna --access-key-id ${LIUNA_KEY}
aws iam delete-access-key --user-name chenyang --access-key-id ${CHENYANG_KEY}
# 从 Group 移除用户并删除用户
aws iam remove-user-from-group --user-name zhangwei --group-name group-01
aws iam remove-user-from-group --user-name liuna --group-name group-01
aws iam remove-user-from-group --user-name chenyang --group-name group-02
aws iam delete-user --user-name zhangwei
aws iam delete-user --user-name liuna
aws iam delete-user --user-name chenyang
# 解除 Group 的 Policy 挂载并删除 Group
for GROUP in group-01 group-02; do
aws iam detach-group-policy --group-name ${GROUP} \
--policy-arn arn:aws:iam::${AWS_ACCOUNT_ID}:policy/S3-ABAC-ListBuckets
aws iam detach-group-policy --group-name ${GROUP} \
--policy-arn arn:aws:iam::${AWS_ACCOUNT_ID}:policy/S3-ABAC-ReadOnly
aws iam detach-group-policy --group-name ${GROUP} \
--policy-arn arn:aws:iam::${AWS_ACCOUNT_ID}:policy/S3-ABAC-ReadWrite
done
aws iam delete-group --group-name group-01
aws iam delete-group --group-name group-02
# 删除 Policy
aws iam delete-policy --policy-arn arn:aws:iam::${AWS_ACCOUNT_ID}:policy/S3-ABAC-ListBuckets
aws iam delete-policy --policy-arn arn:aws:iam::${AWS_ACCOUNT_ID}:policy/S3-ABAC-ReadOnly
aws iam delete-policy --policy-arn arn:aws:iam::${AWS_ACCOUNT_ID}:policy/S3-ABAC-ReadWrite
# 清空并删除存储桶
for BUCKET in a-mydev-bucket-01 a-mydev-bucket-02 a-mydev-bucket-03; do
aws s3 rm s3://${BUCKET} --recursive
aws s3api delete-bucket --bucket ${BUCKET} --region ${AWS_REGION}
echo "Deleted bucket: ${BUCKET}"
done
# 清理临时文件和 profile
rm -f /tmp/zhangwei-keys.json /tmp/liuna-keys.json /tmp/chenyang-keys.json
for PROFILE in zhangwei liuna chenyang; do
aws configure set aws_access_key_id "" --profile ${PROFILE}
aws configure set aws_secret_access_key "" --profile ${PROFILE}
done
echo "清理完成"
六、日常管理中的标签更新
1、新增一个组对某个桶的访问权限
场景:a-mydev-bucket-02 当前只允许 group-02 读写,现在需要新增 group-01 的读写权限。
操作:修改存储桶的 s3-rw-access 标签值,追加新组名。
# 当前标签值:/group-02/
# 修改后标签值:/group-02/group-01/
aws s3control tag-resource \
--account-id ${AWS_ACCOUNT_ID} \
--resource-arn arn:aws:s3:::a-mydev-bucket-02 \
--tags Key=s3-rw-access,Value=/group-02/group-01/
无需修改任何 IAM Policy,无需修改任何用户标签。
2、移除一个组对某个桶的访问权限
场景:a-mydev-bucket-01 当前允许 group-01 和 group-02 读写,现在需要移除 group-02 的读写权限。
操作:修改存储桶的 s3-rw-access 标签值,删除对应组名。
# 当前标签值:/group-01/group-02/
# 修改后标签值:/group-01/
aws s3control tag-resource \
--account-id ${AWS_ACCOUNT_ID} \
--resource-arn arn:aws:s3:::a-mydev-bucket-01 \
--tags Key=s3-rw-access,Value=/group-01/
3、新增用户
场景:新用户 wangli(王丽)加入 group-02。
操作:创建用户、加入 Group、打标签。IAM Policy 和存储桶标签无需修改。
aws iam create-user --user-name wangli
aws iam add-user-to-group --user-name wangli --group-name group-02
aws iam tag-user --user-name wangli --tags \
Key=s3-rw-access,Value=group-02 Key=s3-ro-access,Value=group-02
七、小结
1、适用场景
- 100+ IAM User × 100+ S3 存储桶的大规模权限管理
- 多对多授权关系(一个桶被多个组共享)
- 主要通过 AWS Console 和 AKSK 访问 S3
- 需要精确控制读写/只读权限
- 希望 IAM Policy 固定不变,通过标签管理权限
2、方案限制
| 限制项 | 说明 | 应对方案 |
|---|---|---|
| 标签值 256 字符限制 | 单个标签值最多容纳约 25 个组名 | 使用多个标签键扩展(如 s3-rw-access-2),需增加对应的 Policy Statement |
| 子串误匹配风险 | 组名存在前缀包含关系时可能误匹配 | 使用固定长度编号、即固定数字长度不要随意修改标签命名规范,或在用户标签值中也包含分隔符 |
| 不支持S3存储桶内Prefix前缀级别权限 | ABAC 标签只能控制到桶级别 | 如需前缀级别控制,考虑增加 S3 Access Grants 或 Access Points |
| 非Admin用户要修改存储桶标签时、自身需要具备 ABAC 权限 | 启用 ABAC 后需使用 TagResource API |
确保管理员有 s3:TagResource 权限 |
3、与其他方案的选择建议
| 场景 | 推荐方案 |
|---|---|
| 用户和存储桶简单一对一,Console + AKSK 访问 | ABAC StringEquals 精确匹配 |
| 用户和存储桶多对多权限,Console + AKSK 访问 | ABAC StringLike 通配符匹配(本方案) |
| 极大规模多对多(单桶超过 25 个组共享),且访问方式不依赖 Console | S3 Access Grants |
| 需要S3存储桶内Prefix前缀级别精细控制 | S3 Access Points 或 S3 Access Grants |
八、参考文档
- Enabling ABAC in general purpose buckets
- Using tags with S3 general purpose buckets
- Tag IAM users
- IAM JSON policy elements: Condition operators - StringLike
- Conditions with multiple context keys or values
- IAM policy elements: Variables and tags
- IAM and STS quotas
- Introducing ABAC for Amazon S3 (Blog)
- Access control in Amazon S3
- Managing access to shared datasets with access points
最后修改于 2026-04-07