按产品分类浏览文章 关于本站

在多用户和多存储桶的多对多分配权限时使用 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:PrincipalTagaws:ResourceTag 条件键进行匹配,一条 Policy 即可覆盖所有存储桶的授权,无需逐一列出 ARN。

本方案采用两套独立的标签键(s3-rw-accesss3-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

控制台操作:

  1. 打开 IAM 控制台 → Users → 选择用户
  2. 选择 Tags 标签页 → Manage tags
  3. 添加 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

八、参考文档


最后修改于 2026-04-07