对象存储桶加固实战:公共读与临时凭证

对象存储桶权限是云上最容易配错的一处。为了省掉签名逻辑,不少站点直接把桶设成公共读,结果整桶可被列举,备份文件、应用日志、用户上传的证件照一并暴露。本文从盘点现状开始,一步步把匿名访问关掉,同时保证前端资源仍能正常加载。

适用场景

三类信号说明该查桶权限了:站点图片附件直接使用存储桶裸域名,没有走 CDN;前端 JS 里出现 accessKeyId、secretAccessKey 字样;桶里存过数据库备份、应用日志、临时导出文件。第三类最危险,一旦桶公开,等同于把整站数据打包放在公网等人下载。

此外,做等保测评或安全审计时,”存储桶是否允许匿名列举”是必查项。提前自查的成本远低于被通报后整改。

前置条件

  • 具备桶策略管理权限的 RAM 子账号,不要用主账号密钥操作;
  • 已安装 ossutil(对象存储)或 aws cli(S3 兼容存储);
  • 掌握站点前端的资源引用方式:直连桶域名,还是已经套了 CDN;
  • 可修改应用配置的发布权限,用于切换到临时凭证方案。

原理说明

对象存储的访问控制在多数云平台上由三层叠加构成:桶 ACL、桶策略、对象 ACL。判定逻辑是任一显式放行即可访问,因此只把桶 ACL 改成私有、桶策略里还留着允许所有主体的语句,匿名访问依然成立。这是自查时最常见的漏项,也是”我明明关了公共读为什么还能打开”的根本原因。

第二层风险来自长期凭证。前端直传方案如果使用主账号或长期密钥,密钥必然出现在浏览器里,泄露只是时间问题。正确做法是 STS 临时凭证:服务端用 RAM 角色换取有效期通常十五分钟到一小时的临时令牌,并附带一个收窄到具体前缀、具体动作的会话策略。即使令牌被截获,攻击者可做的操作也被限制在策略范围内,且很快过期。

第三层是访问路径。公共读之所以流行,是因为私有桶的图片无法被页面直接引用。解决办法不是重新公开,而是让 CDN 回源时携带签名,或在服务端下发预签名链接。前者对用户透明,适合图片站与资讯站;后者适合下载类业务,链接天然带时限。

操作步骤

步骤一:盘点桶的暴露面

先确认对象规模与当前访问控制状态,规模数据决定后续迁移窗口。

ossutil config -e oss-cn-hangzhou.aliyuncs.com -i LTAI*** -k ***
ossutil ls oss://my-bucket -s
ossutil stat oss://my-bucket
ossutil get-acl oss://my-bucket
ossutil bucket-policy --method get oss://my-bucket

ossutil ls -s 输出对象总数与总容量;get-acl 返回 public-read 说明桶可被匿名读取;bucket-policy –method get 输出桶策略 JSON,重点找允许所有主体的语句。三项都确认完,再动手改。

步骤二:关闭匿名访问

先把对象级 ACL 统一收敛,再改桶 ACL,最后覆盖放行匿名主体的策略。顺序不能反,否则中途会出现部分对象仍可访问的窗口。

# 批量把对象 ACL 改为继承桶
ossutil set-acl oss://my-bucket/ private -r -f

# 桶 ACL 设为私有
ossutil set-acl oss://my-bucket private

# 用显式拒绝策略兜底
cat > /tmp/bucket-policy.json <<'EOF'
{
  "Version": "1",
  "Statement": [
    {
      "Effect": "Deny",
      "Principal": ["*"],
      "Action": ["oss:GetObject", "oss:ListObjects"],
      "Resource": ["acs:oss:*:*:my-bucket/*"],
      "Condition": {
        "StringNotEquals": {
          "acs:SourceVpc": "vpc-xxxxxxxx"
        }
      }
    }
  ]
}
EOF
ossutil bucket-policy --method put oss://my-bucket /tmp/bucket-policy.json

这段策略用的是显式拒绝而不是允许:除指定 VPC 来源外,读对象与列目录一律拒绝。显式拒绝优先级最高,能压过历史遗留的允许语句,适合收尾阶段兜底。稳定运行几天后再把策略精简为只保留必要规则。

步骤三:让前端资源继续可访问

桶转私有后,直连桶域名的图片会返回 403。两条路径可选,优先第一条。

