STS临时凭证机制详解:如何用RAM角色彻底告别AccessKey泄露风险
我们团队之前出过一次安全事故根因不复杂某位开发同学图省事把某个云厂商账号的 AccessKey 直接写在了项目配置文件里代码仓库又是内部共享的结果被扫描工具扒了出来当晚就被拿去刷了一台高配实例账单上多了一笔刺眼的扣款。从那天起我就开始把所有涉及云资源的调用全部改成了 Security Token Service也就是 STS。这篇文章就围绕“STS是什么、怎么用”展开把我在实际项目中配置 STS 的完整过程、踩过的坑、以及现在仍在沿用的最佳实践一次性写清楚。不管你是刚开始接触云安全的开发者还是正在给团队设计凭证体系的运维同学这篇内容应该都能给你省下不少弯路。1. STS到底解决什么问题——从长期凭证的痛点说起很多人第一次看到 STS 三个字母都会下意识把它归类为“又一种云厂商的安全产品”。其实它本质上就是云平台提供的一套临时凭证颁发服务全称是 Security Token Service。它的核心价值只有一句话让你的应用用临时凭证去访问云资源而不是把长期有效的 AccessKey 暴露在代码里。1.1 长期凭证的三大痛点每一个都让人睡不着觉先说痛点。过去我们访问云资源方式非常直接——在代码里配置一对 AccessKeyId 和 AccessKeySecret然后用它初始化各种云产品的 SDK。这玩意儿一旦创建出来除非你手动删除否则它永远有效。这就会带来三个让人头疼的问题。第一个是泄露风险。只要密钥进了代码仓库、日志、配置文件或者环境变量它就随时可能被别人看到。很多公司的代码仓库并不是完全封闭的外包、合作伙伴、离职员工都可能接触过历史版本的代码。GitHub 上甚至有专门的爬虫项目在扫描公开仓库里的密钥一旦扫到几小时内就能看到异常账单。这里我有一个惨痛经验在整改之前我们全公司搜索一遍找到的 AccessKey 数量有三位数其中相当一部分还能访问生产环境。第二个是权限失控。很多团队创建密钥的时候图省事直接给账号绑一个“ AdministratorAccess”级别的系统策略意味着这串字符持有整个账号的管理员权限。更可怕的是企业账号下面往往还有子账号每个子账号又能创建多个密钥每个密钥权限都极大。这种权限设计一旦出问题根本没办法收口。第三个是轮转困难。安全审计要求定期更换密钥但很多业务系统不敢换因为 AccessKey 一旦更换所有用旧密钥初始化的客户端都会断掉需要重新部署。在微服务架构下涉及的服务可能有几十上百个每一次轮转都是一次大型上线出错概率极高。所以很多时候团队明知密钥该换了也拖着不动安全风险越积越大。1.2 STS 的思路给应用发一张临时通行证那 STS 是换了一种什么思路呢我用一个生活化的类比来解释。你去某栋大楼办事如果保安给你一把万能钥匙你可以随时打开大楼里任何一间办公室的门这相当于长期密钥权限极大且不敢轻易更换。而 STS 做的事情是让保安根据你的来访目的给你一张写了有效期的临时通行证比如“仅限今天 14:00 到 16:00 期间允许进入 3 层会议室和 5 层财务室。逾期无效。”有了这张通行证你能完成工作但不会触碰无关区域过期以后通行证自己就废了也不用担心丢失的问题。落到技术上就是应用不再持有任何长期有效的云账号密钥而是每次需要访问云资源时先去调用 STS 服务换回一组临时凭证。这组凭证包含三个元素临时 AccessKeyId、临时 AccessKeySecret、以及一个 SecurityToken。云服务的 SDK 在鉴权的时候看到 SecurityToken 就知道这是临时凭证就会额外检查它的有效期和适用范围。这种做法带来的好处非常直接即使临时凭证泄露攻击者也只能在极短的时间内、用极其有限的操作权限搞破坏。而长期密钥彻底不用出现在代码里了。从运维角度来讲我们也彻底摆脱了“定期给所有服务换密钥”这个噩梦因为临时凭证是应用自动申请的根本不涉及人工更换。2. 吃透 STS 的三个核心概念角色、信任策略、临时凭证很多教程上来就教你怎么调用 API但没把概念讲清楚。结果就是配了不对不对就改改了还不对。我建议先花十分钟把三个核心概念理解透剩下的事情基本都是水到渠成。2.1 角色Role不是一个“人”而是一个“身份牌”STS 体系中最重要的对象是 RAM 角色也被称为云资源访问角色。这个角色和我们常说的“用户”是两个完全不同的概念。用户是长期存在的身份有自己的登录密码和长期密钥它像一个员工有工号、有员工卡、有固定权限。而角色更像一块“身份牌”它本身没有长期密钥任何人都不能直接“登录”使用它它身上绑定了两个关键文档——信任策略和权限策略。权限策略决定“扮演这个角色能做什么”信任策略决定“谁有资格来扮演这个角色”。用一个更容易理解的方式来说角色像一件救生衣。救生衣本身不能划船但任何落水的人穿上它都能浮起来。你只要在规定条件下穿上救生衣就暂时获得了它的浮力权限脱下救生衣凭证过期你就恢复成普通人。2.2 信任策略Trust Policy谁能拿到这张身份牌信任策略是创建角色时必填的一项配置它的本质是一份 JSON 格式的权限策略文档用来声明“哪些帐号下的哪些身份可以扮演这个角色”。举个例子某云厂商默认的信任策略会写成类似这样{ Version: 1, Statement: [ { Effect: Allow, Action: sts:AssumeRole, Principal: { RAM: acs:ram::1234567890123456:root } } ] }注意看Principal字段它表示的是角色所属的账号ID即只有这个账号下的身份才有资格扮演该角色。如果还想限制某个具体子账号可以进一步写成acs:ram::1234567890123456:user/某开发者这样只有指定的子账号用户能扮演这个角色。这层设计非常关键。没有信任策略角色就是一个空壳信任策略配得太宽就相当于把大门钥匙放在门口地毯下面。我见过有团队把Principal配置成全局通配符那等于任何账号的合法身份都能扮演你的角色这是严重的越权漏洞。2.3 临时凭证三件套它和长期密钥到底差在哪当你通过 STS 的AssumeRole接口成功扮演一个角色之后云平台会返回一组临时凭证格式如下Credentials: AccessKeyId: STS.xxxxx AccessKeySecret: yyyyy SecurityToken: zzzzz Expiration: 2024-06-01T12:00:00Z这组凭证有几个明显特点。其一有效期很短默认从 900 秒15 分钟到 43200 秒12 小时不等具体看你的角色配置。其二必须三个值同时使用SDK 在鉴权时看见 SecurityToken 字段会先校验临时凭证是否在有效期内再做权限判定。其三权限完全由角色决定即便这个临时凭证被恶意截获攻击者能做的事情也极其有限。还有一个细节值得留意通过 STS 返回的临时 AccessKeyId 通常会带有 STS 前缀例如STS.xxxxxxxx。如果你的日志或者监控系统里需要排查到底是谁调用了 API看到这个前缀就能立刻反应过来是临时凭证方便追查整个请求链路。3. 从零配置 STS一份可以直接照抄的实操流程概念讲清楚了接下来进入实战环节。我用的是某云平台的控制台加 Python SDK 来演示但整个流程在所有主流云平台上是完全相通的。你只需要寻找到对应的入口把名字对应起来即可。整个配置过程分四步创建角色、配置信任策略、创建用于调用 STS 的身份、在代码中换取临时凭证。3.1 创建角色并绑定最小权限策略第一步登录云平台控制台进入访问控制RAM模块选择“角色”然后点击“创建角色”。创建角色时角色类型要选“自定义角色”或“普通服务角色”具体名称根据你的场景来定比如oss-readonly-role只读访问对象存储。创建完成之后角色是空的没有任何权限接下来要给它绑定权限策略。权限策略遵循一个铁律最小权限原则。你的应用只需要读对象存储就不要给它写权限只需要访问一台服务器就不要给它开其他产品的权限。在控制台上选择“精确授权”找到对应的权限策略例如AliyunOSSReadOnlyAccess绑定到角色上就好了。一个角色可以绑定多个权限策略但不要太贪心每一条权限都意味着风险敞口的扩大。3.2 配置信任策略允许应用所在身份扮演该角色角色创建好后导航到“信任策略管理”把本文 2.2 节那段 JSON 填进去。这里有一个非常关键的决策点你的应用是通过子账号还是通过另一个云产品服务来扮演角色的如果应用跑在云服务器上有一个更优的选择给这台服务器绑定一个实例 RAM 角色。这样应用甚至不需要配置任何 AccessKeySDK 直接从实例元数据服务中自动获取临时凭证。整个流程对代码完全透明安全等级也是最高的。如果应用部署在本地机房或者其他非云环境那你就需要创建一个 RAM 子账号再把Principal配置成这个子账号。配置如下{ Statement: [ { Effect: Allow, Action: sts:AssumeRole, Principal: { RAM: acs:ram::1234567890123456:user/sts-bot } } ] }这里的sts-bot是专门为 STS 调用创建的独立子账号它唯一的职责就是调用AssumeRole换取临时凭证。这个子账号本身不要授予任何操作云资源的权限因为真正干活的权限都集中在角色身上。3.3 在应用中调用 AssumeRole 换取临时凭证配置完成后就进入代码环节。下面是我在项目中使用的一段 Python 示例注释写得很详细大家可以按自己的 SDK 版本稍作调整。以某 Python SDK 为例import oss2 from aliyunsdkcore.client import AcsClient from aliyunsdksts.request.v20150401 import AssumeRoleRequest # 步骤1用子账号的长期AccessKey创建STS客户端 # 注意这里的AccessKey权限极小只具备调用AssumeRole的权限 sts_client AcsClient( ak, # 子账号AccessKeyId secret, # 子账号AccessKeySecret cn-hangzhou # 地域根据实际资源所在地调整 ) # 步骤2构造AssumeRole请求 request AssumeRoleRequest.AssumeRoleRequest() request.set_RoleArn(acs:ram::1234567890123456:role/oss-readonly-role) # 角色Arn request.set_RoleSessionName(app-server-01) # 会话名称便于审计 request.set_DurationSeconds(3600) # 临时凭证有效期1小时 # 步骤3调用接口获取临时凭证 response sts_client.do_action_with_exception(request) credentials response.get(Credentials) # 步骤4使用临时凭证初始化业务客户端 auth oss2.StsAuth( credentials.get(AccessKeyId), credentials.get(AccessKeySecret), credentials.get(SecurityToken) ) bucket oss2.Bucket(auth, oss-cn-hangzhou.aliyuncs.com, my-bucket) # 之后所有的OSS读写操作都使用bucket对象完成这段代码里有几个细节要特别说明。第一RoleArn是角色的全局资源名格式是acs:ram::账号ID:role/角色名在角色详情页可以复制。这个字符串是调用 AssumeRole 接口的核心参数写错了会直接报InvalidParameter.RoleArn错误。第二RoleSessionName是会话名称一个自定义字符串建议带上应用服务的标识比如app-server-01。它在云平台的操作审计日志中会显示为“扮演角色者”将来排查问题时你能靠它快速追溯到具体是哪个服务在调用。如果不设置或随意设置出了问题基本没办法定位。第三DurationSeconds决定临时凭证存活时间可以按业务需求设但不要盲目往大了设。我的经验是普通的周期任务设 1 小时足够如果应用本身有自动搜凭证的机制可以设 15 分钟到 30 分钟。临时凭证的寿命越短泄露后的危害窗口越窄。3.4 凭证的生命周期管理让它自动轮转拿到临时凭证后很多人会犯一个错误把临时凭证也存到一个全局变量里长期复用。其实这失去了 STS 的意义。临时凭证应该有明确的过期机制应用应该在过期之前主动换取新的。比较好的做法是写一个简单的凭证管理器允许并发调用时共用同一个临时凭证同时维护一个过期时间戳。每次需要获取凭证的时候检查是否过期如果剩余时间少于 5 分钟就重新调用 AssumeRole 接口刷新。这种设计有两个好处减少 STS 接口的调用频率避免被限流同时确保凭证在任何时候都是新鲜的。下面的 Python 是一个简化版的凭证刷新逻辑import time import threading class StsTokenManager: def __init__(self, sts_client, role_arn, session_name, duration3600): self._client sts_client self._role_arn role_arn self._session_name session_name self._duration duration self._expire_at 0 self._credentials None self._lock threading.Lock() def get_credentials(self): # 锁保证多线程环境下不会反复换取凭证 with self._lock: if time.time() 300 self._expire_at: # 少于5分钟过期则刷新 self._refresh() return self._credentials def _refresh(self): request AssumeRoleRequest.AssumeRoleRequest() request.set_RoleArn(self._role_arn) request.set_RoleSessionName(self._session_name) request.set_DurationSeconds(self._duration) response self._client.do_action_with_exception(request) self._credentials response.get(Credentials) expire_time self._credentials.get(Expiration) # 将Expiration字符串转为时间戳存储, 这里省略具体实现 self._expire_at parse_expire_time(expire_time)这套逻辑执行起来非常稳定。团队在代码里只需要引用一个token_manager的单例对象所有需要访问云资源的模块都从它那拿凭证安全性和可维护性都拉满。4. 常见问题与排查实录这些坑我替你们踩过了STS 的配置流程不算长但实际跑起来会遇到不少报错。我把这两年项目中出现的高频问题整理成一张速查表并在后面详细分析两个印象最深的案例。报错信息问题根源解决办法AccessDenied: You are not authorized to perform sts:AssumeRole调用方没有 STS 接口的调用权限给子账号绑定包含sts:AssumeRole权限的策略InvalidParameter.RoleArn角色 Arn 字符串填写错误到控制台角色详情页复制完整的 ArnForbidden: The role you are assuming is not enabled for STS角色的信任策略未配置正确检查角色的信任策略中是否存在允许该身份扮演的规则The SecurityToken included in the request is invalid临时凭证过期或用错地方检查系统时间确认是否过期重新换取凭证EntityExists: The same role session name existsRoleSessionName 重复触发了 STS 限制给每个会话设置不同的会话名称或增加随机后缀临时凭证权限生效延迟权限策略刚修改未完全生效等待一小段时间后重试通常几秒钟到一分钟4.1 问题一子账号明明有角色为什么还提示没有权限第一次配置时我很自然地创建了一个子账号把角色的信任策略Principal指向了它然后在代码里用子账号的 AccessKey 去调用 AssumeRole。结果系统直接抛了AccessDenied的错误。当时我第一反应是信任策略写错了反复检查 JSON 格式最后发现原因非常简单子账号自身没有调用 STS 接口的权限。这就好比你想进某栋大楼的会议室会议室门禁系统已经录入了你的指纹信任策略允许但你连大楼的旋转门都进不去子账号没有调用 STS 的权限。解决方案是在子账号上额外绑定一个自定义权限策略内容如下{ Statement: [ { Effect: Allow, Action: sts:AssumeRole, Resource: * } ] }注意这里的Resource可以写成具体某个角色的 Arn也可以写成*。从安全性出发建议写成多个角色 Arn 的列表这样即使子账号被泄露攻击者也没法随便去扮演其他更敏感的角色。这个小细节在当时看是小事事后复盘时觉得价值巨大——STS 体系的精细化管控就藏在这些不起眼的配置里。4.2 问题二临时凭证似乎总是提前失效还有一次我们的定时任务运行到一半突然报错提示The SecurityToken included in the request is invalid。我们排查了一圈最后发现问题出在服务器时钟上。那台服务器的系统时间比真实时间快了将近 6 分钟而临时凭证的剩余有效时间本来就不长再加上请求在网络中的传输延迟系统判定请求已过期。STS 服务对时间偏移的容忍度通常只有几十秒服务器时间不准凭证刷新再及时也没用。从那以后我们把所有服务器的 NTP 时间同步设置为强制检查项并写入公司的运维基线。这个坑值得所有做 STS 集成的团队注意先看服务器时间准不准再去怀疑代码逻辑。4.3 问题三修改权限策略后临时凭证没有立即生效在某些场景下角色的权限策略改了以后本该立刻生效的新功能还是报权限不足。这个问题的根源在于 STS 的鉴权链路存在轻微缓存。正常情况下权限策略变更会在几秒到一分钟内同步到鉴权节点但如果遇到网络延迟或全局缓存刷新可能需要稍微等一会儿。遇到这种情况不要反复重试等一个短周期再验证即可。另外可以确认一下是不是同一个角色被多个服务同时使用某个服务还持有旧的临时凭证。我们曾经因为两个模块共用同一个角色A 模块改了权限策略B 模块依然拿着旧会话的老凭证在跑排查过程绕了不少弯。分区管理角色别让所有应用共用同一个角色是值得坚持的设计原则。5. 把 STS 用得既稳又安全我的一些最佳实践与心得上面这些步骤和问题排查解决的是“能不能用起来”的问题。但在生产环境长期运行还需要考虑更多层面。以下几条是我个人在这些年项目经验中沉淀下来的操作规范你可以结合自己的业务来参考。5.1 角色的设计要贴近业务边界不要让一个角色承载所有的事。比如读对象存储和写数据库这是两种完全不同的场景就应该拆成两个角色。拆开之后的好处很明显某个角色的信任策略要调整时影响面是可控的某个角色的权限被滥用时审计也能更清晰。这也符合最小权限原则的延伸——每个应用只能扮演它业务所需的角色无法越权去扮演其他角色的凭证。5.2 会话名称一定要有业务语义RoleSessionName这个字段在设计时很不起眼但它是审计的金钥匙。我建议统一使用“业务模块-环境-节点”的命名方式例如report-service-prod-01。这样当你在操作审计里发现某个角色在凌晨三点被调用时一眼就能判断是不是正常行为。如果不设置会话名称或者只是随意写一个无意义的字符串在安全追溯环节就是一场灾难。5.3 临时凭证长度要按业务场景定制有一些团队习惯把DurationSeconds统一设成 12 小时最大值理由是省得频繁刷新。这个习惯我极度不推荐。凭证有效期越长泄露后的窗口期越大。比较合理的策略是分层面向用户请求的在线服务15 分钟到 30 分钟靠自动刷新机制保障连续可用周期性批处理任务按任务最长执行时间设置比如 1 小时不要给余量留太大需要人工介入的运维操作30 分钟以内操作完立即失效这个策略既保障了业务的连贯性又把风险控制在一个可接受的范围。5.4 给子账号和角色设置独立的风险监控STS 的临时凭证是一把利器但管理不好也会生锈。我建议在云平台的监控告警中为 STS 相关接口添加两条独立告警一条是针对AssumeRole调用频率的异常检测另一条是针对某个具体角色被不同来源 IP 频繁扮演的告警。这两条规则能帮你捕捉到大部分异常行为。即使不设置这么精细的告警至少也要保证操作审计日志是开启状态并且定期导出保存。临时凭证的特点是“短平快”没有审计记录事后想查基本束手无策。5.5 能不用长期 AccessKey 的地方坚决不用在 STS 体系之外还有一个容易被忽略的安全层代码仓库中的密钥扫描。把长期 AccessKey 彻底请出代码仓库后我们还在 CI 流水线中加入了密钥扫描步骤任何包含形如 AccessKey 的内容都不允许合入主干分支。这就从源头上让密钥没法进入业务代码。我们内部还规定了一个原则凡是新写的服务一律使用 STS 临时凭证方案不接受新的长期密钥申请存量系统中的长期密钥逐个业务进行替换并设定了明确的完成期限。我个人的体会是STS 不是一个需要“懂原理但用不上”的功能而是云上应用的凭证安全底座。尤其是多云架构和微服务成为标配的今天密钥管理的问题只会越来越突出。与其等出了事故再亡羊补牢不如现在就把这套临时凭证机制用起来。配置过程不复杂收益却是实打实的——至少以后再也不用半夜爬起来处理密钥泄露也不用每次安全合规扫描的时候提心吊胆地批量给几十个服务换密钥了。