干云安全这一行的人,大概都经历过那种后背发凉的瞬间:明明只是想让某个前端资源能被公网访问,结果一不小心把整个桶里几百份含身份信息的备份暴露了出去。Amazon S3 的“对象私有”问题,是我这几年看到最多、后果也最直接的安全事故之一。今天不聊理论,结合我自己排查和处理过的案例,把“预防性控制 + 强制执行”这套组合拳彻底拆开讲清楚。
先说结论:S3 对象私有不是靠某一个开关就能保证的。真正可落地的方案,必须在配置源头做预防性控制,让“公共访问”从一开始就配置不上去;同时对存量资源和漏网之鱼做强制执行,用扫描、审计、自动修复把已经出现的风险迅速压下去。两条线缺一不可,缺了哪条,都可能在某个没人注意的角落里埋雷。
1. 先想清楚:S3 对象为什么会和“公开”沾边
1.1 几条常见的翻车路线
我处理过的 S3 对象公开事件里,绝大多数不是黑客攻破了什么,而是配置层面的疏忽。这里列几个典型的翻车场景,大家可以对号入座。
第一种是“静态网站托管改崩了”。有人想用 S3 托管前端静态页面,于是在桶策略里加了一条 Principal: "*" 的只读策略,本意是允许访客加载 JS 和 CSS。但问题在于:这个桶里如果后来放了配置文件、数据库导出、日志压缩包,那这些对象会跟着一起被公网读取。更麻烦的是,很多人给完公读权限后就把这件事忘了,前端资源在 CloudFront 后面跑得挺好,桶里的隐私数据却悄悄裸奔了好几个月。
第二种是“对象 ACL 的历史遗留问题”。在 S3 的早期设计里,对象级别可以通过 ACL 单独设置公共读权限。很多人上传文件时看到“公共读”选项,觉得“我就给这一个文件开一下,应该没事”。但现实往往是:这个“就一下”的文件里躺着几千条用户手机号。旧账号里开启过 ACL 的桶尤其危险,因为存储在对象上的 public-read 授权不会因为你后来改了桶策略就自动消失。
第三种是“跨账户共享写歪了”。有些团队为了和其他账号共享数据,会写一个比较宽松的桶策略,本意是只允许指定账号访问,结果 Principal 写成了 "*",或者把 Action 从 s3:GetObject 扩展成了 s3:*。这类策略问题通常不会被立刻发现,因为它不影响自己人的正常读写,直到某次安全扫描亮起红灯。
第四种和客户端上传有关。如果应用通过预签名 URL 或者 IAM 临时凭证上传对象,而上传时又顺手带了 x-amz-acl: public-read 这个请求头,那对象被创建出来的那一刻就已经对外公开了。这种问题用桶策略很难发现,因为桶策略里并没有任何公网授权,真正的问题隐藏在对象的 ACL 里。
1.2 只堵一个口子远远不够
看到上面这些场景,你可能已经意识到:S3 的公开权限可以来自桶策略,也可以来自对象 ACL,甚至可以来自上传请求参数。入口太多,意味着你不能只靠一道防线。
“预防性控制”解决的是“以后不允许再出错”:比如在账号层面把 Block Public Access 打开、把桶的 ACL 机制禁用、通过组织策略禁止关闭这些开关。预防性控制的特点是前置、自动化、范围大,它让错误配置在提交的那一刻就被拦截。
“强制执行”解决的是“已经出错的东西必须被发现和处理”:比如用 Access Analyzer 检测存量桶是否公开、用 AWS Config 持续审计桶和对象的合规状态、用 EventBridge 捕获配置变更事件并触发自动修复。强制执行处理的是预防性控制还没来得及覆盖的死角,也包括那些在预防策略上线之前就已经存在的历史遗留问题。
如果只做预防,历史桶里已经存在的公共策略没人管;如果只做强制,每次有人设置了公共策略,你都要靠人工去发现和处理,中间的空窗期足够被扫描器抓取好几轮了。两条线拉起来之后,S3 对象私有才能真正变成一个“系统默认状态”,而不是靠某个人每次小心翼翼不出错。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 预防性控制:让“公开”在配置源头就失效
2.1 Block Public Access 四件套分别防什么
S3 Block Public Access 是 AWS 在多次数据泄露事件后推出的“总开关”,很多人知道要开,但不一定清楚它内部四个选项分别挡的是什么。
| 参数 | 含义 | 主要作用 |
|---|---|---|
| BlockPublicAcls | 阻止通过 ACL 授予公共访问权限 | 当有人尝试通过 PutBucketAcl / PutObjectAcl 写入 public-read 等公共授权时,API 会直接拒绝 |
| IgnorePublicAcls | 忽略桶或对象上已有的公共 ACL | 即使对象上挂着历史遗留的公共 ACL,S3 也不会再按公共 ACL 放行访问 |
| BlockPublicPolicy | 阻止桶策略授予公共访问权限 | 当 PutBucketPolicy 的桶策略内容允许匿名主体或所有 AWS 账户访问时,设置会被拒绝 |
| RestrictPublicBuckets | 限制桶策略中的公共访问只对特定服务主体生效 | 进一步收紧,避免同账号下其他授权途径意外获得公开访问能力 |
实际配置时我一般建议直接把四个选项全部打开,不用纠结。对绝大多数私有数据存储场景来说,这四个全开不会带来任何功能损失。可能会有团队担心“以后要托管静态网站怎么办”,但答案很明确:静态网站不应该依赖 S3 的公网直读,而应该用 CloudFront + OAC(Origin Access Control)的方式,让 S3 桶保持私有,CloudFront 以受信任服务身份回源读取。这样既保住了网站访问,又不会让桶里的其他对象暴露。
2.2 对象所有权与 ACL:从账户层面根除“公共读”按钮
很多人忽略了 S3 Object Ownership 这个设置。简单来说,它决定了上传到桶里的对象归谁所有、ACL 是否还能起作用。新版的推荐做法是把 Object Ownership 设置为 BucketOwnerEnforced,这会让 ACL 机制在桶上被彻底禁用。
为什么这层这么关键?因为如果桶开启了 ACL,而且 Object Ownership 是 ObjectWriter 或 BucketOwnerPreferred,那么上传者(尤其是跨账号上传者)可能拥有对象的所有权,他可以给自己上传的对象设置公共读 ACL。这就等于在你精心设计的桶策略之外,开了一扇不受管控的侧门。把 Object Ownership 切到 BucketOwnerEnforced 之后,ACL 不再生效,所有人只能通过桶策略和 IAM 权限来访问对象,审计面和管控面就干净多了。
你可以在控制台里检查一下旧桶的 Permissions -> Object Ownership,如果看到不是 BucketOwnerEnforced,建议尽快规划切换。切换前需要确认有没有依赖 ACL 的第三方工具或跨账号流程,比如某些基于 ACL grant 的授权场景,切换后可能报 403。大多数现代工具都走 IAM 策略,对这块的依赖已经很少了,所以切换的收益远大于成本。
2.3 IAM 权限与桶策略的最小化设计
账号级开关配好之后,还要管住人的操作范围。我给客户做权限设计时,会重点盯两个点:
一是普通开发人员不需要 s3:PutObjectAcl 和 s3:PutBucketAcl 这类权限。如果开发者根本没有给对象或桶写 ACL 的权限,那即使他想设置公共读,也会被 IAM 拦住。你可以在 IAM 托管策略里主动 Deny 这两个 Action,给“手滑”加一道闸门。
二是桶策略里的 Principal 必须极其敏感。看到 Principal: "*" 就要条件反射地追问:这条策略真的要允许所有匿名用户访问吗?如果只是给特定合作伙伴账号共享资源,应该写成明确的账号 ARN;如果只是允许自己组织内部的跨账号访问,也要限制到具体的角色 ARN。
还有一种容易被忽略的情况:通过预签名 URL 分享对象时,虽然对象本身是私有的,但预签名 URL 在有效期内等于一把临时钥匙。如果分享出去的有效期设置成一个星期甚至一个月,又转发了多次,那实际暴露面也很可观。这类场景建议尽量缩短有效期、限定最小权限,并且做访问日志审计。
2.4 把安全基线写进 IaC,让新桶生下来就带锁
如果你还在用“手动创建桶 + 事后补配置”的方式,那 S3 对象私有基本只能靠运气。更靠谱的做法是把安全配置写进基础设施即代码(IaC)模板里,让任何一个新桶在创建那一刻就自动继承一套完整的防护基线。
以 Terraform 为例,你可以在模板里同时声明账号级和桶级的防护配置,让每一次资源创建都默认安全。下面是我常用的一段基线模板片段,供参考:
hcl复制# 账号级:任何人新建桶,都受账号层 BPA 约束
resource "aws_s3_account_public_access_block" "account" {
account_id = data.aws_caller_identity.current.account_id
block_public_acls = true
ignore_public_acls = true
block_public_policy = true
restrict_public_buckets = true
}
# 桶级:即使账号级设置被某个角色例外绕过,桶自己也有独立防线
resource "aws_s3_bucket" "secure" {
bucket = "my-secure-data-${var.env}"
force_destroy = false
}
resource "aws_s3_bucket_public_access_block" "secure" {
bucket = aws_s3_bucket.secure.id
block_public_acls = true
ignore_public_acls = true
block_public_policy = true
restrict_public_buckets = true
}
# 默认关闭 ACL / 使用 BucketOwnerEnforced
resource "aws_s3_bucket_ownership_controls" "secure" {
bucket = aws_s3_bucket.secure.id
rule {
object_ownership = "BucketOwnerEnforced"
}
}
# 顺手把加密和版本管理也配齐
resource "aws_s3_bucket_server_side_encryption_configuration" "secure" {
bucket = aws_s3_bucket.secure.id
rule {
apply_server_side_encryption_by_default {
sse_algorithm = "aws:kms"
}
}
}
resource "aws_s3_bucket_versioning" "secure" {
bucket = aws_s3_bucket.secure.id
versioning_configuration {
status = "Enabled"
}
}
这段模板的意义在于:它把一个“私有桶该有的样子”固化成标准,而不是让每个人每次创建桶时都靠记忆去勾选那些选项。人总会犯错,模板不会。配合 CI 里的 checkov 或 tfsec 扫描,任何试图去掉 BPA 或把 Object Ownership 改回老模式的代码提交,都会在 Merge 之前被拦下。
3. 强制执行:对存量桶和漏网配置持续监管
3.1 用 Access Analyzer 实时发现已被公开的桶
预防性控制再严,也挡不住在它上线之前就已经存在的历史问题。S3 Access Analyzer 就是专门用来回答“现在有哪些桶已经向外部暴露了”的工具。
启用方式很简单:在 IAM Access Analyzer 里创建一个 Analyzer,类型选择 External access analyzer,它会自动扫描当前账号下的 S3 桶策略、ACL,并输出两类 findings——一种是允许匿名外部访问,另一种是允许其他 AWS 账户访问。无论哪种,只要是意料之外的,都应该被当成风险事件来处理。
实际使用中我建议把 Access Analyzer 的 findings 接入 Security Hub 或者 SNS 告警,不要只是开了之后等想起来才去看一眼。曾经有个客户的备份桶因为某个合作伙伴离职前的临时授权一直没清理,被 Access Analyzer 标成 cross-account 访问持续了好几个月,就是因为没有设置通知。把告警接出去之后,这类问题才能变成“当天发现,当天处理”。
需要注意的是,Access Analyzer 是检测工具,不是拦截工具。它不会阻止公共策略的上传,只会告诉你“已经出问题了”。所以它不能替代第 2 章的预防性控制,两者是配合关系。
3.2 AWS Config 规则与自动修正:不合规就处理
AWS Config 可以把“S3 资产必须保持私有”变成一条可量化、可持续追踪的规则。S3 相关的托管规则我常用这几个:
s3-account-level-public-access-blocks-periodic:定期检查账号级 BPA 是否全开。s3-bucket-level-public-access-prohibited:检查每个桶的 BPA 设置是否完整。s3-bucket-public-read-prohibited:检查桶是否允许公共读。s3-bucket-public-write-prohibited:检查桶是否允许公共写。s3-bucket-ssl-requests-only:顺带要求桶只允许 HTTPS 访问。
开启 Config 规则后,你可以为不合规资源配置自动修正(remediation)。一个常见的做法是:当 Config 发现某个桶没有开启 BPA,触发一个自动化修复动作,自动调用 API 把桶级 BPA 全开。
但这里我要提醒一句:自动修正动作要谨慎设计。不要对生产环境所有桶无差别执行“一键修复”,尤其不能把桶策略整个覆盖掉。最稳妥的方案是设计一个小型 Lambda,它只做“增量修复”:检查 get_public_access_block 返回的参数,如果是 false,就调用 put_public_access_block 把全开配置推进去;同时检测桶策略,如果存在公网主体,就把原策略保存到 S3 的审计桶里再做拒绝处理。修复动作必须留审计痕迹,方便事后回溯。
3.3 EventBridge 审计事件:从 CloudTrail 到自动关门的最后一环
单靠 Config 的定时检查,发现风险通常有几分钟到几十分钟的滞后。对于真正的高风险配置变更,我推荐再加一层“实时事件”防线。
原理是:所有对 S3 安全配置的修改都会以 CloudTrail 事件的形式被记录下来。你可以创建 EventBridge 规则,监听下面几类关键事件:
PutBucketPolicyPutBucketAclPutObjectAclPutPublicAccessBlock(如果有人试图关闭 BPA,这就是严重信号)DeleteBucketPolicy
当事件发生时,EventBridge 可以把事件投递给 SNS 告警,也可以直接触发一个 Lambda 做实时检查。这个 Lambda 不需要太复杂:拿到事件里的桶名和发起者身份,先调一次 get_public_access_block 和 get_bucket_policy,如果发现桶策略里的 Principal 是 * 或包含所有 AWS 账户授权,就立即执行关闭操作并通知安全团队。
这套机制我实测下来非常有价值。很多安全事故并不是配置人员恶意为之,而是某次调试时图方便先放开了权限,想着“等会儿再关”,结果后来忘了。有了事件驱动检查,就算忘了,系统也会在几秒内提醒他甚至直接把权限收回去。
3.4 对象清单与访问日志:查漏补缺的兜底手段
当桶的数量很多、对象数量达到千万级以上时,上面的策略主要覆盖桶级配置,但对象级的 ACL 状态很难被全部实时感知。这时候可以选择性开启 S3 Inventory 功能,让 AWS 定期输出一份对象清单,里面包含每个对象的 ACL、加密状态、大小等元数据。把这份清单投递到专用的审计桶中,再用 Athena 跑查询,就能批量找出那些 ACL 中存在公共授权的对象。
服务器访问日志则是另一个层面:如果某个对象在某段时间内收到了大量来源 IP 分散的 GET 请求,通常意味着它已经被公开或 URL 已经泄露。把访问日志投递到审计桶后,可以定期跑一遍 SQL,筛选出没有 Referer、来源分散、下载量异常的热点对象,这些都是值得排查的线索。
不过这两个手段的存储成本都不低,属于“重要资产必开,非核心资产按需开”的定位。我一般会先给含有个人信息、密钥、备份数据的核心桶开启 Inventory 和访问日志,其他普通桶靠 Access Analyzer 和 Config 兜底就够了。
4. 从 0 到 1 的落地参考:新账号与存量账号都覆盖
4.1 新账号 S3 安全基线的 6 个步骤
如果你现在接手了一个全新账号,或者想给整个组织建一套 S3 私有标准,可以参考下面这个顺序,从上到下把保险层层扣上。
第一步,先把账号级 Block Public Access 全开。使用 CLI 可以执行:
bash复制aws s3control put-public-access-block \
--account-id 123456789012 \
--public-access-block-configuration \
BlockPublicAcls=true,IgnorePublicAcls=true,BlockPublicPolicy=true,RestrictPublicBuckets=true
这一步的作用是给整个账号定基调:任何新桶和已有桶,都不能再接受新的公网策略配置。
第二步,检查并迁移所有存量桶的 Object Ownership。把非 BucketOwnerEnforced 的桶都记录下来,在确认没有依赖 ACL 的工作流后,逐个切换过去。
第三步,把 S3 安全基线模板纳入 IaC。新建桶一律通过 Terraform 或 CloudFormation 模板创建,模板里强制包含第 2.4 节的那些配置项。不允许任何人通过控制台手动创建生产桶。
第四步,在管理账号上配置组织级策略(SCP),防止子账号自己关闭账号级 BPA,或者写入公网桶策略。
第五步,开启 Access Analyzer + AWS Config + Security Hub,把 S3 相关检测规则全部打开,并配置告警到一个没人会忽略的渠道。如果团队用 Slack,就推到安全频道;如果只有邮件,建议再绑一个值班手机号。
第六步,建立“发现即响应”的流程。无论谁收到 S3 公开告警,第一反应不是争论为什么,而是先把风险控制住。等恢复之后再去复盘根因和责任人。
4.2 SCP:从组织层防止有人关掉 BPA
对于多账号组织,最怕的就是某个业务账号的 admin 为了“方便”,把 BPA 关掉或者改桶策略。这时候需要在组织管理账号上添加一条 SCP,对下面的所有账号生效,内容大致如下:
json复制{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "DenyDisableS3PublicAccessBlock",
"Effect": "Deny",
"Action": [
"s3:PutAccountPublicAccessBlock",
"s3:PutBucketPublicAccessBlock"
],
"Resource": "*"
},
{
"Sid": "DenyPublicAclViaHeaders",
"Effect": "Deny",
"Action": [
"s3:PutBucketAcl",
"s3:PutObjectAcl"
],
"Resource": "*",
"Condition": {
"StringEquals": {
"s3:x-amz-acl": [
"public-read",
"public-read-write",
"authenticated-read"
]
}
}
}
]
}
注意这样一条 SCP 比较严格,会挡住所有子账号修改 Public Access Block 的 API 调用。如果某些账号确实有合法的“公开只读”需求,需要走正式审批流程,单独为那个账号设置 OU 并挂上允许例外的新 SCP,而不是把整条策略放开。SCP 是组织级治理工具,不建议在没想清楚审批流程前草率上全量拦截。
4.3 存量桶排查示例:先摸清家底再动手
不管你的新账号基线有多漂亮,生产环境里几乎总是存在“历史遗留”账号和桶。处理存量问题的第一步是摸清家底:到底哪些桶 BPA 没开,哪些桶策略里有公网主体。
下面这段 Python 脚本可以快速列出一个账号下所有 BPA 未全开的桶:
python复制import boto3
s3 = boto3.client("s3")
s3control = boto3.client("s3control")
account_id = boto3.client("sts").get_caller_identity()["Account"]
paginator = s3.get_paginator("list_buckets")
risky_buckets = []
for page in paginator.paginate():
for bucket in page["Buckets"]:
name = bucket["Name"]
try:
block = s3.get_public_access_block(Bucket=name)["PublicAccessBlockConfiguration"]
if not all([
block.get("BlockPublicAcls", False),
block.get("IgnorePublicAcls", False),
block.get("BlockPublicPolicy", False),
block.get("RestrictPublicBuckets", False),
]):
risky_buckets.append(name)
except s3.exceptions.NoSuchPublicAccessBlockConfiguration:
risky_buckets.append(name)
except Exception as e:
print(f"[ERROR] {name}: {e}")
print("BPA 未全开的桶:", risky_buckets)
跑完可以把结果保存下来,逐个处理。处理顺序建议是:先为这些桶单独补上 BPA,再检查桶策略和 ACL,最后考虑是否备份并迁移数据。如果有些桶确实不需要继续保持,直接删除可能是最安全的选择。
5. 实战中的几个坑和长期维护建议
5.1 几个最容易误判的场景
第一个坑是认为“账号级 BPA 开了,存量公共策略就自动失效了”。实际上,账号级 BPA 主要阻止未来新的公共配置写入,对已经存在的桶策略公共授权,处理逻辑并不可靠。尤其是 RestrictPublicBuckets 和 IgnorePublicAcls 的具体行为容易被高估。即使开了账号级 BPA,也必须手动检查一遍存量桶,把已经存在的公共策略和公共 ACL 清掉,不能一开了之。
第二个坑是切换 Object Ownership 前没有梳理依赖。BucketOwnerEnforced 模式会禁用 ACL,如果某个上传工具或日志同步服务还在使用 x-amz-acl 请求头,切换后会立刻收到 AccessDenied。我建议在非生产账号里先找几个典型工作流验证一遍,再逐步推广到生产账号。
第三个坑是桶策略和 ACL 有两套授权体系,出问题时的排查思路容易走偏。如果一个对象访问时报 403,不要只盯着桶策略看,还要确认对象 ACL 里是否只有 bucket owner 有权限。配合 S3 Access Analyzer 的 findings 检查会更高效。
第四个坑是以为“我没在桶策略里允许公网访问,对象就一定私有”。S3 的预签名 URL 就是一种合法的“临时公网访问”途径。如果有人拿到了一条有效期很长的预签名 URL,他不需要任何 AWS 凭证就能读取对象。这类对象在 Access Analyzer 里也不会被标为 public,因为它属于正常授权。因此密钥管理和 URL 有效期控制也是“对象私有”的一部分。
第五个坑是测试公开访问时用错了方法。用自己当前账号的凭证访问当然永远是通的,正确做法是模拟匿名用户去访问,比如:
bash复制curl -sI https://your-bucket-name.s3.amazonaws.com/secret.txt
# 或者显式不带签名
aws s3api get-object \
--bucket your-bucket-name \
--key secret.txt \
--no-sign-request /dev/null
只有当这样的请求返回 AccessDenied,才算真正验证了“外部不可访问”。
5.2 长期维护的几条铁律
长期维护 S3 对象私有,我更愿意把它当成一个持续运营动作,而不是一次性配置。
- 将账号级 BPA 全开设为新账号的默认前提,用 SCP 禁止关闭。
- 生产桶不允许通过控制台手动设置权限,配置变更一律走 IaC 代码评审。
- Config 规则发现不合规时,至少做到告警必达,最好能自动修复并留下审计记录。
- Access Analyzer 的 findings 每周至少复核一遍,异常授权及时清理。
- 每年做一次面向开发团队的安全配置培训,重点讲真实泄露案例,比抽象规范有用得多。
- 对核心资产桶开通 Inventory 和访问日志,定期分析是否存在异常下载行为。
S3 对象私有这件事,真正难的不是技术,而是把“默认安全”变成一种肌肉记忆。我自己在不同账号里反复踩过坑之后的最大体会是:不要相信任何人的临时操作承诺,一切以自动化基线为准。只要预防性控制覆盖了源头,强制执行兜住了存量,这套双轨机制就能让 Amazon S3 里的对象真正回归私有状态。
