通过 S3 存储桶策略限制 S3 外部请求访问 + 仅允许 VPC 内网访问
本文描述了如何通过 Amazon S3 存储桶策略,实现控制台用户与 API 用户的差异化访问控制:允许指定 IAM 角色通过 AWS Management Console 操作存储桶,仅允许指定 IAM 用户经 S3 Interface Endpoint 访问,并拒绝其他身份和网络来源。同时介绍如何使用 AWS CLI 和 Boto3 生成基于内网端点的 Presign URL,并说明签名、网络连通性、权限策略及费用方面的注意事项。
一、背景
使用 S3 存储桶策略满足以下安全需求:
- 允许 A 用户(登录 AWS 控制台的管理员)通过 AWS Console 浏览、上传和下载对象
- 允许 B 用户(登录 AWS 控制台的普通用户)通过 AWS Console 浏览、上传和下载对象
- 针对 C 用户使用 Access Key/Secret Key 发起的 API 访问,仅允许来自指定 VPC Endpoint 的请求
- 其他访问者、其他访问途径拒绝访问
二、S3 Bucket Policy 设计
1、整理变量用于拼接规则
账号信息:
- AWS Account ID:
123456789012 - 区域:
ap-northeast-1 - 被访问的存储桶名称:
lxy-s3-bucket-permission-test-01
网络信息:
- S3 Interface Endpoint ID:
vpce-0c3c6abdaac363a51
用户信息:
- A 用户(登录 AWS 控制台的管理员)对应的 IAM Role ARN(可以是联邦认证 Assumed Role):
arn:aws:iam::123456789012:role/OrganizationAccountAccessRole - B 用户(登录 AWS 控制台的普通用户)对应的 IAM Role ARN:
arn:aws:iam::123456789012:role/s3-permission-test - C 用户(通过 API 和 Access Key/Secret Key 访问 S3 的用户)对应的 IAM User ARN:
arn:aws:iam::123456789012:user/s3-api-user
2、Policy 编写
A 用户和 B 用户通过 AWS Management Console 发起的 S3 请求不经过指定 VPC Endpoint,因此不能使用一个面向所有 Principal 的 aws:SourceVpce 显式拒绝语句。本文将身份限制和网络来源限制拆分为两个独立的拒绝语句。
访问决策如下:
| 请求身份 | 请求来源 | 决策 | 说明 |
|---|---|---|---|
A 用户(OrganizationAccountAccessRole 角色) |
AWS Console | 允许 | 对目标 Bucket 和其中对象执行 s3:* |
B 用户(s3-permission-test 角色) |
AWS Console | 允许 | 对目标 Bucket 和其中对象执行 s3:* |
C 用户(s3-api-user IAM 用户) |
vpce-0c3c6abdaac363a51 |
允许 | 仅从指定 Endpoint 对目标 Bucket 和其中对象执行 s3:* |
C 用户(s3-api-user IAM 用户) |
公网或其他 VPC Endpoint | 显式拒绝 | 不是指定 aws:SourceVpce 则拒绝 |
| 其他 Principal | 任意来源 | 显式拒绝 | 不允许通过 IAM Policy 绕过 Bucket Policy |
存储桶策略如下。每个身份的 Bucket ARN 和 Object ARN 合并在同一个 Resource 数组中,并统一使用 s3:*:
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "AllowFullAccessRolesAllS3Access",
"Effect": "Allow",
"Principal": {
"AWS": [
"arn:aws:iam::123456789012:role/OrganizationAccountAccessRole",
"arn:aws:iam::123456789012:role/s3-permission-test"
]
},
"Action": "s3:*",
"Resource": [
"arn:aws:s3:::lxy-s3-bucket-permission-test-01",
"arn:aws:s3:::lxy-s3-bucket-permission-test-01/*"
]
},
{
"Sid": "AllowApiUserAllS3AccessFromVpce",
"Effect": "Allow",
"Principal": {
"AWS": "arn:aws:iam::123456789012:user/s3-api-user"
},
"Action": "s3:*",
"Resource": [
"arn:aws:s3:::lxy-s3-bucket-permission-test-01",
"arn:aws:s3:::lxy-s3-bucket-permission-test-01/*"
],
"Condition": {
"StringEquals": {
"aws:SourceVpce": "vpce-0c3c6abdaac363a51"
}
}
},
{
"Sid": "DenyApiUserOutsideDesignatedVpce",
"Effect": "Deny",
"Principal": {
"AWS": "arn:aws:iam::123456789012:user/s3-api-user"
},
"Action": "s3:*",
"Resource": [
"arn:aws:s3:::lxy-s3-bucket-permission-test-01",
"arn:aws:s3:::lxy-s3-bucket-permission-test-01/*"
],
"Condition": {
"StringNotEquals": {
"aws:SourceVpce": "vpce-0c3c6abdaac363a51"
}
}
},
{
"Sid": "DenyAllOtherPrincipals",
"Effect": "Deny",
"Principal": "*",
"Action": "s3:*",
"Resource": [
"arn:aws:s3:::lxy-s3-bucket-permission-test-01",
"arn:aws:s3:::lxy-s3-bucket-permission-test-01/*"
],
"Condition": {
"ArnNotEquals": {
"aws:PrincipalArn": [
"arn:aws:iam::123456789012:role/OrganizationAccountAccessRole",
"arn:aws:iam::123456789012:role/s3-permission-test",
"arn:aws:iam::123456789012:user/s3-api-user"
]
}
}
}
]
}
三、测试流程
1、主要测试流程
| 测试路径 | 测试操作 | 预期结果 |
|---|---|---|
A 用户(OrganizationAccountAccessRole 角色)访问 AWS 控制台 |
浏览 Bucket、上传测试对象、下载测试对象、读取 Bucket Policy | 成功 |
B 用户(s3-permission-test 角色)访问 AWS 控制台 |
浏览 Bucket、上传测试对象、下载测试对象、读取 Bucket Policy | 成功 |
| C 用户通过指定 Interface Endpoint | ListObjectsV2、PutObject、GetObject |
成功 |
| C 用户通过公网 S3 Endpoint | ListObjectsV2、PutObject、GetObject |
AccessDenied |
| 其他 IAM Principal | 通过公网和指定 Endpoint 分别访问 | AccessDenied |
2、管理员用户 A 以 Federated Login 方式登录(Assume 为角色)
切换后登陆角色后,可以看到访问成功。
结果符合当前策略配置的预期。
3、普通用户 B 以 Federated Login 方式登录(Assume 为角色)
切换后登陆角色后,可以看到访问成功。
结果符合当前策略配置的预期。
4、应用程序 C 配置 Access Key/Secret Key 后从代码访问(AK/SK 隶属于该 IAM 用户)
(1) 从公网调用测试
aws s3 ls lxy-s3-bucket-permission-test-01
收到如下明确的拒绝错误:
aws: [ERROR]: An error occurred (AccessDenied) when calling the ListObjectsV2 operation: User: arn:aws:iam::123456789012:user/s3-api-user is not authorized to perform: s3:ListBucket on resource: "arn:aws:s3:::lxy-s3-bucket-permission-test-01" with an explicit deny in a resource-based policy
结果符合当前策略配置的预期。
(2) 从内网调用,包括跨云内网调用(S3 Interface Endpoint 会解析为内网 IP)
从内网通过 S3 Interface Endpoint 调用:
aws s3 ls lxy-s3-bucket-permission-test-01 \
--endpoint-url https://bucket.vpce-0c3c6abdaac363a51-fx3uwgx1.s3.ap-northeast-1.vpce.amazonaws.com
正常获取存储桶内容:
2026-08-11 04:26:45 542065 153713sf34m34ra3ar4jf3.jpg
结果符合当前策略配置的预期。
四、使用 VPC Endpoint 访问 S3 存储桶并执行 Presign 签名提供访问权限
1、为 S3 内网 Interface Endpoint 生成 Presign URL 的背景
在典型的文件管理/运维场景中,通常使用 AWS CLI 的命令操作 S3,例如本机向存储桶复制文件的命令是 aws s3 cp filename s3://bucket-name/prefix-dir/filename,这种操作十分便利。当需要向没有 Access Key/Secret Key 的应用提供临时访问权限时,可以使用 Presign URL 签名功能,命令是 aws s3 presign s3://bucket-name/prefix-dir/filename,即可获得包含下载 URL 的签名地址。
在以上场景中,S3 Presign URL 可以签署包含临时访问 Token 的 URL,用于向访问者提供下载或上传文件的临时授权。默认情况下,使用 AWS CLI 生成的 Presign URL 包含文件访问路径,其访问地址是 AWS S3 服务的公网 Endpoint。而签名与 URL 地址中的主机名相关,如果仅替换其中的域名为 S3 内网 Interface Endpoint 访问端点,则访问会失败,报错为 SSL 证书错误。此场景下,生成签名时必须将目标 S3 内网 Interface Endpoint 作为签名参数,才可生成内网访问签名。
下面介绍具体配置和需要传入的参数。
2、AWS CLI 为内网 S3 Interface Endpoint 的访问请求生成 Presign 签名
AWS CLI 的 aws s3 presign 命令支持通过 --endpoint-url 指定 S3 Interface Endpoint。SigV4(AWS Signature Version 4)会将最终请求的 Host、对象路径和查询参数纳入签名计算,因此必须在生成 Presign URL 时直接传入内网端点,不能在签名完成后替换 URL 中的域名。
从 VPC Endpoint 控制台复制 Bucket API 对应的完整 DNS 名称。控制台显示的端点 DNS 通常包含通配符,例如:
*.vpce-0123456789abcdef0-abc123.s3.ap-northeast-1.vpce.amazonaws.com
访问 Bucket API 时,应将通配符替换为固定前缀 bucket,得到如下 Endpoint URL:
https://bucket.vpce-0123456789abcdef0-abc123.s3.ap-northeast-1.vpce.amazonaws.com
本文使用 Path-style 寻址方式,使 Bucket 名称位于 URL 路径中,并与下一章节的 Python 示例保持一致。首先在已经配置好凭证的 AWS CLI Profile 中设置寻址方式:
aws configure set s3.addressing_style path \
--profile <your-profile-name>
aws configure get s3.addressing_style \
--profile <your-profile-name>
返回结果如下:
path
然后执行如下命令,为指定对象生成有效期为 900 秒的下载 URL:
aws s3 presign \
"s3://<your-bucket-name>/<object-key>" \
--profile <your-profile-name> \
--region ap-northeast-1 \
--endpoint-url "https://bucket.vpce-0123456789abcdef0-abc123.s3.ap-northeast-1.vpce.amazonaws.com" \
--expires-in 900
返回结果的结构如下。示例中的凭证、时间和签名均为占位符,不是可直接使用的真实签名:
https://bucket.vpce-0123456789abcdef0-abc123.s3.ap-northeast-1.vpce.amazonaws.com/<your-bucket-name>/<object-key>?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Credential=<credential>&X-Amz-Date=<timestamp>&X-Amz-Expires=900&X-Amz-SignedHeaders=host&X-Amz-Signature=<signature>
Path-style 不是 S3 Interface Endpoint 的强制要求。对于符合 DNS 命名规则且不包含点号的 Bucket,也可以将 s3.addressing_style 设置为 virtual:
aws configure set s3.addressing_style virtual \
--profile <your-profile-name>
此时仍然将 https://bucket.vpce-... 作为 --endpoint-url 传入,AWS CLI 会生成如下 Virtual-hosted-style 地址:
https://<your-bucket-name>.bucket.vpce-0123456789abcdef0-abc123.s3.ap-northeast-1.vpce.amazonaws.com/<object-key>?<presign-query-string>
如果 Bucket 名称包含点号,例如 example.private.bucket,Virtual-hosted-style 会产生多层 DNS 标签,可能导致 TLS 通配符证书无法匹配。因此,此类 Bucket 建议使用本文采用的 Path-style。
生成 URL 后,可以从能够访问该 Interface Endpoint 的 VPC、对等网络或者经 VPN、Direct Connect 连接的本地网络执行如下命令:
PRESIGNED_URL='<presigned-url>'
curl --fail-with-body \
--output <download-file-name> \
"${PRESIGNED_URL}"
Presign URL 只提供临时身份授权,不提供网络连通性,也不会绕过 IAM Policy、Bucket Policy、VPC Endpoint Policy、SCP(Service Control Policy)或 KMS Key Policy。常见问题如下:
| 问题现象 | 检查内容 |
|---|---|
SignatureDoesNotMatch |
检查签名生成后是否修改了 Host、对象路径、查询参数或 URL 编码,并确认 --region 与 Bucket 所在区域一致 |
| TLS 证书校验错误 | 检查是否使用 bucket.vpce-... 基础端点、是否直接使用了私有 IP,以及 Virtual-hosted-style 的 Bucket 名称是否包含点号 |
| 连接超时或 DNS 解析失败 | 检查客户端到端点 ENI 的路由、DNS、安全组入站 TCP 443 规则和网络 ACL |
AccessDenied |
检查签名身份的 s3:GetObject 权限、Bucket Policy、VPC Endpoint Policy,以及 SSE-KMS 对象所需的 kms:Decrypt 权限 |
| URL 提前失效 | 检查 IAM Role、STS 或 IAM Identity Center 临时凭证的剩余有效期;URL 有效期不会超过签名凭证的有效期 |
注意:Presign URL 在有效期内属于可直接使用的临时访问凭据,不应记录到应用日志、监控标签、工单或公开文档中。S3 Interface Endpoint 还会产生端点每小时费用和数据处理费用;如果访问方全部位于同一 VPC,应同时评估不收取端点每小时费用的 S3 Gateway Endpoint 是否已经满足访问需求。
下面转向使用 AWS SDK for Python(Boto3)生成相同类型的 Presign URL。
3、AWS Python SDK 为内网 S3 Interface Endpoint 的访问请求生成 Presign 签名
import boto3
from botocore.config import Config
region = "ap-northeast-1"
interface_endpoint = (
"https://bucket."
"vpce-0123456789abcdef0-abc123."
"s3.ap-northeast-1.vpce.amazonaws.com"
)
s3 = boto3.client(
"s3",
region_name=region,
endpoint_url=interface_endpoint,
config=Config(
signature_version="s3v4",
s3={
# 强制生成 PATH 格式的 URL
"addressing_style": "path",
},
),
)
presigned_url = s3.generate_presigned_url(
ClientMethod="get_object",
Params={
"Bucket": "my-private-bucket",
"Key": "documents/report.pdf",
},
ExpiresIn=900,
)
print(presigned_url)
五、参考文档
AWS STS GetCallerIdentity API 参考:
https://docs.aws.amazon.com/STS/latest/APIReference/API_GetCallerIdentity.html
AWS IAM Principal 策略元素参考:
https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_elements_principal.html
AWS IAM aws:PrincipalArn 全局条件键参考:
AWS 官方文档:通过 AWS PrivateLink 使用 Amazon S3 Interface Endpoint,包括端点 DNS 格式、Private DNS、AWS CLI 和 SDK 访问示例。
https://docs.aws.amazon.com/AmazonS3/latest/userguide/privatelink-interface-endpoints.html
AWS CLI s3 presign 命令参考,包括有效期参数和最大有效期限制。
https://docs.aws.amazon.com/cli/latest/reference/s3/presign.html
AWS CLI S3 配置参考,包括 path、virtual 和 auto 三种 addressing_style 配置。
https://docs.aws.amazon.com/cli/latest/topic/s3-config.html
Amazon S3 SigV4 查询字符串认证说明,包括 Canonical Request、X-Amz-* 查询参数和 Host 签名规则。
https://docs.aws.amazon.com/AmazonS3/latest/API/sigv4-query-string-auth.html
Amazon S3 官方文档:预签名 URL 的权限模型、有效期和限制条件。
https://docs.aws.amazon.com/AmazonS3/latest/userguide/using-presigned-url.html
Boto3 S3 Client generate_presigned_url API 参考。
Botocore Config API 参考,包括 signature_version 和 S3 服务专用配置。
https://botocore.amazonaws.com/v1/documentation/api/latest/reference/config.html
最后修改于 2026-08-11