# 路径 A:CDN 回源私有桶
# 在 CDN 控制台的回源配置里把回源类型改为私有桶,关联回源角色
# 源站地址填桶的内网域名,桶上不需要开公共读

# 路径 B:服务端下发预签名链接,适合下载类业务
ossutil sign oss://my-bucket/report/2026-10.pdf --timeout 600

–timeout 单位是秒,下载类链接给三百到六百秒足够。不要图省事设成七天,那等于变相公开,签名链接一旦被转发就失去控制。

步骤四:改造成 STS 临时凭证

前端直传场景必须改。服务端先申请角色,再用角色换取临时令牌,会话策略把权限收窄到单个前缀。

# 服务端换取临时凭证
aliyun sts AssumeRole \
  --RoleArn "acs:ram::1234567890:role/oss-upload-role" \
  --RoleSessionName "web-upload" \
  --DurationSeconds 900 \
  --Policy '{"Version":"1","Statement":[{"Effect":"Allow","Action":["oss:PutObject"],"Resource":["acs:oss:*:*:my-bucket/user-upload/*"]}]}'

# 浏览器侧只用返回的临时 AccessKeyId / AccessKeySecret / SecurityToken
# 上传路径固定前缀,禁止覆盖他人对象

关键参数是 DurationSeconds(有效期,900 秒即十五分钟)与 Policy(会话策略)。会话策略只能进一步收窄角色权限,不能放大,所以角色本身的权限也必须按最小化配置,两层都收紧才算到位。

步骤五:清理历史凭证并开启审计

# 检查是否还有代码在使用长期密钥
grep -rn "LTAI\|accessKeyId\|secretAccessKey" /www/wwwroot --include="*.js" --include="*.php" | head
# 开启桶访问日志
ossutil logging --method put oss://my-bucket oss://log-bucket/logs/
ossutil logging --method get oss://my-bucket

开启日志后重点看请求者字段:出现匿名标识说明仍有匿名访问;出现陌生地址且请求集中在列举操作,基本可以判定桶正在被遍历。日志建议放在另一个桶,且该桶必须私有。

配置验证

  • 匿名读:curl -I https://my-bucket.oss-cn-hangzhou.aliyuncs.com/test.txt 应返回 403 AccessDenied,返回 200 说明仍是公开。
  • 匿名列举:不带任何凭证执行 ossutil ls oss://my-bucket 应报无权限。
  • 前端加载:清浏览器缓存与 CDN 缓存后访问页面,图片全部正常,资源域名应为 CDN 域名而非桶域名。
  • 签名过期:用 --timeout 60 生成的链接在一分钟后访问应返回签名过期错误。
  • 日志核对:访问日志中出现新的匿名请求即为漏配,回到步骤二检查桶策略。

常见问题

Q1:桶 ACL 和桶策略都改了,匿名访问还是 200?

检查三个地方:对象级 ACL 是否单独设过公开读,对象 ACL 会覆盖桶的默认行为;是否开启了静态网站托管,该端点允许匿名读取;是否配置了 CDN 回源且回源鉴权关闭,此时访问的其实是 CDN 缓存而不是桶本身。用带随机参数的 URL 绕开缓存再测,例如在地址后加一个随机查询串,能区分是缓存还是真实放行。

Q2:关掉公共读后站点图片全部 403,怎么快速恢复?

临时办法是回滚桶 ACL 争取时间,但这只是拖延。正确做法是当天就把 CDN 私有回源配好:CDN 用回源角色访问私有桶,用户访问 CDN 域名,桶保持私有。若资源量不大,也可以把图片迁到独立桶、只对该桶开公共读,并且桶内只放图片,用前缀隔离风险。

Q3:预签名链接会不会被转发滥用?

会。签名链接本质是带时限的通行证,任何拿到地址的人在有效期内都能访问。控制手段是缩短有效期、签名时绑定来源 IP(部分云支持),以及敏感文件走服务端鉴权后再流式转发,而不是把签名地址直接返回给前端。

总结

桶权限加固的判断标准很简单:在完全不提供任何凭证的情况下,能读到一个对象就算没配好。落地顺序是先盘点、再关匿名、然后解决前端访问路径、最后把长期密钥换成 STS 临时凭证,并用访问日志做持续验证。公共读不是绝对不能用,而是只能用在只放公开静态资源、且桶内没有其他内容的独立桶上。