ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

OSS权限排查实战:从一次AccessDenied看ACL陷阱

OSS权限排查实战:从一次AccessDenied看ACL陷阱 最近在某企业客户现场排查一个诡异的上传故障日志里反复出现同一个错误oss upload failed: AccessDenied: put public object ACL is not allowed。乍一看像是权限配置不对可明明Bucket权限已经设置为公共读上传者的AccessKey也给了AliyunOSSFullAccess。折腾了一下午最后定位到问题根源时团队里一个小伙子脱口而出原来OSS的ACL还有这层讲究。这个场景其实就是OSS对象存储日常使用的缩影——大多数人会对上传下载文件这个基本功能非常熟悉但一旦涉及权限模型、ACL策略、服务端与客户端的职责边界就容易掉进各种隐性坑里。这篇文章不打算复述官方文档而是结合我这些年用OSS对象存储、Ceph对象存储以及RustFS(S3)兼容存储的实际经历把那些文档里写了但没强调、或者说不踩坑根本记不住的知识点拎出来讲透。内容既覆盖新手入门需要理解的存储模型也包含老手平时容易忽略的权限细节和选型思路。1. 先拆清楚OSS对象存储的底层工作逻辑1.1 Bucket、Object、Endpoint三者的真实关系很多初学者会把OSS对象存储当成一个网盘这其实是个不太准确的类比。网盘的核心逻辑是文件夹—文件的层级结构而OSS的核心逻辑是桶Bucket—对象Object的扁平结构。Bucket是存储空间的载体Object是存储内容的实体两者之间并不存在嵌套关系。这里的扁平结构值得展开说。你可能会在控制台里看到类似images/avatar.jpg这样的路径看起来像是一个文件夹下的文件但本质上images/avatar.jpg就是完整的一个Object名称Key那个看起来像目录的images/前缀只是OSS为了便于人类理解和API分组而做的一种Key命名约定。也就是说你能通过prefix参数列出所有以images/开头的Object但OSS底层并没有为images/这个目录分配任何独立存储实体。这一点直接影响后续的上传流程设计。举个例子如果你需要实现按日期归档日志的功能大多数人会下意识创建logs/2025/01/15/这种方式的前缀然后在代码里拼接完整的Object Key。这本身没问题但如果你的程序里出现了先创建文件夹再上传文件、或者试图对某个前缀进行重命名的逻辑那就走偏了——OSS压根没有文件夹重命名的语义你要做的是批量复制Object并修改Key然后删除旧Object。Endpoint访问域名则是连接客户端与存储服务的门牌号。国内使用OSS时Endpoint通常对应你创建Bucket时选择的Region比如oss-cn-hangzhou.aliyuncs.com。不同Region的Endpoint彼此隔离数据默认不跨Region流动除非你开启了跨区域复制功能。这里有一个很实用的经验在做多环境部署时建议把Endpoint、Bucket名称、Region全部配置化而不是硬编码在代码里。我遇到过不止一个项目因为测试环境用了oss-cn-shanghai的Endpoint、生产环境换到oss-cn-beijing结果代码里写死了访问域名导致生产环境的图片加载不出来。这种问题排查起来还特别费劲因为你第一反应是检查权限而实际上只是Endpoint配置错了。1.2 数据模型、访问入口与常用的访问方式理解OSS的数据模型之后还需要搞清楚它的访问入口。大致归纳为三类入口类型典型场景特点控制台人工管理、临时查看、配置权限交互友好适合少量操作API/SDK程序化上传下载、服务端业务集成最常用功能完整工具类如ossutil、ossbrowser批量迁移、命令行操作、调试效率高适合运维场景从我个人的经验来看服务端业务集成是使用频率最高的场景。常见的设计方案有两种一种是服务端先接收客户端上传的文件再转存到OSS即服务端中转另一种是客户端直传OSS即客户端直传服务端只负责签发临时上传凭证。服务端中转的好处是逻辑简单、便于统一管控但缺点是会占用应用服务器的带宽和计算资源。如果你们的应用是面向C端用户的并且有大文件上传需求比如视频、安装包我强烈建议使用客户端直传方案再配合STS临时凭证来保障安全而不是让每个用户的文件都从应用服务器过一遍。讲到STS临时凭证这里先埋一个伏笔——权限控制这部分恰恰是OSS使用中最容易出问题的环节我在后面的章节会专门用一个完整的排查案例来展示。2. 从一次AccessDenied排错看懂OSS权限模型2.1 完整的错误信息与表象分析回到文章开头提到的那个问题。客户使用的自有平台姑且称为X平台对接OSS当用户上传文件到指定Bucket时日志刷出了大量类似这样的错误oss upload failed: AccessDenied: put public object ACL is not allowed request id: 5C1B... Status: 403客户运维同事的第一反应是看RAM权限——毕竟错误是AccessDenied。他们给程序使用的RAM子账号添加了AliyunOSSFullAccess甚至一度尝试了AdministratorAccess但错误依然没有消失。接着他们又怀疑是Bucket Policy限制检查后发现Bucket并没有配置显式的拒绝策略。这样一来大家就更困惑了。权限都开到最大了为什么还会被拒绝其实这个问题的排查方向应该拆分来看AccessDenied分为很多种——签名不匹配、请求被Bucket Policy拒绝、RAM授权不足、ACL规则限制等。它们虽然返回的状态码都是403但错误码和错误信息各不相同。这里需要把目光集中在那句put public object ACL is not allowed上它明确指出了一个容易被忽略的事实你正在尝试设置Object的ACL为public但OSS不允许这个行为。2.2 一步步追查从访问身份到存储空间策略再到Object单独权限顺着报错信息排查链路应该是这样的第一层确认访问身份。如果你是使用RAM子账号调用的API先确认这个子账号所属RAM用户/角色的策略是否包含oss:PutObjectAcl权限。AliyunOSSFullAccess确实包含这个权限所以这一层通常没问题这个案例中也没有问题。第二层确认Bucket所属的权限策略。OSS的Bucket Policy支持显式允许和显式拒绝且显式拒绝的优先级最高。如果Bucket Policy中存在拒绝oss:PutObjectAcl的语句那么即使RAM授权给足了请求依然会被拒绝。这个客户没有配置Bucket Policy因此这一层也排除。第三层确认Bucket的ACL规则。这也是这一案例真正的坑点。OSS的Bucket ACL支持三类权限级别private只有Bucket Owner有读写权限public-read所有人可读只有Owner可写public-read-write所有人可读可写一般不建议在生产环境使用在这个案例中客户为了前端页面可以匿名访问图片将Bucket的ACL设置成了public-read。问题就出在这里——当某个Bucket的ACL被设置为public-read或public-read-write时Aliyun OSS服务端会禁止Bucket内单个Object被再次显式设置为public-read。为什么因为在OSS的权限模型里Bucket ACL是Object ACL的作用域上界Bucket已经对所有匿名用户开放了读权限再单独设置Object为public-read不仅冗余而且可能引发安全配置混乱。换句话说当Bucket是私有的时候你可以单独设置某个Object为公共读这相当于在整体私有空间里开放单个文件但当Bucket已经整体公共读时你再尝试单独对某个Object重复设置公共读OSS就直接用403来拒绝这个冗余请求报错就是put public object ACL is not allowed。当时客户的业务代码在每次上传文件后都会调用PutObjectAcl接口将文件的ACL显式设置为public-read。之前Bucket一直是private所以这段逻辑正常跑了一年多后来运维同学为了让静态资源免鉴权访问将Bucket改成了public-read这段多此一举的代码立刻变成了故障源。第四层检查Object级别的ACL。OSS的Object也可以设置独立ACL。当你不对Object显式设置ACL时Object会继承Bucket的ACL准确说是遵守Bucket的默认权限策略。如果代码里强制将Object ACL设置为public-read、而整个Bucket的ACL又已经是public-read时就触发了服务端的校验拦截。当时我们给出的解决方案也很简单——将业务代码中每次上传之后设置Object ACL的调用移除完全依赖Bucket的public-read权限。如果某些文件确实需要保持私有比如用户隐私数据则在Bucket不开公共读的前提下单独对指定Object设置public-read或使用签名URL。2.3 Root Cause总结与权限配置的核心原则整个排错链路走完其实可以得到几条权限配置的核心原则明确Bucket权限与Object权限的作用范围。Bucket ACL偏向于整体访问边界Object ACL更偏向于单个文件的精细控制。两者之间存在服务端校验规则不是任意组合都能通过。代码中尽量避免高频调用PutObjectAcl。每次上传都显式设置ACL不仅拖慢上传速度多一次API请求还会埋下许多隐患。更通用的做法是设计好Bucket级别的默认权限再针对特殊文件走独立策略。权限排查要先读错误信息中的错误码和错误描述而不是看状态码就猜。AccessDenied只是结果真正的线索在put public object ACL is not allowed这半句里。在权限模型的细节上还有一点容易被忽略OSS的访问控制体系是一个组合模型包含了RAM授权、Bucket Policy、Bucket ACL、Object ACL以及STS临时凭证。其中任何一个环节显式拒绝了请求都过不去反之只有所有环节都放行请求才可能成功。这就像过安检有多道门禁任何一道门拒绝了你你都到不了登机口。3. RAM Policy、Bucket Policy、STS临时凭证的分工与协同3.1 三种权限控制方式的使用边界在实际项目中权限相关的概念往往是混淆重灾区。这里我尝试用最简洁的方式厘清。RAM Policy解决的是操作者是谁、他能不能做某件事的问题。它挂载在RAM用户、RAM角色或RAM用户组上是一种身份授权策略。比如你创建一个叫作app-server的RAM用户给它绑定AliyunOSSFullAccess那么这个用户就可以对账号下所有Bucket执行所有OSS操作。Bucket Policy解决的是某个Bucket允许/拒绝谁做什么的问题。它是资源维度的授权策略挂在具体的Bucket上。用法上Bucket Policy不仅可以用在RAM身份上也可以授权给其他云账号、匿名用户*还可以对指定IP段、指定请求源VPC等条件做精细控制。STS临时凭证解决的是临时、可控、可撤销的授权问题。服务端通过调用AssumeRole接口获得一个临时AccessKeyId、临时AccessKeySecret和安全Token下发给客户端。客户端用这套临时凭证直传OSS到期自动失效无需在客户端代码里保存长期AccessKey。三者的核心分工可以概括为RAM Policy管谁能做什么Bucket Policy管这个资源让谁做什么STS管临时的入场券。3.2 实战中最推荐的权限设计方案如果你需要设计一套稳定、安全、可维护的OSS权限体系这里有一份可以直接参考的方案。第一所有服务端应用使用的账号统一用RAM子账号并遵循最小权限原则。不要图省事直接给AliyunOSSFullAccess而应该按业务需要配置具体Action和Resource。比如一个只负责上传头像的应用策略可以聚焦在oss:PutObject上Resource限定到指定的Bucket和Object前缀。第二开启公共读的Bucket尽量不要和存放敏感数据的Bucket混用。有的团队图省事把用户头像、用户身份证照片、业务日志全部塞进同一个Bucket然后设置成整体私有再靠Object ACL对外开放头像和图片。这个方案的坏处是ACL配置散落各处极易出现误开放或误关闭。更好的做法是把要公开的资源和要私密的资源放在不同的Bucket中公开的直接配public-read私密的保持private再用签名URL或STS来授权访问。逻辑清晰事故率也低。第三凡是涉及客户端直传一律使用STS临时凭证。服务端生成STS Token的时候可以精确控制此次上传的权限范围、过期时间以及允许的Object路径前缀。比如某个用户在发布文章时需要上传三张配图服务端可以签发一个有效期5分钟、只允许上传到article/2025/3461/前缀下的临时凭证过期自动失效。这种做法在安全上的收益远大于在客户端内置AccessKey的便利。第四定期审计Bucket Policy和RAM Policy是否有过度授权。我见过很多团队在排障过程中把权限越开越大战斗力是暂时恢复了但几个月之后根本没人记得哪个策略是谁加的。建议用脚本定期拉取账号下的权限配置清单做一个权限最小化review。3.3 STS临时凭证的具体接入思路STS接入流程不算复杂典型步骤是在RAM中创建角色Role并为该角色配置信任策略允许你的服务端RAM用户调用AssumeRole。为这个角色配置权限策略比如仅允许往指定Bucket的指定前缀上传。服务端调用AssumeRole接口传入角色的ARN、自定义会话名称和过期时间拿到临时凭证。将临时凭证下发给客户端客户端使用临时AccessKeyId、临时AccessKeySecret和SecurityToken初始化OSS客户端。客户端直接调用OSS的上传接口把文件传到指定位置。在实际落地时有两点尤其要提醒临时凭证的最小过期时间是900秒15分钟最大过期时间根据角色配置而定一般建议上传场景设置15到30分钟即可太长会增加凭证泄露风险另外STS临时凭证偶尔会因为客户端本地时钟和服务端时间差过大而出现校验失败这点在接入时需要在客户端SDK里做时间校准或自动重试。4. 选型对照从Ceph对象存储到RustFS(S3)的一线观察4.1 Ceph对象存储适合什么场景在热词里出现Ceph对象存储说明很多人正在做自建对象存储的调研。Ceph的RGWRADOS Gateway实现了S3兼容接口可以对外提供对象存储能力是国内不少企业自建存储系统的首选。Ceph的优势在于一个集群同时提供块存储RBD、文件存储CephFS和对象存储RGW适合已有物理机资源、需要统一存储底座的中大型团队。和OSS相比Ceph最大的挑战在于运维复杂度。它不是一个开箱即用的软件部署、调优、监控、扩缩容都需要专门的团队。我见过不少团队把Ceph搭起来之后长期停留在能用但性能不稳定的状态最后不得不重做架构。从数据一致性角度来看Ceph RGW已经提供了很强的可用性保障但在多站点容灾、跨地域复制、生命周期管理等高层能力上和云上OSS比起来还是需要自己拼装。4.2 从RustFS(S3)这类轻量化实现看对象存储生态热词中出现的RustFS(S3)对象存储则代表了另一个方向的诉求——轻量化的S3兼容存储。RustFS这类项目的特点是用Rust实现部署简单资源占用低接口兼容S3协议。适合个人开发者、小团队、边缘节点等对存储容量和并发要求不极端的场景。比如你在内网自建一个开发测试环境或者做一个离线数据归档中台又或者需要在后端服务旁边挂一个本地S3兼容存储这类轻量实现就非常合适。但这里要给一个忠告S3兼容不等于OSS兼容。OSS的API虽然兼容S3大部分语义但两者在ACL细节、服务端加密、Bucket Policy语法、错误码返回值上仍然存在差异。如果用RustFS做开发调试、然后直接部署到云上OSS一定要做好API差异的回归测试尤其是之前提到的PutObjectAcl这类边界行为——极有可能在RustFS上正常到了OSS上就被拒绝。4.3 选型决策表什么情况下选哪种对象存储我把常见的对象存储选型决策整理成一张表方便对照场景特征推荐选择理由中小团队快速上线不想投入运维精力云上OSS开箱即用SLA有保障工具链完整已有大量物理机追求统一存储底座Ceph同时提供块、文件、对象三种存储形态边缘节点、本地开发、内网离线场景RustFS等轻量S3实现部署成本低资源占用小安全等级高需要完全内网隔离自建Ceph/RustFS数据不出内网合规性好大量小文件高并发读取云上OSS CDN边缘节点缓存降低回源压力选型决策其实没有最好只有最适合。如果团队没有专职运维、业务快速增长优先选云上OSS如果公司有大量闲置物理机且能养起一个存储运维团队自建Ceph才有意义。至于RustFS这类轻量实现更多的是一个补充选项而不是替代方案。5. 从上传链路到业务代码OSS接入的完整性避坑清单5.1 文件上传的完整流程与异常处理设计一个健壮的OSS上传流程不只是调一个PutObject接口那么简单。完整的业务链路其实包括六个环节客户端发起上传请求携带业务上下文比如用户ID、文件类型、文件大小。服务端校验用户身份、业务合法性、文件类型与大小限制。服务端签发STS临时凭证或生成上传签名URL。客户端携带临时凭证直传OSS。OSS上传完成触发回调通知服务端或者客户端上报上传结果由服务端主动验证文件是否存在。服务端记录文件元信息到业务数据库。在实际项目中第5步很多团队会做得比较粗糙。最常见的问题是客户端说上传成功但服务端数据库里根本没有这条记录最后用户以为发布成功了实际上页面显示不出来。要规避这个问题最好启用OSS的上传回调Callback功能让OSS在上传完成后主动请求你的业务服务服务端收到回调并持久化成功后再通知客户端上传成功。这样一来上传结果就不依赖客户端自报可靠性高很多。异常处理上也值得多花心思。上传失败并不能都靠简单的重试解决需要按错误类型做不同处理。比如SignatureDoesNotMatch大概率是密钥或时间戳问题盲目重试没有意义RequestTimeout则可以退避重试AccessDenied则需要先确认权限配置。在设计上建议将OSS异常类型做一次包装统一返回给上层业务而不是把一堆SDK异常堆到Controller层。5.2 资源管理与成本控制对象存储的成本控制往往是被忽视的重点。很多团队直到月底看到账单才意识到自己为那些永远没被访问的对象付了太多钱。优化方向有几点合理配置生命周期规则。对日志类、备份类数据在一段时间后自动转义到低频访问存储类型或归档存储类型可以显著降低存储成本。开启服务端加密。不管Bucket内数据是否敏感都建议开启服务端加密。OSS支持KMS托管密钥、OSS完全托管密钥等多种方式加密几乎不影响读写性能但对合规性帮助很大。定期清理未完成的分片上传任务。大文件上传会调用InitiateMultipartUpload创建分片任务如果中途客户端断开分片会一直占着存储空间。一定要在代码中记录并定期清理这些孤儿分片。图片和视频类资源加上处理参数或CDN加速。不要直接用原图URL对外服务一方面浪费流量另一方面拖慢用户访问速度。5.3 一个可以直接复用的上传代码骨架为了让大家少踩一些坑这里分享一段我在项目中常用的服务端上传代码骨架以Java为例使用阿里云OSS SDKpublic String uploadToOss(InputStream inputStream, String fileName, String contentType) { // 1. 构建OSSClient指定Endpoint OSS ossClient new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret); try { // 2. 生成唯一的Object Key避免文件覆盖 String objectKey uploads/ UUID.randomUUID().toString() - fileName; ObjectMetadata metadata new ObjectMetadata(); metadata.setContentType(contentType); metadata.setContentLength(inputStream.available()); // 3. 调用PutObject上传 ossClient.putObject(bucketName, objectKey, inputStream, metadata); // 4. 构造文件访问URL return https:// bucketName . endpoint / objectKey; } catch (Exception e) { // 按OSS异常类型分类处理抛给上层业务 throw new BizException(文件上传失败, e); } finally { // 5. 关闭OSSClient重要 ossClient.shutdown(); } }有几个细节值得强调Object Key中使用了UUID拼接目的是避免同名文件互相覆盖。如果确实需要保留原始文件名也要确保业务上不存在重名冲突。shutdown()务必放在finally里否则OSSClient持有的连接资源会一直不释放高并发场景下可能会拖垮应用。在有STS临时凭证的场景下accessKeyId、accessKeySecret、securityToken三个参数都要传给OSSClientBuilder不能只传两个。另外现在官方推荐使用OSS Java SDK 3.x版本API更规范、性能也更好。老项目如果还在用2.x甚至更低版本建议尽早迁移。5.4 权限、域名、HTTPS与安全配置的补充建议OSS接入很容易忽略HTTPS和自定义域名问题。尤其当你启用了CDN加速后如果CDN回源使用HTTP、而客户端访问使用HTTPS中间环节可能出现协议不一致导致的部分资源加载失败。建议所有回源请求都开启HTTPS并且配置自定义域名时将证书托管到CDN或OSS侧。还有一个安全细节不要将Bucket名称设置为容易猜测的字符串组合。虽然Bucket名称本身不等于安全边界但使用无规律前缀能显著降低被恶意扫描的命中概率。更关键的是凡是匿名访问的公开Bucket务必开启内容合规检测和访问日志万一出现违规内容上传你可以快速定位到来源IP和时间点。6. OSS对象存储使用中容易踩中的隐性坑清单6.1 上传时内容类型导致文件在浏览器中被下载很多人会忽略ContentTypeMIME类型的设置。如果你用默认配置上传一个.png文件但OSS服务端识别到的ContentType是application/octet-stream那么用户通过浏览器访问这个图片URL时浏览器可能会直接下载而不是预览。解决这个问题有两种方案上传时显式在ObjectMetadata中设置ContentType或者在上传完成后调用CopyObject并带上新的ContentType来修正元数据。推荐第一种方案因为第二种会产生额外的一次API调用。6.2 跨域访问导致浏览器侧上传失败前端直传OSS时最常见的问题之一是CORS跨域资源共享未配置。浏览器里报错通常是No Access-Control-Allow-Origin header is present而服务端日志一切正常因为请求根本没到达或响应被浏览器拦截了。OSS控制台里需要为Bucket配置CORS规则指定允许的来源Origin、允许的方法GET、PUT等、允许的Headers通常设置为*或根据SDK要求配置以及暴露的Headers。这个配置非常容易被忽略但少了它前端直传方案就完全跑不起来。6.3 大文件上传必须启用分片和断点续传OSS的PutObject接口要求文件在请求体内一次传完不适合大文件比如超过100MB。推荐的做法是使用UploadFile接口实现分片上传和断点续传。这也是我强调不要自己手写分片逻辑的原因——官方SDK的分片、并发控制、失败重试、断点记录已经非常成熟自己造轮子只会增加故障点。6.4 访问控制列表中公共读写的诱惑与代价最后一个提醒也是很多安全事件的高发原因不要在生产环境将Bucket ACL设置为public-read-write。公共读写意味着任何知道URL的人都能对任意Object进行上传、覆盖、删除操作。一旦被恶意利用轻则资源被塞满产生巨额账单重则整个Bucket内容被清空。如果确实需要临时开放公共写权限比如进行数据迁移应该设置短期策略并在完成后及时关闭同时全程开启访问日志。7. 排障复盘当重试解决不了问题时应当如何建立系统性排查思路接触OSS这几年我逐渐总结出一套适用于对象存储类问题的排障方法论。当重试解决不了问题时按下面几个顺序逐层排查通常不会走弯路。第一步确认错误码。不要只记AccessDenied或RequestTimeout这种粗糙结果要打开SDK的Debug级别日志拿到完整的OSS错误码和RequestId。每个RequestId对应服务端一条完整请求链路的日志极利于和售后或自建服务比对。第二步确认请求参数。检查Endpoint、Bucket名称、Object Key、Region是否匹配。很多诡异问题最后都出现在这类基础配置上。第三步确认身份与权限。按照我前面讲的层次模型逐层核对RAM Policy、Bucket Policy、Bucket ACL、Object ACL、STS Token是否过期哪个环节拒绝你了。第四步确认网络链路。如果是内网环境检查VPC Endpoint是否可达如果是公网检查防火墙和代理是否拦截了HTTPS请求。第五步确认SDK版本和依赖冲突。OSS SDK某些旧版本会存在已知Bug比如旧版Java SDK与高版本Jackson依赖冲突导致序列化异常。升级SDK前做好回归测试即可。这套排查方法虽然朴素但在云服务黑盒场景下非常可靠。毕竟OSS本身作为服务端你能掌控的只有你自己的配置和代码。绝大多数问题的根因终究还是在这两端。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进